Skip to content

Commit

Permalink
Basic access management
Browse files Browse the repository at this point in the history
  • Loading branch information
pouriaMaleki authored Feb 21, 2021
1 parent 94e4872 commit b8dc865
Show file tree
Hide file tree
Showing 33 changed files with 1,534 additions and 181 deletions.
1 change: 1 addition & 0 deletions backend/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ PORT=4000
JWT_SECRET="JWT_DARK_SECRET"
ALLOW_ORIGIN=http://localhost:3000
CLIENT_ADDRESS=http://localhost:3000
NODE_ENV=development

DATABASE_URL="postgresql://admin:example@localhost:5432/moro?schema=public"

Expand Down
1,179 changes: 1,038 additions & 141 deletions backend/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"graphql-tools": "^7.0.2",
"helmet": "^4.2.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.20",
"node-fetch": "^2.6.1",
"uuid": "^8.3.2"
},
Expand All @@ -45,6 +46,7 @@
"@types/express": "^4.17.9",
"@types/express-jwt": "^6.0.0",
"@types/jest": "^26.0.20",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.11",
"@types/uuid": "^8.3.0",
"concurrently": "^5.3.0",
Expand Down
2 changes: 2 additions & 0 deletions backend/prisma/migrations/20210202171154_access/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "permissions" TEXT[];
11 changes: 6 additions & 5 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ model Project {
}

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String?
projects Project?
id Int @id @default(autoincrement())
email String @unique
name String?
password String?
projects Project?
permissions String[]
}

enum TOKEN_TYPES {
Expand Down
5 changes: 3 additions & 2 deletions backend/src/graphql/resolverHelper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { prisma } from '../server/prisma';
import { UserWithoutPassword } from '../types';
import { StitchingResolver } from './resolvers-types';

/*
Expand All @@ -24,6 +25,6 @@ export function resolverHelper<TArgs, TResult>(
| StitchingResolver<TResult, any, any, TArgs>
| undefined,
) {
return (args: TArgs): TResult =>
(resolver as ResolverFn<TArgs, TResult>)({}, args, { prisma }, {});
return (args: TArgs, user?: UserWithoutPassword): TResult =>
(resolver as ResolverFn<TArgs, TResult>)({}, args, { prisma, user }, {});
}
28 changes: 28 additions & 0 deletions backend/src/graphql/resolvers-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type Project = {

export type Query = {
__typename?: 'Query';
getAllPermissions: Array<Scalars['String']>;
projects?: Maybe<Array<Maybe<Project>>>;
user?: Maybe<User>;
users?: Maybe<Array<Maybe<User>>>;
Expand All @@ -37,6 +38,13 @@ export type UserInput = {
password?: Maybe<Scalars['String']>;
};

export type AdminUserInput = {
id: Scalars['Int'];
email?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
permissions?: Maybe<Array<Scalars['String']>>;
};

export type CredentialsInput = {
email: Scalars['String'];
password?: Maybe<Scalars['String']>;
Expand Down Expand Up @@ -65,6 +73,7 @@ export type User = {
id?: Maybe<Scalars['Int']>;
email?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
permissions?: Maybe<Array<Maybe<Scalars['String']>>>;
};

export type AuthResult = {
Expand All @@ -77,6 +86,7 @@ export type AuthResult = {
export type Mutation = {
__typename?: 'Mutation';
createUser?: Maybe<User>;
updateUser?: Maybe<User>;
register?: Maybe<AuthResult>;
login?: Maybe<AuthResult>;
forgotPassword?: Maybe<AuthResult>;
Expand All @@ -88,6 +98,10 @@ export type MutationCreateUserArgs = {
user: UserInput;
};

export type MutationUpdateUserArgs = {
user: AdminUserInput;
};

export type MutationRegisterArgs = {
user: UserInput;
};
Expand Down Expand Up @@ -214,6 +228,7 @@ export type ResolversTypes = ResolversObject<{
String: ResolverTypeWrapper<Scalars['String']>;
Query: ResolverTypeWrapper<{}>;
UserInput: UserInput;
AdminUserInput: AdminUserInput;
CredentialsInput: CredentialsInput;
EmailInput: EmailInput;
NewPasswordInput: NewPasswordInput;
Expand All @@ -232,6 +247,7 @@ export type ResolversParentTypes = ResolversObject<{
String: Scalars['String'];
Query: {};
UserInput: UserInput;
AdminUserInput: AdminUserInput;
CredentialsInput: CredentialsInput;
EmailInput: EmailInput;
NewPasswordInput: NewPasswordInput;
Expand All @@ -256,6 +272,7 @@ export type QueryResolvers<
ContextType = ApolloContext,
ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']
> = ResolversObject<{
getAllPermissions?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
projects?: Resolver<
Maybe<Array<Maybe<ResolversTypes['Project']>>>,
ParentType,
Expand All @@ -272,6 +289,11 @@ export type UserResolvers<
id?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
email?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
permissions?: Resolver<
Maybe<Array<Maybe<ResolversTypes['String']>>>,
ParentType,
ContextType
>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

Expand All @@ -295,6 +317,12 @@ export type MutationResolvers<
ContextType,
RequireFields<MutationCreateUserArgs, 'user'>
>;
updateUser?: Resolver<
Maybe<ResolversTypes['User']>,
ParentType,
ContextType,
RequireFields<MutationUpdateUserArgs, 'user'>
>;
register?: Resolver<
Maybe<ResolversTypes['AuthResult']>,
ParentType,
Expand Down
10 changes: 2 additions & 8 deletions backend/src/server/apolloContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApolloExpressContext } from '../types';
import { prisma } from './prisma';
import { checkMissingPermissions } from '../user/permissions/checkMissingPermissions';

// This file will make apollo context available for resolvers

Expand All @@ -14,12 +15,5 @@ export const apolloContext = ({ req }: ApolloExpressContext): ApolloContext => (
prisma,
user: req.user,
isAuthenticated: () => !!req.user,
checkMissingPermissions: (neededPermissions) => {
console.log(neededPermissions);
// get user permissions
// subtract perm from user permissions
// return missing permissions list

return [];
},
checkMissingPermissions: checkMissingPermissions(req.user),
});
6 changes: 5 additions & 1 deletion backend/src/server/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import expressJwt from 'express-jwt';
import { getApolloServer } from '../graphql/graphqlServer';
import { apolloContext } from './apolloContext';
import { JWT_ALGORITHM, JWT_SECRET } from '../utils/constants';
import { expressAddUserToRequest } from './expressAddUserToRequest';

export const startExpress = (): void => {
const app = express();
Expand All @@ -22,7 +23,7 @@ export const startExpress = (): void => {
});
}

// Extract user from JWT (Authorization header Bearer) as user in all requests
// Extract user id from JWT (Authorization header Bearer) as user in all requests
app.use(
expressJwt({
secret: JWT_SECRET,
Expand All @@ -31,6 +32,9 @@ export const startExpress = (): void => {
}),
);

// get user by user id from the db and put it in the express request (req.user)
app.use(expressAddUserToRequest);

// some level of http security
app.use(
helmet({
Expand Down
17 changes: 17 additions & 0 deletions backend/src/server/expressAddUserToRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { RequestHandler } from 'express';
import { ExpressRequest } from '../types';
import { getUserById } from '../user/utils/getUserById';

export const expressAddUserToRequest: RequestHandler = async (
req,
res,
next,
): Promise<void> => {
const request = req as ExpressRequest;
if (request.user?.id) {
const user = await getUserById(request.user.id);
if (user) request.user = user;
}

next();
};
4 changes: 3 additions & 1 deletion backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { Request } from 'express';
import { ExpressContext } from 'apollo-server-express/dist/ApolloServer';
import { User } from '@prisma/client';

export type UserWithoutPassword = Omit<User, 'password'>;

export interface ExpressRequest extends Request {
user: User;
user?: UserWithoutPassword;
}

export interface ApolloExpressContext extends ExpressContext {
Expand Down
6 changes: 2 additions & 4 deletions backend/src/user/createTokenFromUser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ describe('createTokenFromUser', () => {
it('creates token correctly', () => {
const token = createTokenFromUser({
id: 1,
email: 'test@test.test',
name: 'Test',
});

const tokenDecryptedKeys = Object.keys(jwt.verify(token, JWT_SECRET)).sort();

expect(tokenDecryptedKeys).toEqual(['email', 'iat', 'id', 'name']);
expect(tokenDecryptedKeys).toEqual(['iat', 'id']);
});

it('does not include extra fields', () => {
Expand All @@ -25,6 +23,6 @@ describe('createTokenFromUser', () => {

const tokenDecryptedKeys = Object.keys(jwt.verify(token, JWT_SECRET)).sort();

expect(tokenDecryptedKeys).toEqual(['email', 'iat', 'id', 'name']);
expect(tokenDecryptedKeys).toEqual(['iat', 'id']);
});
});
2 changes: 0 additions & 2 deletions backend/src/user/createTokenFromUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { JWT_SECRET, JWT_ALGORITHM } from '../utils/constants';
export const createTokenFromUser = <T extends PublicUser>(user: T): string => {
const body: PublicUser = {
id: user.id,
email: user.email,
name: user.name,
};
return jwt.sign(body, JWT_SECRET, { algorithm: JWT_ALGORITHM });
};
2 changes: 1 addition & 1 deletion backend/src/user/forgotPassword.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resolverHelper } from '../graphql/resolverHelper';
import { prisma } from '../server/prisma';
import { TOKEN_EXPIRE_MINUTES } from '../utils/constants';
import { createFakeToken } from './createFakeToken';
import { createFakeToken } from './utils/createFakeToken';
import { hashUserPassword } from './hashUserPassword';
import { forgotPassword as forgotPasswordResolver } from './forgotPassword';
const forgotPassword = resolverHelper(forgotPasswordResolver);
Expand Down
2 changes: 1 addition & 1 deletion backend/src/user/getUser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QueryResolvers } from '../graphql/resolvers-types';

export const getUser: QueryResolvers['user'] = async (parent, args, ctx) => {
return ctx.user;
return ctx.user ? ctx.user : {};
};
6 changes: 6 additions & 0 deletions backend/src/user/permissions/allPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const allPermissions = [
'users.create',
'users.read',
'users.update',
'users.delete',
];
14 changes: 14 additions & 0 deletions backend/src/user/permissions/checkMissingPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { difference } from 'lodash';
import { UserWithoutPassword } from '../../types';
import { isDevEnv } from '../../utils/isDevEnv';

export const checkMissingPermissions = (user: UserWithoutPassword | undefined) => (
neededPermissions: string[],
): string[] => {
if (!user) return neededPermissions;

// exceptionally skip permission check if it's a development environment
if (isDevEnv()) return [];

return difference(neededPermissions, user.permissions);
};
5 changes: 5 additions & 0 deletions backend/src/user/permissions/getAllPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { QueryResolvers } from '../../graphql/resolvers-types';
import { allPermissions } from './allPermissions';

export const getAllPermissions: QueryResolvers['getAllPermissions'] = () =>
allPermissions;
2 changes: 1 addition & 1 deletion backend/src/user/resetPassword.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { prisma } from '../server/prisma';
import { TOKEN_EXPIRE_MINUTES } from '../utils/constants';
import { resetPassword as resetPasswordResolver } from './resetPassword';
import { hashUserPassword } from './hashUserPassword';
import { createFakeToken } from './createFakeToken';
import { createFakeToken } from './utils/createFakeToken';
const resetPassword = resolverHelper(resetPasswordResolver);

describe('resetPassword', () => {
Expand Down
Loading

0 comments on commit b8dc865

Please sign in to comment.