getcord / cord

The complete SDK for Chat, Commenting, and Notifications
Apache License 2.0
68 stars 25 forks source link

Make Admin tool accessible without Cord Slack integration #4

Open dmmiller opened 2 months ago

jwatzman commented 2 weeks ago

This hacky patch will make the SDK testbed "andrei" user an admin, and then log you into the admin console as him. This requires you to have loaded the SDK testbed at least once before loading the admin page. While Cord exposed the admin page at admin.cord.com and so needed a real login there, for anyone self-deploying this, it might be sufficient to just firewall the admin port so that you can only access it internally, in which case you don't need "real" auth, so this might be good enough for you. Then again, it very possibly is not, hence why I'm dumping it here as an example and not making a PR.

diff --git a/server/src/admin/routes/SlackLoginHandler.ts b/server/src/admin/routes/SlackLoginHandler.ts
index f0d1cdc..748254f 100644
--- a/server/src/admin/routes/SlackLoginHandler.ts
+++ b/server/src/admin/routes/SlackLoginHandler.ts
@@ -1,24 +1,15 @@
 import * as url from 'url';
-import * as Slack from '@slack/web-api';
 import type { Request, Response } from 'express';
-import * as jwt from 'jsonwebtoken';
-import * as cookie from 'cookie';

 import env from 'server/src/config/Env.ts';
 import { Viewer } from 'server/src/auth/index.ts';
 import { encodeSessionToJWT } from 'server/src/auth/encodeSessionToJWT.ts';
-import { OrgLoader } from 'server/src/entity/org/OrgLoader.ts';
 import { ADMIN_SERVER_HOST, APP_ORIGIN } from 'common/const/Urls.ts';
 import { getAuthorizationHeaderWithToken } from 'common/auth/index.ts';
-import {
-  CORD_SLACK_TEAM_ID,
-  SLACK_ADMIN_LOGIN_APP_CLIENT_ID,
-  SLACK_ADMIN_LOGIN_APP_ID,
-} from 'common/const/Ids.ts';
+import { CORD_SDK_TEST_APPLICATION_ID } from 'common/const/Ids.ts';
 import { anonymousLogger } from 'server/src/logging/Logger.ts';
-import { UserLoader } from 'server/src/entity/user/UserLoader.ts';
-
-const slackClient = new Slack.WebClient();
+import { OrgEntity } from 'server/src/entity/org/OrgEntity.ts';
+import { UserEntity } from 'server/src/entity/user/UserEntity.ts';

 export const SLACK_LOGIN_ROUTE = 'login/slack';

@@ -35,132 +26,28 @@ export const ADMIN_LOGIN_SLACK_REDIRECT_URL = url.format({
 const ADMIN_SESSION_EXPIRATION_SECONDS = 60 * 60 * 24; // valid for 24 hours

 export default async function SlackLoginHandler(req: Request, res: Response) {
-  const { code, state } = req.query;
-  if (typeof state !== 'string' || typeof code !== 'string') {
-    return res.redirect(APP_ORIGIN);
-  }
-
-  let finalDestination: string | undefined = undefined;
-
-  try {
-    const { host, redirect_to } = jwt.verify(
-      state,
-      env.OAUTH_STATE_SIGNING_SECRET,
-      {
-        algorithms: ['HS512'],
-      },
-    ) as { host: string; redirect_to?: string };
-
-    if (host !== ADMIN_SERVER_HOST) {
-      return res.redirect(
-        url.format({
-          protocol: 'https',
-          host,
-          pathname: SLACK_LOGIN_ROUTE,
-          query: { code, state },
-        }),
-      );
-    }
-    finalDestination = redirect_to;
-  } catch (e) {
-    anonymousLogger().logException(
-      'SlackLoginHandler',
-      e,
-      undefined,
-      undefined,
-      'warn',
-    );
-    return res.redirect(APP_ORIGIN);
-  }
-
-  const cookieNonce = cookie.parse(req.header('Cookie') || '')[
-    'admin_nonce'
-  ] as string | undefined;
-  if (!cookieNonce) {
-    anonymousLogger().warn('SlackLoginHandler', {
-      message: 'missing admin_nonce cookie',
-    });
-
-    return res.redirect(APP_ORIGIN);
-  }
-
   try {
-    const response = await slackClient.openid.connect.token({
-      code,
-      client_id: SLACK_ADMIN_LOGIN_APP_CLIENT_ID,
-      client_secret: env.SLACK_ADMIN_CLIENT_SECRET,
-      redirect_uri: ADMIN_LOGIN_SLACK_REDIRECT_URL,
-    });
-
-    if (!response.ok || !response.id_token) {
-      anonymousLogger().warn('SlackLoginHandler', {
-        message: 'slack oauth failed',
-        ...response,
-      });
-
-      return res.redirect(APP_ORIGIN);
-    }
-
-    const userInfo = jwt.decode(response.id_token) as { [key: string]: string };
-    const {
-      'https://slack.com/user_id': user_id,
-      'https://slack.com/team_id': team_id,
-      nonce,
-    } = userInfo;
-
-    if (nonce !== cookieNonce) {
-      anonymousLogger().warn('SlackLoginHandler', {
-        message: 'wrong nonce',
-      });
-
-      return res.redirect(APP_ORIGIN);
-    }
-
-    if (team_id !== CORD_SLACK_TEAM_ID) {
-      anonymousLogger().warn('SlackLoginHandler', {
-        message: `logged in with wrong slack team ${team_id}`,
-      });
-
-      return res.redirect(APP_ORIGIN);
-    }
-
-    const org = await new OrgLoader(Viewer.createServiceViewer()).loadSlackOrg(
-      team_id,
-      SLACK_ADMIN_LOGIN_APP_ID,
-    );
-
-    if (!org) {
-      anonymousLogger().warn('SlackLoginHandler', {
-        message: `org not found for team ${team_id}`,
-      });
-
-      return res.redirect(APP_ORIGIN);
-    }
-
-    const user = await new UserLoader(
-      Viewer.createOrgViewer(org.id),
-      () => null,
-    ).loadUserForSlackUserWithinViewerOrg(user_id);
-
-    if (!user) {
-      anonymousLogger().warn('SlackLoginHandler', {
-        message: `user not found for user ${user_id} in org ${org.id}`,
-      });
-
-      return res.redirect(APP_ORIGIN);
-    }
-
-    if (!user?.admin) {
-      anonymousLogger().warn('SlackLoginHandler', {
-        message: `user ${user_id} not found or not admin`,
-      });
-
-      return res.redirect(APP_ORIGIN);
-    }
+    const [user, org] = await Promise.all([
+      UserEntity.findOne({
+        where: {
+          platformApplicationID: CORD_SDK_TEST_APPLICATION_ID,
+          externalID: 'andrei',
+        },
+      }),
+      OrgEntity.findOne({
+        where: {
+          platformApplicationID: CORD_SDK_TEST_APPLICATION_ID,
+          externalID: 'cord',
+        },
+      }),
+    ]);
+
+    user!.admin = true;
+    await user!.save();

     const token = encodeSessionToJWT(
       {
-        viewer: Viewer.createLoggedInViewer(user.id, org.id),
+        viewer: Viewer.createLoggedInViewer(user!.id, org!.id),
       },
       ADMIN_SESSION_EXPIRATION_SECONDS,
     );
@@ -178,7 +65,7 @@ export default async function SlackLoginHandler(req: Request, res: Response) {
         sameSite: 'lax', // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#lax
         maxAge: 1000 * ADMIN_SESSION_EXPIRATION_SECONDS,
       })
-      .redirect(finalDestination ?? '/');
+      .redirect('/');
   } catch (e) {
     anonymousLogger().logException(
       'SlackLoginHandler',
diff --git a/server/src/admin/server.ts b/server/src/admin/server.ts
index 03e787a..016ae4a 100644
--- a/server/src/admin/server.ts
+++ b/server/src/admin/server.ts
@@ -25,7 +25,6 @@ import SlackLoginHandler, {
   SLACK_LOGIN_ROUTE,
 } from 'server/src/admin/routes/SlackLoginHandler.ts';
 import type { RequestWithContext } from 'server/src/RequestContext.ts';
-import { slackAdminLoginURL } from 'server/src/slack/util.ts';
 import type { Session } from 'server/src/auth/index.ts';
 import { getSessionFromAuthHeader } from 'server/src/auth/session.ts';
 import {
@@ -103,7 +102,7 @@ export async function adminMain(port: ListenPort) {
           secure: true,
           sameSite: 'lax',
         })
-        .redirect(slackAdminLoginURL(nonce, req.originalUrl));
+        .redirect(`/${SLACK_LOGIN_ROUTE}`);
     }