diff --git a/package-lock.json b/package-lock.json index 76efd19a..bea0d701 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,7 +106,8 @@ "@types/koa-joi-router-docs": "^1.0.1", "@types/koa-log4": "^2.3.3", "@types/koa-mount": "^4.0.1", - "@types/koa-passport": "^4.0.2", + "@types/koa-passport": "^4.0.3", + "@types/koa-roles": "^2.0.0", "@types/koa-session": "^5.10.4", "@types/koa-static": "^4.0.2", "@types/node": "^16.11.7", @@ -4802,15 +4803,24 @@ } }, "node_modules/@types/koa-passport": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/koa-passport/-/koa-passport-4.0.2.tgz", - "integrity": "sha512-WDQhhOW+coTCe//wTaPViim9oodEuAscXiwDUSRlcNH2j7MYa4bqlLq3UdkUp5rY/b0wE4blkBxI90jpwEc1Mg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/koa-passport/-/koa-passport-4.0.3.tgz", + "integrity": "sha512-tNMYd/bcv0Zw7fc0CzEBYM9uUzVtn4XWzdUYfkTgSkEljP6nap7eI4E5x43ukrUQvztgXSYFkz3Uk+ujFeUzTg==", "dev": true, "dependencies": { "@types/koa": "*", "@types/passport": "*" } }, + "node_modules/@types/koa-roles": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/koa-roles/-/koa-roles-2.0.0.tgz", + "integrity": "sha512-TfXPQRiTx0zfrPVubcp4JDYr+G8WD0r5Cnx8NTrgvL8w7mZtJHp4V8C43U18eeij9uksfAn8SYJr1nLJgtPPfQ==", + "dev": true, + "dependencies": { + "@types/koa": "*" + } + }, "node_modules/@types/koa-router": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/koa-router/-/koa-router-7.4.4.tgz", @@ -30582,15 +30592,24 @@ } }, "@types/koa-passport": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/koa-passport/-/koa-passport-4.0.2.tgz", - "integrity": "sha512-WDQhhOW+coTCe//wTaPViim9oodEuAscXiwDUSRlcNH2j7MYa4bqlLq3UdkUp5rY/b0wE4blkBxI90jpwEc1Mg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/koa-passport/-/koa-passport-4.0.3.tgz", + "integrity": "sha512-tNMYd/bcv0Zw7fc0CzEBYM9uUzVtn4XWzdUYfkTgSkEljP6nap7eI4E5x43ukrUQvztgXSYFkz3Uk+ujFeUzTg==", "dev": true, "requires": { "@types/koa": "*", "@types/passport": "*" } }, + "@types/koa-roles": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/koa-roles/-/koa-roles-2.0.0.tgz", + "integrity": "sha512-TfXPQRiTx0zfrPVubcp4JDYr+G8WD0r5Cnx8NTrgvL8w7mZtJHp4V8C43U18eeij9uksfAn8SYJr1nLJgtPPfQ==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, "@types/koa-router": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/koa-router/-/koa-router-7.4.4.tgz", diff --git a/package.json b/package.json index 9f9bbd37..b2c3c389 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,8 @@ "@types/koa-joi-router-docs": "^1.0.1", "@types/koa-log4": "^2.3.3", "@types/koa-mount": "^4.0.1", - "@types/koa-passport": "^4.0.2", + "@types/koa-passport": "^4.0.3", + "@types/koa-roles": "^2.0.0", "@types/koa-session": "^5.10.4", "@types/koa-static": "^4.0.2", "@types/node": "^16.11.7", diff --git a/src/backend/middleware/auth.js b/src/backend/middleware/auth.ts similarity index 58% rename from src/backend/middleware/auth.js rename to src/backend/middleware/auth.ts index 4ac3a4d8..72ddea92 100644 --- a/src/backend/middleware/auth.js +++ b/src/backend/middleware/auth.ts @@ -1,22 +1,33 @@ +import { ParameterizedContext } from 'koa'; import Roles from 'koa-roles'; import { isString } from 'lodash'; import { ORCID as orcidUtils } from 'orcid-utils'; -import config from '../config.ts'; -import { getLogger } from '../log.ts'; +import config from '../config'; +import { getLogger } from '../log'; +import { CommunityModel, GroupModel, PersonaModel, UserModel } from '../models'; +import { User } from '../models/entities'; const log = getLogger('backend:middleware:auth'); -/** - * Installs authorization middleware into the koa app. - * - * @param {Object} ctx - the koa context object - * @param {funtion} next - continue to next middleware - */ +export type Auth = { + can: Roles['can']; + middleware: Roles['middleware']; + getUser: (ctx: ParameterizedContext<{ user?: User }>) => Promise; + isIdentityOf: PersonaModel['isIdentityOf']; + isMemberOf: GroupModel['isMemberOf']; + isMemberOfCommunity: CommunityModel['isMemberOf']; + isOwnerOfCommunity: CommunityModel['isOwnerOf']; +}; -const authWrapper = (users, groups, communities, personas) => { +const authWrapper = ( + users: UserModel, + groups: GroupModel, + communities: CommunityModel, + personas: PersonaModel, +): Auth => { const roles = new Roles(); - roles.getUser = async ctx => { + const getUser: Auth['getUser'] = async ctx => { log.debug('Retrieving current user.'); if (ctx.isAuthenticated() && ctx.state.user) { @@ -38,7 +49,7 @@ const authWrapper = (users, groups, communities, personas) => { return; }; - roles.isMemberOf = async (group, id) => { + const isMemberOf: Auth['isMemberOf'] = async (group, id) => { log.debug(`Checking if ${id} is a member of ${group}.`); if ( config.adminUsers && @@ -55,57 +66,63 @@ const authWrapper = (users, groups, communities, personas) => { } }; - roles.isMemberOfCommunity = async (community, id) => { + const isMemberOfCommunity: Auth['isMemberOfCommunity'] = async ( + community, + id, + ) => { log.debug(`Checking if ${id} is a member of ${community}.`); return communities.isMemberOf(community, id); }; - roles.isOwnerOfCommunity = async (community, id) => { + const isOwnerOfCommunity: Auth['isOwnerOfCommunity'] = async ( + community, + id, + ) => { log.debug(`Checking if ${id} is an owner of ${community}.`); return communities.isOwnerOf(community, id); }; - roles.isIdentityOf = async (persona, id) => { + const isIdentityOf: Auth['isIdentityOf'] = async (persona, id) => { log.debug(`Checking if ${id} is the identity of ${persona}.`); return personas.isIdentityOf(persona, id); }; roles.use('access private pages', async ctx => { log.debug('Checking if user is authenticated...'); - const user = await roles.getUser(ctx); + const user = await getUser(ctx); log.debug('User is authenticated:', !!user); return !!user; }); roles.use('access moderator pages', async ctx => { log.debug('Checking if user can access moderator pages.'); - const user = await roles.getUser(ctx); + const user = await getUser(ctx); if (!user) return false; return ( - (await roles.isMemberOf('moderators', user.orcid)) || - (await roles.isMemberOf('admins', user.orcid)) + (await isMemberOf('moderators', user.orcid)) || + (await isMemberOf('admins', user.orcid)) ); }); roles.use('access admin pages', async ctx => { log.debug('Checking if user can access admin pages.'); - const user = await roles.getUser(ctx); + const user = await getUser(ctx); if (!user) return false; - return roles.isMemberOf('admins', user.orcid); + return isMemberOf('admins', user.orcid); }); roles.use('access this community', async ctx => { log.debug('Checking if user can edit this community.'); - const user = await roles.getUser(ctx); + const user = await getUser(ctx); if (!user) return false; - const isAdmin = await roles.isMemberOf('admins', user.orcid); + const isAdmin = await isMemberOf('admins', user.orcid); if (isAdmin) return true; if (ctx.state.community) { - return roles.isMemberOfCommunity(ctx.state.community, user.orcid); + return isMemberOfCommunity(ctx.state.community, user.orcid); } else { return false; } @@ -113,14 +130,14 @@ const authWrapper = (users, groups, communities, personas) => { roles.use('edit this community', async ctx => { log.debug('Checking if user can edit this community.'); - const user = await roles.getUser(ctx); + const user = await getUser(ctx); if (!user) return false; - const isAdmin = await roles.isMemberOf('admins', user.orcid); + const isAdmin = await isMemberOf('admins', user.orcid); if (isAdmin) return true; if (ctx.state.community) { - return roles.isOwnerOfCommunity(ctx.state.community, user.orcid); + return isOwnerOfCommunity(ctx.state.community, user.orcid); } else { return false; } @@ -128,14 +145,14 @@ const authWrapper = (users, groups, communities, personas) => { roles.use('edit this persona', async ctx => { log.debug('Checking if user can edit this persona.'); - const user = await roles.getUser(ctx); + const user = await getUser(ctx); if (!user) return false; - const isAdmin = await roles.isMemberOf('admins', user.orcid); + const isAdmin = await isMemberOf('admins', user.orcid); if (isAdmin) return true; if (ctx.state.persona) { - return roles.isIdentityOf(ctx.state.persona, user.orcid); + return isIdentityOf(ctx.state.persona, user.orcid); } else { return false; } @@ -143,10 +160,10 @@ const authWrapper = (users, groups, communities, personas) => { roles.use('edit this user', async ctx => { log.debug('Checking if user can edit this user.'); - const user = await roles.getUser(ctx); + const user = await getUser(ctx); if (!user) return false; - const isAdmin = await roles.isMemberOf('admins', user.orcid); + const isAdmin = await isMemberOf('admins', user.orcid); if (isAdmin) return true; if (ctx.state.identity) { @@ -158,7 +175,15 @@ const authWrapper = (users, groups, communities, personas) => { } }); - return roles; + return { + can: roles.can.bind(roles), + middleware: roles.middleware.bind(roles), + getUser, + isIdentityOf, + isMemberOf, + isMemberOfCommunity, + isOwnerOfCommunity, + }; }; export default authWrapper; diff --git a/src/backend/models/users.ts b/src/backend/models/users.ts index 85c66891..6ecea843 100644 --- a/src/backend/models/users.ts +++ b/src/backend/models/users.ts @@ -30,7 +30,7 @@ export class UserModel extends EntityRepository { findOneByKey( app: string, secret: string, - params: string[], + params?: string[], ): Promise { try { return this.em.findOne(