diff --git a/package.json b/package.json index 97f04b404..cbe872d79 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "start:webpackDevServer": "node ./scripts/start.js", "start": "node dist/index.js", "test": "node scripts/test.js --env=jsdom", - "watch:types": "nodemon --config ./config/nodemon/types.json", "watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts ./config/watcher.ts" }, "author": "", @@ -112,7 +111,6 @@ "html-webpack-plugin": "^3.2.0", "jest": "^23.2.0", "loader-utils": "^1.1.0", - "nodemon": "^1.17.5", "npm-run-all": "^4.1.3", "postcss-flexbugs-fixes": "^3.3.1", "postcss-font-magician": "^2.2.1", diff --git a/src/core/server/graph/common/context.ts b/src/core/server/graph/common/context.ts new file mode 100644 index 000000000..d939d7b64 --- /dev/null +++ b/src/core/server/graph/common/context.ts @@ -0,0 +1,13 @@ +import { User } from "talk-server/models/user"; + +export interface CommonContextOptions { + user?: User; +} + +export default class CommonContext { + public user?: User; + + constructor({ user }: CommonContextOptions) { + this.user = user; + } +} diff --git a/src/core/server/graph/common/directives/auth.ts b/src/core/server/graph/common/directives/auth.ts index 600f5a2c1..2b21a6366 100644 --- a/src/core/server/graph/common/directives/auth.ts +++ b/src/core/server/graph/common/directives/auth.ts @@ -1,12 +1,39 @@ import { DirectiveResolverFn } from "graphql-tools"; -const auth: DirectiveResolverFn = (next, src, args, context) => { - return next().then(str => { - if (typeof str === "string") { - return str.toUpperCase(); +import CommonContext from "talk-server/graph/common/context"; +import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; + +export interface AuthDirectiveArgs { + roles?: GQLUSER_ROLE[]; + userIDField?: string; +} + +const auth: DirectiveResolverFn< + Record, + CommonContext +> = (next, src, { roles, userIDField }: AuthDirectiveArgs, { user }) => { + // If there is a user on the request. + if (user) { + // If the role and user owner checks are disabled, then allow them based on + // their authenticated status. + if (!roles && !userIDField) { + return next(); } - return str; - }); + + // And the user has the expected role. + if (roles && roles.includes(user.role)) { + // Let the request continue. + return next(); + } + + // Or the item is owned by the specific user. + if (userIDField && src[userIDField] && src[userIDField] === user.id) { + return next(); + } + } + + // TODO: return better error. + throw new Error("not authorized"); }; export default auth; diff --git a/src/core/server/graph/management/context.ts b/src/core/server/graph/management/context.ts index dda52cb5b..c77f1a207 100644 --- a/src/core/server/graph/management/context.ts +++ b/src/core/server/graph/management/context.ts @@ -1,13 +1,16 @@ import { Db } from "mongodb"; +import CommonContext from "talk-server/graph/common/context"; export interface ManagementContextOptions { db: Db; } -export default class ManagementContext { +export default class ManagementContext extends CommonContext { public db: Db; constructor({ db }: ManagementContextOptions) { + super({}); + this.db = db; } } diff --git a/src/core/server/graph/tenant/context.ts b/src/core/server/graph/tenant/context.ts index c49b3d49a..21e2dc9b9 100644 --- a/src/core/server/graph/tenant/context.ts +++ b/src/core/server/graph/tenant/context.ts @@ -1,4 +1,5 @@ import { Db } from "mongodb"; +import CommonContext from "talk-server/graph/common/context"; import { Tenant } from "talk-server/models/tenant"; import { User } from "talk-server/models/user"; import loaders from "./loaders"; @@ -10,16 +11,16 @@ export interface TenantContextOptions { user?: User; } -export default class TenantContext { +export default class TenantContext extends CommonContext { public loaders: ReturnType; public mutators: ReturnType; public db: Db; public tenant: Tenant; - public user?: User; constructor({ user, tenant, db }: TenantContextOptions) { + super({ user }); + this.tenant = tenant; - this.user = user; this.loaders = loaders(this); this.mutators = mutators(this); this.db = db; diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 9f5c0fdcd..bf6e5d6f8 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -2,7 +2,14 @@ ## Custom Directives ################################################################################ -directive @auth(roles: [USER_ROLE!]!) on FIELD_DEFINITION +""" +auth is a directive that will enforce authorization rules on the schema +definition. It will restrict the viewer of the field based on roles or if the +`userIDField` is specified, it will see if the current users ID equals the field +specified. This allows users that own a specific resource (like a comment, or a +flag) see their own content, but restrict it to everyone else. +""" +directive @auth(roles: [USER_ROLE!], userIDField: String) on FIELD_DEFINITION ################################################################################ ## Custom Scalar Types @@ -195,7 +202,7 @@ type Settings { autoCloseStream when true will auto close the stream when the `closeTimeout` amount of seconds have been reached. """ - autoCloseStream: Boolean! + autoCloseStream: Boolean! @auth(roles: [ADMIN]) """ customCssUrl is the URL of the custom CSS used to display on the frontend. @@ -254,12 +261,12 @@ type Settings { """ wordlist will return a given list of words. """ - wordlist: WordlistSettings @auth(roles: [ADMIN]) + wordlist: WordlistSettings @auth(roles: [ADMIN, MODERATOR]) """ domains will return a given list of whitelisted domains. """ - domains: [String!] @auth(roles: [ADMIN]) + domains: [String!] @auth(roles: [ADMIN]) @auth(roles: [ADMIN]) """ auth contains all the settings related to authentication and authorization. @@ -294,7 +301,7 @@ type User { """ role is the current role of the User. """ - role: USER_ROLE! + role: USER_ROLE! @auth(roles: [ADMIN, MODERATOR], userIDField: "id") } ################################################################################ @@ -561,7 +568,7 @@ type Mutation { """ createComment will create a Comment as the current logged in User. """ - createComment(input: CreateCommentInput!): CreateCommentPayload + createComment(input: CreateCommentInput!): CreateCommentPayload @auth } ################################################################################