Skip to content

Commit

Permalink
feat(be): Restrict access to pages users don't have access to (intern…
Browse files Browse the repository at this point in the history
…al-pr-358)

* Page access middleware + improvements to the middleware system

* updated the middleware to use `loggedInAndSignup` to make sure the user is logged in before checking for access

* PR changes
  • Loading branch information
liana-p authored Nov 16, 2021
1 parent 7487ca5 commit c5b6688
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 32 deletions.
31 changes: 4 additions & 27 deletions packages/hash/api/src/graphql/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import GraphQLJSON from "graphql-type-json";

import { ForbiddenError } from "apollo-server-express";
import { Entity } from "../apiTypes.gen";

import {
Expand Down Expand Up @@ -38,7 +37,6 @@ import {
getImpliedEntityVersion,
} from "./entity/impliedHistory";

import { GraphQLContext, LoggedInGraphQLContext } from "../context";
import { logout } from "./user/logout";
import { me } from "./user/me";
import { isShortnameTaken } from "./user/isShortnameTaken";
Expand All @@ -54,30 +52,9 @@ import { joinOrg } from "./user/joinOrg";
import { fileFields } from "./file";
import { requestFileUpload } from "./file/requestFileUpload";
import { createFileFromLink } from "./file/createFileFromLink";

const loggedIn =
(next: any) => (obj: any, args: any, ctx: GraphQLContext, info: any) => {
if (!ctx.user) {
throw new ForbiddenError("You must be logged in to perform this action.");
}
return next(obj, args, ctx, info);
};

const signedUp =
(next: any) =>
(obj: any, args: any, ctx: LoggedInGraphQLContext, info: any) => {
if (!ctx.user.isAccountSignupComplete()) {
throw new ForbiddenError(
"You must complete the sign-up process to perform this action.",
);
}
return next(obj, args, ctx, info);
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const loggedInAndSignedUp =
(next: any) => (obj: any, args: any, ctx: GraphQLContext, info: any) =>
loggedIn(signedUp(next))(obj, args, ctx, info);
import { loggedIn } from "./middlewares/loggedIn";
import { loggedInAndSignedUp } from "./middlewares/loggedInAndSignedUp";
import { canAccessAccount } from "./middlewares/canAccessAccount";

export const resolvers = {
Query: {
Expand All @@ -91,7 +68,7 @@ export const resolvers = {
getAccountEntityTypes: loggedInAndSignedUp(getAccountEntityTypes),
entity: loggedInAndSignedUp(entity),
getEntityType: loggedInAndSignedUp(getEntityType),
page: loggedInAndSignedUp(page),
page: canAccessAccount(page),
getImpliedEntityHistory: loggedInAndSignedUp(getImpliedEntityHistory),
getImpliedEntityVersion: loggedInAndSignedUp(getImpliedEntityVersion),
// Logged in users only
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ForbiddenError } from "apollo-server-express";
import { Scalars } from "../../apiTypes.gen";
import { GraphQLContext, LoggedInGraphQLContext } from "../../context";
import { loggedInAndSignedUp } from "./loggedInAndSignedUp";
import { ResolverMiddleware } from "./middlewareTypes";

/** Middleware verifying the current logged in user has access to the requested account.
* This middleware needs to be run on a query that is passing an
* account id
*/
export const canAccessAccount: ResolverMiddleware<
GraphQLContext,
{
accountId: Scalars["ID"];
},
LoggedInGraphQLContext
> = (next) =>
loggedInAndSignedUp(async (_, args, ctx, info) => {
let isAllowed = false;
if (ctx.user.accountId === args.accountId) {
isAllowed = true;
} else {
isAllowed = await ctx.user.isMemberOfOrg(
ctx.dataSources.db,
args.accountId,
);
}
if (!isAllowed) {
throw new ForbiddenError(
`You cannot perform this action as you don't have permission to access the account with accountId ${args.accountId}`,
);
}
return next(_, args, ctx, info);
});
14 changes: 14 additions & 0 deletions packages/hash/api/src/graphql/resolvers/middlewares/loggedIn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ForbiddenError } from "apollo-server-express";
import { GraphQLContext, LoggedInGraphQLContext } from "../../context";
import { ResolverMiddleware } from "./middlewareTypes";

export const loggedIn: ResolverMiddleware<
GraphQLContext,
any,
LoggedInGraphQLContext
> = (next) => (obj: any, args: any, ctx: GraphQLContext, info: any) => {
if (!ctx.user) {
throw new ForbiddenError("You must be logged in to perform this action.");
}
return next(obj, args, ctx as LoggedInGraphQLContext, info);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { GraphQLContext, LoggedInGraphQLContext } from "../../context";
import { loggedIn } from "./loggedIn";
import { ResolverMiddleware } from "./middlewareTypes";
import { signedUp } from "./signedUp";

export const loggedInAndSignedUp: ResolverMiddleware<
GraphQLContext,
any,
LoggedInGraphQLContext
> = (next) => (obj: any, args: any, ctx: GraphQLContext, info: any) =>
loggedIn(signedUp(next))(obj, args, ctx, info);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Resolver } from "../../apiTypes.gen";

export type ResolverMiddleware<
TStartContext,
TArgs,
TEndContext = TStartContext,
> = (
next: Resolver<any, any, TEndContext, any>,
) => Resolver<any, any, TStartContext, TArgs>;
13 changes: 13 additions & 0 deletions packages/hash/api/src/graphql/resolvers/middlewares/signedUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ForbiddenError } from "apollo-server-express";
import { LoggedInGraphQLContext } from "../../context";
import { ResolverMiddleware } from "./middlewareTypes";

export const signedUp: ResolverMiddleware<LoggedInGraphQLContext, any> =
(next) => (obj: any, args: any, ctx: LoggedInGraphQLContext, info: any) => {
if (!ctx.user.isAccountSignupComplete()) {
throw new ForbiddenError(
"You must complete the sign-up process to perform this action.",
);
}
return next(obj, args, ctx, info);
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const createOrgEmailInvitation: Resolver<
throw new ApolloError(msg, "NOT_FOUND");
}

if (!(await user.isMemberOfOrg(client, org))) {
if (!(await user.isMemberOfOrg(client, org.entityId))) {
throw new ForbiddenError(
`User with entityId ${user.entityId} is not a member of the org with entityId ${org.entityId}`,
);
Expand Down
7 changes: 4 additions & 3 deletions packages/hash/api/src/model/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,12 +363,13 @@ class __User extends Account {
);
}

async isMemberOfOrg(client: DBClient, { entityId }: Org) {
async isMemberOfOrg(client: DBClient, orgEntityId: string) {
const orgMemberships = await this.getOrgMemberships(client);

return (
orgMemberships.find(
({ properties }) => properties.org.__linkedData.entityId === entityId,
({ properties }) =>
properties.org.__linkedData.entityId === orgEntityId,
) !== undefined
);
}
Expand All @@ -381,7 +382,7 @@ class __User extends Account {
client: DBClient,
params: { org: Org; responsibility: string },
) {
if (await this.isMemberOfOrg(client, params.org)) {
if (await this.isMemberOfOrg(client, params.org.entityId)) {
throw new Error(
`User with entityId '${this.entityId}' is already a member of the organization with entityId '${params.org.entityId}'`,
);
Expand Down
4 changes: 3 additions & 1 deletion packages/hash/integration/src/tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ describe("logged in user ", () => {

expect(updatedExistingUser).not.toBeNull();

expect(await updatedExistingUser.isMemberOfOrg(db, org)).toBe(true);
expect(await updatedExistingUser.isMemberOfOrg(db, org.entityId)).toBe(
true,
);
});

it("can create an org email invitation", async () => {
Expand Down

0 comments on commit c5b6688

Please sign in to comment.