diff --git a/api/src/controllers/group.ts b/api/src/controllers/group.ts index 9f6e41b..c5b8681 100644 --- a/api/src/controllers/group.ts +++ b/api/src/controllers/group.ts @@ -10,7 +10,7 @@ import { Body } from 'tsoa' -import Group, { GroupPayload } from '../model/Group' +import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group' import User from '../model/User' import { UserResponse } from './user' @@ -241,6 +241,13 @@ const updateUsersListInGroup = async ( message: 'Group not found.' } + if (group.name === PUBLIC_GROUP_NAME) + throw { + code: 400, + status: 'Bad Request', + message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.` + } + const user = await User.findOne({ id: userId }) if (!user) throw { diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index 24ed1e8..78e4bcb 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -5,7 +5,9 @@ import { fetchLatestAutoExec, ModeType, verifyTokenInDB, - isAuthorizingRoute + isAuthorizingRoute, + isPublicRoute, + publicUser } from '../utils' import { desktopUser } from './desktop' import { authorize } from './authorize' @@ -41,7 +43,7 @@ export const authenticateAccessToken: RequestHandler = async ( return res.sendStatus(401) } - authenticateToken( + await authenticateToken( req, res, nextFunction, @@ -50,8 +52,12 @@ export const authenticateAccessToken: RequestHandler = async ( ) } -export const authenticateRefreshToken: RequestHandler = (req, res, next) => { - authenticateToken( +export const authenticateRefreshToken: RequestHandler = async ( + req, + res, + next +) => { + await authenticateToken( req, res, next, @@ -60,7 +66,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => { ) } -const authenticateToken = ( +const authenticateToken = async ( req: Request, res: Response, next: NextFunction, @@ -83,12 +89,12 @@ const authenticateToken = ( const authHeader = req.headers['authorization'] const token = authHeader?.split(' ')[1] - if (!token) return res.sendStatus(401) - jwt.verify(token, key, async (err: any, data: any) => { - if (err) return res.sendStatus(401) + try { + if (!token) throw 'Unauthorized' + + const data: any = jwt.verify(token, key) - // verify this valid token's entry in DB const user = await verifyTokenInDB( data?.userId, data?.clientId, @@ -101,8 +107,16 @@ const authenticateToken = ( req.user = user if (tokenType === 'accessToken') req.accessToken = token return next() - } else return res.sendStatus(401) + } else throw 'Unauthorized' } - return res.sendStatus(401) - }) + + throw 'Unauthorized' + } catch (error) { + if (await isPublicRoute(req)) { + req.user = publicUser + return next() + } + + res.sendStatus(401) + } } diff --git a/api/src/middlewares/authorize.ts b/api/src/middlewares/authorize.ts index 3901b3e..6b389fb 100644 --- a/api/src/middlewares/authorize.ts +++ b/api/src/middlewares/authorize.ts @@ -5,7 +5,7 @@ import { PermissionSettingForRoute, PermissionType } from '../controllers/permission' -import { getPath } from '../utils' +import { getPath, isPublicRoute } from '../utils' export const authorize: RequestHandler = async (req, res, next) => { const { user } = req @@ -17,6 +17,9 @@ export const authorize: RequestHandler = async (req, res, next) => { // no need to check for permissions when user is admin if (user.isAdmin) return next() + // no need to check for permissions when route is Public + if (await isPublicRoute(req)) return next() + const dbUser = await User.findOne({ id: user.userId }) if (!dbUser) return res.sendStatus(401) diff --git a/api/src/model/Group.ts b/api/src/model/Group.ts index 6341825..3185f44 100644 --- a/api/src/model/Group.ts +++ b/api/src/model/Group.ts @@ -3,6 +3,8 @@ import { GroupDetailsResponse } from '../controllers' import User, { IUser } from './User' const AutoIncrement = require('mongoose-sequence')(mongoose) +export const PUBLIC_GROUP_NAME = 'Public' + export interface GroupPayload { /** * Name of the group diff --git a/api/src/routes/api/spec/group.spec.ts b/api/src/routes/api/spec/group.spec.ts index 86196ce..fe1072e 100644 --- a/api/src/routes/api/spec/group.spec.ts +++ b/api/src/routes/api/spec/group.spec.ts @@ -5,6 +5,7 @@ import request from 'supertest' import appPromise from '../../../app' import { UserController, GroupController } from '../../../controllers/' import { generateAccessToken, saveTokensInDB } from '../../../utils' +import { PUBLIC_GROUP_NAME } from '../../../model/Group' const clientId = 'someclientID' const adminUser = { @@ -27,6 +28,12 @@ const group = { description: 'DC group for testing purposes.' } +const PUBLIC_GROUP = { + name: PUBLIC_GROUP_NAME, + description: + 'A special group that can be used to bypass authentication for particular routes.' +} + const userController = new UserController() const groupController = new GroupController() @@ -535,6 +542,24 @@ describe('group', () => { expect(res.text).toEqual('User not found.') expect(res.body).toEqual({}) }) + + it('should respond with Bad Request when adding user to Public group', async () => { + const dbGroup = await groupController.createGroup(PUBLIC_GROUP) + const dbUser = await userController.createUser({ + ...user, + username: 'publicUser' + }) + + const res = await request(app) + .post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) + .auth(adminAccessToken, { type: 'bearer' }) + .send() + .expect(400) + + expect(res.text).toEqual( + `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.` + ) + }) }) describe('RemoveUser', () => { diff --git a/api/src/utils/index.ts b/api/src/utils/index.ts index 13bc904..32b5b79 100644 --- a/api/src/utils/index.ts +++ b/api/src/utils/index.ts @@ -16,6 +16,7 @@ export * from './getRunTimeAndFilePath' export * from './getServerUrl' export * from './instantiateLogger' export * from './isDebugOn' +export * from './isPublicRoute' export * from './zipped' export * from './parseLogToArray' export * from './removeTokensInDB' diff --git a/api/src/utils/isPublicRoute.ts b/api/src/utils/isPublicRoute.ts new file mode 100644 index 0000000..0b121ee --- /dev/null +++ b/api/src/utils/isPublicRoute.ts @@ -0,0 +1,31 @@ +import { Request } from 'express' +import { getPath } from './getAuthorizedRoutes' +import Group, { PUBLIC_GROUP_NAME } from '../model/Group' +import Permission from '../model/Permission' +import { PermissionSettingForRoute } from '../controllers' +import { RequestUser } from '../types' + +export const isPublicRoute = async (req: Request): Promise => { + const group = await Group.findOne({ name: PUBLIC_GROUP_NAME }) + if (group) { + const path = getPath(req) + + const groupPermission = await Permission.findOne({ + path, + group: group?._id + }) + if (groupPermission?.setting === PermissionSettingForRoute.grant) + return true + } + + return false +} + +export const publicUser: RequestUser = { + userId: 0, + clientId: 'public_app', + username: 'publicUser', + displayName: 'Public User', + isAdmin: false, + isActive: true +} diff --git a/api/src/utils/seedDB.ts b/api/src/utils/seedDB.ts index 7bff3a6..5187900 100644 --- a/api/src/utils/seedDB.ts +++ b/api/src/utils/seedDB.ts @@ -1,5 +1,5 @@ import Client from '../model/Client' -import Group from '../model/Group' +import Group, { PUBLIC_GROUP_NAME } from '../model/Group' import User from '../model/User' import Configuration, { ConfigurationType } from '../model/Configuration' @@ -31,6 +31,15 @@ export const seedDB = async (): Promise => { console.log(`DB Seed - Group created: ${GROUP.name}`) } + // Checking if 'Public' Group is already in the database + const publicGroupExist = await Group.findOne({ name: PUBLIC_GROUP.name }) + if (!publicGroupExist) { + const group = new Group(PUBLIC_GROUP) + await group.save() + + console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`) + } + // Checking if user is already in the database let usernameExist = await User.findOne({ username: ADMIN_USER.username }) if (!usernameExist) { @@ -68,6 +77,13 @@ const GROUP = { name: 'AllUsers', description: 'Group contains all users' } + +const PUBLIC_GROUP = { + name: PUBLIC_GROUP_NAME, + description: + 'A special group that can be used to bypass authentication for particular routes.' +} + const CLIENT = { clientId: 'clientID1', clientSecret: 'clientSecret'