Skip to content

Commit

Permalink
Merge pull request #21 from UN-OCHA/HPC-7904
Browse files Browse the repository at this point in the history
HPC-7904: 🛂 Setup authentication for resolvers
  • Loading branch information
s0 authored Nov 17, 2021
2 parents 5f75249 + a2a8a1b commit 7b051a9
Show file tree
Hide file tree
Showing 32 changed files with 1,743 additions and 1,568 deletions.
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

0 comments on commit 7b051a9

Please sign in to comment.