From 7aff924e01cc8e2de1b462722a34857b659704e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Pyykk=C3=B6?= Date: Wed, 12 Apr 2023 20:24:34 +0300 Subject: [PATCH] Get auth to actually work on graphql subscription (#1163) get auth to actually work on graphql subscription --- backend/accessControl.ts | 8 +-- backend/context.ts | 1 + backend/graphql/User/queries.ts | 6 +- backend/middlewares/fetchUser.ts | 55 ++++++++++++------- backend/patches/nexus+1.3.0.patch | 29 ++++++++++ backend/server.ts | 51 ++++++++--------- .../components/Dashboard/Users/SearchForm.tsx | 8 ++- .../components/Dashboard/Users/WideGrid.tsx | 14 ++++- frontend/lib/with-apollo-client/get-apollo.ts | 9 ++- helm/templates/shibbo/shibbo-deployment.yml | 2 +- helm/values.yaml | 5 +- 11 files changed, 122 insertions(+), 66 deletions(-) create mode 100644 backend/patches/nexus+1.3.0.patch diff --git a/backend/accessControl.ts b/backend/accessControl.ts index 5990612f2..4e8933ad7 100644 --- a/backend/accessControl.ts +++ b/backend/accessControl.ts @@ -1,10 +1,10 @@ import { FieldAuthorizeResolver } from "nexus/dist/plugins/fieldAuthorizePlugin" export enum Role { - USER, - ADMIN, - ORGANIZATION, //for automated scripts, not for accounts - VISITOR, + VISITOR = 0, + USER = 1, + ADMIN = 2, + ORGANIZATION = 3, //for automated scripts, not for accounts } type AuthorizeFunction = ( diff --git a/backend/context.ts b/backend/context.ts index e21178fa6..5bf13e15c 100644 --- a/backend/context.ts +++ b/backend/context.ts @@ -24,6 +24,7 @@ export interface Context extends BaseContext { tmcClient: TmcClient locale?: string req: IncomingMessage + connectionParams?: Record } export interface ServerContext extends BaseContext { diff --git a/backend/graphql/User/queries.ts b/backend/graphql/User/queries.ts index de9356157..36a5e2538 100644 --- a/backend/graphql/User/queries.ts +++ b/backend/graphql/User/queries.ts @@ -9,7 +9,7 @@ import { import { User } from "@prisma/client" -import { isAdmin, Role } from "../../accessControl" +import { isAdmin } from "../../accessControl" import { ForbiddenError, UserInputError } from "../../lib/errors" import { buildUserSearch, convertPagination } from "../../util/db-functions" import { notEmpty } from "../../util/notEmpty" @@ -139,10 +139,6 @@ export const UserSubscriptions = extendType({ }, authorize: isAdmin, subscribe(_, { search }, ctx) { - if (ctx.role !== Role.ADMIN) { - throw new ForbiddenError("Not authorized") - } - const queries = buildUserSearch(search) ?? [] const fieldCount = queries.length diff --git a/backend/middlewares/fetchUser.ts b/backend/middlewares/fetchUser.ts index df954ca80..8fde9502a 100644 --- a/backend/middlewares/fetchUser.ts +++ b/backend/middlewares/fetchUser.ts @@ -1,4 +1,5 @@ import { plugin } from "nexus" +import { MiddlewareFn } from "nexus/dist/plugin" import { Role } from "../accessControl" import { Context } from "../context" @@ -7,29 +8,43 @@ import { redisify } from "../services/redis" import TmcClient from "../services/tmc" import { UserInfo } from "/domain/UserInfo" +const authMiddlewareFn: MiddlewareFn = async ( + root, + args, + ctx: Context, + info, + next, +) => { + if (ctx.userDetails || ctx.organization) { + return next(root, args, ctx, info) + } + + const rawToken = + ctx.req?.headers?.authorization ?? + (ctx.req?.headers?.["Authorization"] as string) ?? + ctx.connectionParams?.authorization ?? + ctx.connectionParams?.["Authorization"] // graphql websocket + // connection? + + if (!rawToken) { + ctx.role = Role.VISITOR + } else if (rawToken.startsWith("Basic")) { + await setContextOrganization(ctx, rawToken) + } else { + await setContextUser(ctx, rawToken) + } + + return next(root, args, ctx, info) +} + export const moocfiAuthPlugin = () => plugin({ name: "moocfiAuthPlugin", onCreateFieldResolver() { - return async (root, args, ctx: Context, info, next) => { - if (ctx.userDetails || ctx.organization) { - return next(root, args, ctx, info) - } - - const rawToken = - ctx.req?.headers?.authorization ?? - (ctx.req?.headers?.["Authorization"] as string) // connection? - - if (!rawToken) { - ctx.role = Role.VISITOR - } else if (rawToken.startsWith("Basic")) { - await setContextOrganization(ctx, rawToken) - } else { - await setContextUser(ctx, rawToken) - } - - return next(root, args, ctx, info) - } + return authMiddlewareFn + }, + onCreateFieldSubscribe() { + return authMiddlewareFn }, }) @@ -67,7 +82,7 @@ const setContextUser = async (ctx: Context, rawToken: string) => { ctx, ) } catch (e) { - // console.log("error", e) + console.log("error", e) } ctx.tmcClient = client diff --git a/backend/patches/nexus+1.3.0.patch b/backend/patches/nexus+1.3.0.patch new file mode 100644 index 000000000..3e31f17c4 --- /dev/null +++ b/backend/patches/nexus+1.3.0.patch @@ -0,0 +1,29 @@ +diff --git a/node_modules/nexus/dist/builder.js b/node_modules/nexus/dist/builder.js +index 656750b..ed7f817 100644 +--- a/node_modules/nexus/dist/builder.js ++++ b/node_modules/nexus/dist/builder.js +@@ -807,8 +807,15 @@ class SchemaBuilder { + parentTypeConfig: typeConfig, + schemaConfig: this.config, + schemaExtension: this.schemaExtension, +- }, fieldConfig.resolve), subscribe: fieldConfig.subscribe }, builderFieldConfig); ++ }, fieldConfig.resolve), subscribe: this.makeFinalResolver({ ++ builder: this.builderLens, ++ fieldConfig: builderFieldConfig, ++ parentTypeConfig: typeConfig, ++ schemaConfig: this.config, ++ schemaExtension: this.schemaExtension, ++ }, fieldConfig.subscribe) }, builderFieldConfig); + } ++ + makeFinalResolver(info, resolver) { + const resolveFn = resolver || graphql_1.defaultFieldResolver; + if (this.onCreateResolverFns.length) { +@@ -819,6 +826,7 @@ class SchemaBuilder { + } + return resolveFn; + } ++ + buildInputObjectField(fieldConfig, typeConfig) { + var _a, _b, _c, _d; + const nonNullDefault = this.getNonNullDefault((_b = (_a = typeConfig.extensions) === null || _a === void 0 ? void 0 : _a.nexus) === null || _b === void 0 ? void 0 : _b.config, 'input'); diff --git a/backend/server.ts b/backend/server.ts index 77774587d..c2b75ed95 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -76,34 +76,6 @@ export default async (serverContext: ServerContext) => { const httpServer = http.createServer(app) const schema = createSchema() - const wsServer = new WebSocketServer({ - server: httpServer, - path: isProduction ? "/api" : "/", - }) - - const serverCleanup = useServer( - { - schema, - context: (ctx) => { - const { prisma, logger, knex, extraContext } = serverContext - - return { - ...ctx, - req: { - headers: { - ...ctx.connectionParams, // compatibility with middleware - }, - }, - prisma, - logger, - knex, - ...extraContext, - } - }, - }, - wsServer, - ) - const apolloServer = new ApolloServer({ schema, plugins: [ @@ -134,6 +106,29 @@ export default async (serverContext: ServerContext) => { }) await apolloServer.start() + const wsServer = new WebSocketServer({ + server: httpServer, + path: isProduction ? "/api" : "/", + }) + + const serverCleanup = useServer( + { + schema, + context: (ctx) => { + const { prisma, logger, knex, extraContext } = serverContext + + return { + ...ctx, + prisma, + logger, + knex, + ...extraContext, + } + }, + }, + wsServer, + ) + useExpressMiddleware(app, apolloServer, serverContext) return { diff --git a/frontend/components/Dashboard/Users/SearchForm.tsx b/frontend/components/Dashboard/Users/SearchForm.tsx index 7c3646ad1..f2afe8d82 100644 --- a/frontend/components/Dashboard/Users/SearchForm.tsx +++ b/frontend/components/Dashboard/Users/SearchForm.tsx @@ -51,7 +51,7 @@ const StyledTableContainer = styled(TableContainer)` width: max-content; ` const StyledFormHelperText = styled(FormHelperText)` - margin: 8px 0; + margin: 8px 0 9px 0; ` const ResultMeta = ({ @@ -139,7 +139,9 @@ const SearchForm = () => { if (meta.finished) { return ( - + 0 ? "3px 0" : undefined }} + > {t("searchFinished")} {meta.count > 0 && ( @@ -156,7 +158,7 @@ const SearchForm = () => { )} - + ) } diff --git a/frontend/components/Dashboard/Users/WideGrid.tsx b/frontend/components/Dashboard/Users/WideGrid.tsx index bf34e5323..f161a22ec 100644 --- a/frontend/components/Dashboard/Users/WideGrid.tsx +++ b/frontend/components/Dashboard/Users/WideGrid.tsx @@ -97,9 +97,10 @@ const WideGrid = () => { const RenderResults = () => { const t = useTranslator(UsersTranslations) - const { data, loading } = useContext(UserSearchContext) + const { data, loading, meta } = useContext(UserSearchContext) const isVeryWide = useMediaQuery("(min-width: 1200px)") const colSpan = 5 + (isVeryWide ? 1 : 0) + if (loading) { return ( @@ -114,14 +115,21 @@ const RenderResults = () => { ) } - if (data.length < 1) + if (data.length < 1) { + if (!meta.finished) { + return null + } + return ( - {t("noResults")} + + {t("noResults")} + ) + } return ( diff --git a/frontend/lib/with-apollo-client/get-apollo.ts b/frontend/lib/with-apollo-client/get-apollo.ts index 081136128..10b3e9f5a 100644 --- a/frontend/lib/with-apollo-client/get-apollo.ts +++ b/frontend/lib/with-apollo-client/get-apollo.ts @@ -125,6 +125,13 @@ function create( ? new GraphQLWsLink( createClient({ url: production ? "wss://www.mooc.fi/api" : "ws://localhost:4000", + connectionParams: () => { + const accessToken = nookies.get()["access_token"] + if (accessToken) { + return { authorization: `Bearer ${accessToken}` } + } + return {} + }, }), ) : null @@ -162,7 +169,7 @@ function create( error: (error) => { // ... console.log("error", error) - // observer.error(error) + observer.error(error) }, }) }) diff --git a/helm/templates/shibbo/shibbo-deployment.yml b/helm/templates/shibbo/shibbo-deployment.yml index 76ab92854..a0e1e47dd 100644 --- a/helm/templates/shibbo/shibbo-deployment.yml +++ b/helm/templates/shibbo/shibbo-deployment.yml @@ -6,7 +6,7 @@ metadata: {{- include "helm.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.shibboTest.replicaCount }} {{- end }} selector: matchLabels: diff --git a/helm/values.yaml b/helm/values.yaml index 55e7ef70c..807659526 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -19,12 +19,15 @@ kafkaConsumer: userCourseProgressRealtime: replicaCount: 1 userPoints: - replicaCount: 10 + replicaCount: 4 userPointsRealtime: replicaCount: 1 userCoursePoints: replicaCount: 2 +shibboTest: + replicaCount: 0 + email: # controls whether the background email service should be running enabled: true