Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HPC-7904: 🛂 Setup authentication for resolvers #21

Merged
merged 27 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eb61ae8
🔥 Remove "Hello world" endpoint
Pl217 Jun 18, 2021
5dcc0bd
⬆️ Upgrade to Typescript `v4.4.4`
Pl217 Nov 2, 2021
6fb008f
⬆️ Update GraphQL to `v15.7.2`
Pl217 Nov 2, 2021
418c748
⬆️ Upgrade `apollo-server-hapi` to new major version: `^3.5.0`
Pl217 Nov 2, 2021
361f32d
⬆️ Update `@hapi/hapi` and `@types/hapi__hapi`
Pl217 Nov 2, 2021
c349a2f
⬆️ Update `ts-node` to `^10.4.0`
Pl217 Nov 2, 2021
8fa496f
⬆️ Update `ts-node-dev` to `^1.1.8`
Pl217 Nov 2, 2021
564f4b6
➕ Install @unocha/hpc-api-core
Pl217 Aug 13, 2021
f4095a0
🔧 Link hpc-api-core to local version for development
Pl217 Aug 13, 2021
4cb31bf
⬆️ Update `@unocha/hpc-repo-tools` to `^0.2.1`
Pl217 Nov 2, 2021
49bd812
⬆️ Upgrade `eslint` to new major version: `^8.2.0`
Pl217 Nov 2, 2021
d9afff8
⬆️ Upgrade `husky` to new major version: `^7.0.4`
Pl217 Nov 2, 2021
aa01008
⬆️ Update `lint-staged` to `^12.0.2`
Pl217 Nov 2, 2021
068ce0b
Add `node` version requirements
Pl217 Nov 16, 2021
47bb477
⬆️ Update `prettier` to `v2.4.1`
Pl217 Nov 2, 2021
6589099
📌 Pin `knex` version to `v0.21.1` used in hpc-api-core
Pl217 Nov 2, 2021
80d67c9
⬆️ Update `pg` to `^8.7.1`
Pl217 Nov 2, 2021
070df17
Change `knex` import syntax
Pl217 Aug 13, 2021
4018d75
🔥 Remove model definitions
Pl217 Nov 1, 2021
52d5c50
♻️ Switch to using model definitions from hpc-api-core
Pl217 Nov 2, 2021
223e75c
Use current and not latest version when finding plan by ID
Pl217 Nov 16, 2021
a562ec2
Export config as a variable
Pl217 Jun 18, 2021
b36966d
Rename `config` to `CONFIG`
Pl217 Aug 13, 2021
d9d6ab6
✨ Introduce auth header parsing method `getTokenFromRequest`
Pl217 Aug 13, 2021
e468c45
🔨 Enable debugging
Pl217 Jun 18, 2021
7bd4855
🛂 Introduce @Permission decorator
Pl217 Jun 18, 2021
a2a8a1b
📝 Document auth decorators usage
Pl217 Jun 18, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .husky/.gitignore

This file was deleted.

48 changes: 48 additions & 0 deletions bin/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/sh

set -e

USAGE="$(basename "$0") prod|ci|dev [-h]

Install the npm dependencies in this repository and perform any
neccesary symlink operations to use packages from these repos.

where:
-h, --help show this help text"

MODE=""

while [ "$1" != "" ]; do
case $1 in
prod ) shift
MODE='prod'
;;
ci ) shift
MODE='ci'
;;
dev ) shift
MODE='dev'
;;
-h | --help ) echo "$USAGE"
exit
;;
esac
done

if [ "$MODE" = "" ]; then
echo "An install mode must be specified, e.g: npm run install-and-link dev"
exit 1
fi

DIR_TO_GO_BACK=$(pwd -P)

echo "Installing dependencies for hpc-api"
yarn install

if [ "$MODE" = "dev" ]; then
echo "Linking @unocha/hpc-api-core to ../hpc-api-core"
rm -rf "node_modules/@unocha/hpc-api-core"
ln -s "../../../hpc-api-core" "node_modules/@unocha/hpc-api-core"
fi

cd $DIR_TO_GO_BACK
34 changes: 34 additions & 0 deletions bin/port-forward.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#! /usr/bin/env node

/*
* A script that forwards port 9393 to 9339 to allow for attaching a debugger
* from outside of the docker image.
*/

const net = require('net');

net
.createServer((from) => {
try {
const to = net.createConnection({
host: 'localhost',
port: 9339,
});
const close = () => {
to.destroy();
from.destroy();
};
from.pipe(to);
to.pipe(from);
to.on('close', close);
to.on('error', close);
to.on('end', close);
from.on('close', close);
from.on('error', close);
from.on('end', close);
} catch (e) {
console.log('Unable to connect');
from.destroy();
}
})
.listen(9393);
4 changes: 1 addition & 3 deletions config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const config = {
export const CONFIG = {
httpPort: process.env.PORT,
storageProviders: {
aws: {
Expand Down Expand Up @@ -37,5 +37,3 @@ const config = {
},
rootURL: process.env.ROOT_URL,
};

export default config;
4 changes: 4 additions & 0 deletions docker-compose.integrated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ services:
dockerfile: ./env/api/Dockerfile
expose:
- '5100'
- '9393'
ports:
- '9339:9393'
volumes:
- .:/srv/www
- ../hpc-api-core:/srv/hpc-api-core
- ./env/api/node.sh:/etc/services.d/node/run
environment:
- POSTGRES_SERVER=postgres://postgres:@pgsql:5432/hpc
Expand Down
29 changes: 29 additions & 0 deletions docs/AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Authentication

Authentication can be used as a guard on a field, query or mutation, restricting data access or actions for a specific group of users.

Since the codebase uses TypeGraphQL, which relies heavily on decorators, authentication is also done using decorators.

Authentication is done with use of `@Permission` decorator. This decorator takes function as an argument with permission object as a return value.

For example:

```lang=js
@Permission(({ args }) =>
Promise.resolve({
or: [
{ type: 'global', permission: P.global.VIEW_ANY_PLAN_DATA },
{ type: 'plan', permission: P.plan.VIEW_DATA, id: args.id },
],
})
)
```

If only global permission check is needed, it can be used directly:

```lang=js
@Permission({
type: 'global',
permission: P.global.VIEW_ALL_JOBS,
})
```
3 changes: 3 additions & 0 deletions env/api/node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ if [ ! -d "node_modules" ]; then
npm install
fi

echo "==> Starting port forwarding for debugger"
node bin/port-forward.js &

echo "==> Waiting for postgres to start"
/wait

Expand Down
35 changes: 20 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,39 @@
"private": false,
"scripts": {
"check-types": "tsc --noEmit",
"install-and-link": "sh ./bin/install.sh",
"start": "ts-node src/server.ts",
"dev": "ts-node-dev --transpile-only --no-notify src/server.ts",
"dev": "ts-node-dev --inspect=127.0.0.1:9339 --transpile-only --no-notify -- src/server.ts",
"prepare": "husky install",
"lint-prettier": "prettier -c .",
"lint-eslint": "eslint --quiet .",
"lint": "yarn lint-prettier && yarn lint-eslint"
},
"dependencies": {
"apollo-server-hapi": "^2.25.0",
"@unocha/hpc-api-core": "^0.6.0",
"apollo-server-hapi": "^3.5.0",
"class-validator": "^0.13.1",
"graphql": "^15.5.0",
"knex": "^0.95.6",
"pg": "^8.6.0",
"graphql": "^15.7.2",
"knex": "0.21.1",
"pg": "^8.7.1",
"reflect-metadata": "^0.1.13",
"ts-node": "^10.0.0",
"ts-node": "^10.4.0",
"type-graphql": "^1.1.1",
"typedi": "^0.10.0",
"typescript": "^4.3.2"
"typescript": "^4.4.4"
},
"devDependencies": {
"@hapi/hapi": "^20.1.3",
"@types/hapi__hapi": "^20.0.8",
"@unocha/hpc-repo-tools": "^0.1.1",
"eslint": "^7.27.0",
"husky": "^6.0.0",
"lint-staged": "^11.0.0",
"prettier": "^2.3.0",
"ts-node-dev": "^1.1.6"
"@hapi/hapi": "^20.2.1",
"@types/hapi__hapi": "^20.0.9",
"@unocha/hpc-repo-tools": "^0.2.1",
"eslint": "^8.2.0",
"husky": "^7.0.4",
"lint-staged": "^12.0.2",
"prettier": "2.4.1",
"ts-node-dev": "^1.1.8"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"lint-staged": {
"*.{ts,js}": [
Expand Down
85 changes: 73 additions & 12 deletions src/common-libs/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,75 @@
const AuthLib = {
async getRolesForAParticipant(participantId: number) {
return participantId; //TODO: add functionality
},

async getParticipantsForATarget(target: {
targetId: number;
target: string;
}) {
return target; //TODO: add functionality
},
import { Request } from '@hapi/hapi';
import { BASIC_AUTH_USER } from '@unocha/hpc-api-core/src/auth';
import { BadRequestError } from '@unocha/hpc-api-core/src/util/error';

interface BasicAuth {
username: string | null;
password: string | null;
}

const parseBasic = (credentialsPart: string): BasicAuth => {
let pieces: (string | null)[];

const decoded = Buffer.from(credentialsPart, 'base64').toString('utf8');

if (!decoded) {
throw new Error('Invalid authorization header');
}

const index = decoded.indexOf(':');

if (index === -1) {
pieces = [decoded];
} else {
pieces = [decoded.slice(0, index), decoded.slice(index + 1)];
}

// Allows for usernameless authentication
if (!pieces[0]) {
pieces[0] = null;
}

// Allows for passwordless authentication
if (!pieces[1]) {
pieces[1] = null;
}

return {
username: pieces[0],
password: pieces[1],
};
};

export default AuthLib;
export const getTokenFromRequest = (req: Request): string | null => {
if (!req.headers?.authorization) {
return null;
}

const pieces = req.headers.authorization.split(/\s+/);

if (!pieces || pieces.length !== 2) {
throw new Error('Bad HTTP authentication header format');
}

const schemePart = pieces[0];
const credentialsPart = pieces[1];

switch (schemePart.toLowerCase()) {
case 'basic': {
const credentials = parseBasic(credentialsPart);

if (credentials.username !== BASIC_AUTH_USER) {
throw new BadRequestError(
'Client Authentication is not supported in the v4 API'
);
}

return credentials.password;
}
case 'bearer':
return credentialsPart;

default:
throw new Error('Unsupported authorization scheme');
}
};
31 changes: 31 additions & 0 deletions src/common-libs/auth/permission-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { actionIsPermitted } from '@unocha/hpc-api-core/src/auth';
import { RequiredPermissionsCondition } from '@unocha/hpc-api-core/src/auth/permissions';
import { Context } from '@unocha/hpc-api-core/src/lib/context';
import { ForbiddenError } from '@unocha/hpc-api-core/src/util/error';
import { createMethodDecorator, ResolverData } from 'type-graphql';

type RequiredPermissions = (
resolverData: ResolverData<Context>
) => Promise<RequiredPermissionsCondition<never>>;

// eslint-disable-next-line @typescript-eslint/naming-convention
export function Permission(
requiredPermissions: RequiredPermissions | RequiredPermissionsCondition<never>
): MethodDecorator {
return createMethodDecorator(
async (resolverData: ResolverData<Context>, next) => {
let permissions: RequiredPermissionsCondition<never>;
if (typeof requiredPermissions === 'function') {
permissions = await requiredPermissions(resolverData);
} else {
permissions = requiredPermissions;
}

if (!(await actionIsPermitted(permissions, resolverData.context))) {
throw new ForbiddenError('No permission to perform this action');
}

return next();
}
);
}
10 changes: 5 additions & 5 deletions src/data-providers/postgres/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import Knex from 'knex';
import config from '../../../config';
import { CONFIG } from '../../../config';

/**
* Initialize a new Postgres provider
*/
export async function createDbConnetion() {
const knex = Knex({
client: 'pg',
connection: config.db.connection,
connection: CONFIG.db.connection,
pool: {
min: config.db.poolMin,
max: config.db.poolMax,
idleTimeoutMillis: config.db.poolIdle,
min: CONFIG.db.poolMin,
max: CONFIG.db.poolMax,
idleTimeoutMillis: CONFIG.db.poolIdle,
},
acquireConnectionTimeout: 2000,
});
Expand Down
17 changes: 0 additions & 17 deletions src/data-providers/postgres/knex-types.ts

This file was deleted.

8 changes: 0 additions & 8 deletions src/data-providers/postgres/models/auth/auth-grant.ts

This file was deleted.

11 changes: 0 additions & 11 deletions src/data-providers/postgres/models/auth/auth-grantLog.ts

This file was deleted.

8 changes: 0 additions & 8 deletions src/data-providers/postgres/models/auth/auth-grantee.ts

This file was deleted.

Loading