1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-08 07:00:04 +00:00

Merge pull request #293 from sasjs/ldap

feat: integratedLDAP authentication
This commit is contained in:
Allan Bowe
2022-10-03 13:09:07 +01:00
committed by GitHub
24 changed files with 1165 additions and 86 deletions

View File

@@ -0,0 +1,185 @@
import express from 'express'
import { Security, Route, Tags, Get, Post, Example } from 'tsoa'
import { LDAPClient, LDAPUser, LDAPGroup, AuthProviderType } from '../utils'
import { randomBytes } from 'crypto'
import User from '../model/User'
import Group from '../model/Group'
import Permission from '../model/Permission'
@Security('bearerAuth')
@Route('SASjsApi/authConfig')
@Tags('Auth_Config')
export class AuthConfigController {
/**
* @summary Gives the detail of Auth Mechanism.
*
*/
@Example({
ldap: {
LDAP_URL: 'ldaps://my.ldap.server:636',
LDAP_BIND_DN: 'cn=admin,ou=system,dc=cloudron',
LDAP_BIND_PASSWORD: 'secret',
LDAP_USERS_BASE_DN: 'ou=users,dc=cloudron',
LDAP_GROUPS_BASE_DN: 'ou=groups,dc=cloudron'
}
})
@Get('/')
public getDetail() {
return getAuthConfigDetail()
}
/**
* @summary Synchronizes LDAP users and groups with internal DB and returns the count of imported users and groups.
*
*/
@Example({
users: 5,
groups: 3
})
@Post('/synchronizeWithLDAP')
public async synchronizeWithLDAP() {
return synchronizeWithLDAP()
}
}
const synchronizeWithLDAP = async () => {
process.logger.info('Syncing LDAP with internal DB')
const permissions = await Permission.get({})
await Permission.deleteMany()
await User.deleteMany({ authProvider: AuthProviderType.LDAP })
await Group.deleteMany({ authProvider: AuthProviderType.LDAP })
const ldapClient = await LDAPClient.init()
process.logger.info('fetching LDAP users')
const users = await ldapClient.getAllLDAPUsers()
process.logger.info('inserting LDAP users to DB')
const existingUsers: string[] = []
const importedUsers: LDAPUser[] = []
for (const user of users) {
const usernameExists = await User.findOne({ username: user.username })
if (usernameExists) {
existingUsers.push(user.username)
continue
}
const hashPassword = User.hashPassword(randomBytes(64).toString('hex'))
await User.create({
displayName: user.displayName,
username: user.username,
password: hashPassword,
authProvider: AuthProviderType.LDAP
})
importedUsers.push(user)
}
if (existingUsers.length > 0) {
process.logger.info(
'Failed to insert following users as they already exist in DB:'
)
existingUsers.forEach((user) => process.logger.log(`* ${user}`))
}
process.logger.info('fetching LDAP groups')
const groups = await ldapClient.getAllLDAPGroups()
process.logger.info('inserting LDAP groups to DB')
const existingGroups: string[] = []
const importedGroups: LDAPGroup[] = []
for (const group of groups) {
const groupExists = await Group.findOne({ name: group.name })
if (groupExists) {
existingGroups.push(group.name)
continue
}
await Group.create({
name: group.name,
authProvider: AuthProviderType.LDAP
})
importedGroups.push(group)
}
if (existingGroups.length > 0) {
process.logger.info(
'Failed to insert following groups as they already exist in DB:'
)
existingGroups.forEach((group) => process.logger.log(`* ${group}`))
}
process.logger.info('associating users and groups')
for (const group of importedGroups) {
const dbGroup = await Group.findOne({ name: group.name })
if (dbGroup) {
for (const member of group.members) {
const user = importedUsers.find((user) => user.uid === member)
if (user) {
const dbUser = await User.findOne({ username: user.username })
if (dbUser) await dbGroup.addUser(dbUser)
}
}
}
}
process.logger.info('setting permissions')
for (const permission of permissions) {
const newPermission = new Permission({
path: permission.path,
type: permission.type,
setting: permission.setting
})
if (permission.user) {
const dbUser = await User.findOne({ username: permission.user.username })
if (dbUser) newPermission.user = dbUser._id
} else if (permission.group) {
const dbGroup = await Group.findOne({ name: permission.group.name })
if (dbGroup) newPermission.group = dbGroup._id
}
await newPermission.save()
}
process.logger.info('LDAP synchronization completed!')
return {
userCount: importedUsers.length,
groupCount: importedGroups.length
}
}
const getAuthConfigDetail = () => {
const { AUTH_PROVIDERS } = process.env
const returnObj: any = {}
if (AUTH_PROVIDERS === AuthProviderType.LDAP) {
const {
LDAP_URL,
LDAP_BIND_DN,
LDAP_BIND_PASSWORD,
LDAP_USERS_BASE_DN,
LDAP_GROUPS_BASE_DN
} = process.env
returnObj.ldap = {
LDAP_URL: LDAP_URL ?? '',
LDAP_BIND_DN: LDAP_BIND_DN ?? '',
LDAP_BIND_PASSWORD: LDAP_BIND_PASSWORD ?? '',
LDAP_USERS_BASE_DN: LDAP_USERS_BASE_DN ?? '',
LDAP_GROUPS_BASE_DN: LDAP_GROUPS_BASE_DN ?? ''
}
}
return returnObj
}

View File

@@ -12,6 +12,7 @@ import {
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User'
import { AuthProviderType } from '../utils'
import { UserResponse } from './user'
export interface GroupResponse {
@@ -147,12 +148,14 @@ export class GroupController {
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const group = await Group.findOne({ groupId })
if (group) return await group.remove()
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
return await group.remove()
}
}
@@ -248,6 +251,13 @@ const updateUsersListInGroup = async (
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
}
if (group.authProvider !== AuthProviderType.Internal)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const user = await User.findOne({ id: userId })
if (!user)
throw {
@@ -256,6 +266,13 @@ const updateUsersListInGroup = async (
message: 'User not found.'
}
if (user.authProvider !== AuthProviderType.Internal)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const updatedGroup =
action === 'addUser'
? await group.addUser(user)

View File

@@ -1,4 +1,5 @@
export * from './auth'
export * from './authConfig'
export * from './client'
export * from './code'
export * from './drive'

View File

@@ -17,7 +17,12 @@ import {
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import {
getUserAutoExec,
updateUserAutoExec,
ModeType,
AuthProviderType
} from '../utils'
import { GroupResponse } from './group'
export interface UserResponse {
@@ -211,7 +216,11 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) throw new Error('Username already exists.')
if (usernameExist)
throw {
code: 409,
message: 'Username already exists.'
}
// Hash passwords
const hashPassword = User.hashPassword(password)
@@ -255,7 +264,11 @@ const getUser = async (
'groupId name description -_id'
)) as unknown as UserDetailsResponse
if (!user) throw new Error('User is not found.')
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
return {
id: user.id,
@@ -284,6 +297,19 @@ const updateUser = async (
const params: any = { displayName, isAdmin, isActive, autoExec }
const user = await User.findOne(findBy)
if (
user?.authProvider !== AuthProviderType.Internal &&
(username !== user?.username || displayName !== user?.displayName)
) {
throw {
code: 405,
message:
'Can not update username and display name of user that is created by an external auth provider.'
}
}
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
@@ -292,7 +318,10 @@ const updateUser = async (
(findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username != findBy.username)
)
throw new Error('Username already exists.')
throw {
code: 409,
message: 'Username already exists.'
}
}
params.username = username
}
@@ -305,7 +334,10 @@ const updateUser = async (
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
if (!updatedUser)
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
throw {
code: 404,
message: `Unable to find user with ${findBy.id || findBy.username}`
}
return {
id: updatedUser.id,
@@ -332,11 +364,19 @@ const deleteUser = async (
{ password }: { password?: string }
) => {
const user = await User.findOne(findBy)
if (!user) throw new Error('User is not found.')
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
if (!isAdmin) {
const validPass = user.comparePassword(password!)
if (!validPass) throw new Error('Invalid password.')
if (!validPass)
throw {
code: 401,
message: 'Invalid password.'
}
}
await User.deleteOne(findBy)

View File

@@ -5,7 +5,12 @@ import { readFile } from '@sasjs/utils'
import User from '../model/User'
import Client from '../model/Client'
import { getWebBuildFolder, generateAuthCode } from '../utils'
import {
getWebBuildFolder,
generateAuthCode,
AuthProviderType,
LDAPClient
} from '../utils'
import { InfoJWT } from '../types'
import { AuthController } from './auth'
@@ -80,8 +85,16 @@ const login = async (
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
const validPass = user.comparePassword(password)
if (!validPass) throw new Error('Invalid password.')
if (
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
user.authProvider === AuthProviderType.LDAP
) {
const ldapClient = await LDAPClient.init()
await ldapClient.verifyUser(username, password)
} else {
const validPass = user.comparePassword(password)
if (!validPass) throw new Error('Invalid password.')
}
req.session.loggedIn = true
req.session.user = {

View File

@@ -1,6 +1,7 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User'
import { AuthProviderType } from '../utils'
const AutoIncrement = require('mongoose-sequence')(mongoose)
export const PUBLIC_GROUP_NAME = 'Public'
@@ -27,6 +28,7 @@ interface IGroupDocument extends GroupPayload, Document {
groupId: number
isActive: boolean
users: Schema.Types.ObjectId[]
authProvider?: AuthProviderType
}
interface IGroup extends IGroupDocument {
@@ -46,6 +48,11 @@ const groupSchema = new Schema<IGroupDocument>({
type: String,
default: 'Group description.'
},
authProvider: {
type: String,
enum: AuthProviderType,
default: 'internal'
},
isActive: {
type: Boolean,
default: true

View File

@@ -1,6 +1,7 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import bcrypt from 'bcryptjs'
import { AuthProviderType } from '../utils'
export interface UserPayload {
/**
@@ -42,6 +43,7 @@ interface IUserDocument extends UserPayload, Document {
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
authProvider?: AuthProviderType
}
export interface IUser extends IUserDocument {
@@ -67,6 +69,11 @@ const userSchema = new Schema<IUserDocument>({
type: String,
required: true
},
authProvider: {
type: String,
enum: AuthProviderType,
default: 'internal'
},
isAdmin: {
type: Boolean,
default: false

View File

@@ -0,0 +1,25 @@
import express from 'express'
import { AuthConfigController } from '../../controllers'
const authConfigRouter = express.Router()
authConfigRouter.get('/', async (req, res) => {
const controller = new AuthConfigController()
try {
const response = controller.getDetail()
res.send(response)
} catch (err: any) {
res.status(500).send(err.toString())
}
})
authConfigRouter.post('/synchronizeWithLDAP', async (req, res) => {
const controller = new AuthConfigController()
try {
const response = await controller.synchronizeWithLDAP()
res.send(response)
} catch (err: any) {
res.status(500).send(err.toString())
}
})
export default authConfigRouter

View File

@@ -18,11 +18,7 @@ groupRouter.post(
const response = await controller.createGroup(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -33,11 +29,7 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups()
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
})
@@ -49,11 +41,7 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
})
@@ -71,11 +59,7 @@ groupRouter.get(
const response = await controller.getGroupByGroupName(name)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -95,11 +79,7 @@ groupRouter.post(
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -119,11 +99,7 @@ groupRouter.delete(
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -140,11 +116,7 @@ groupRouter.delete(
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)

View File

@@ -18,6 +18,7 @@ import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
import permissionRouter from './permission'
import authConfigRouter from './authConfig'
const router = express.Router()
@@ -43,6 +44,14 @@ router.use(
permissionRouter
)
router.use(
'/authConfig',
desktopRestrict,
authenticateAccessToken,
verifyAdmin,
authConfigRouter
)
router.use(
'/',
swaggerUi.serve,

View File

@@ -4,8 +4,13 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
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'
import {
generateAccessToken,
saveTokensInDB,
AuthProviderType
} from '../../../utils'
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
import User from '../../../model/User'
const clientId = 'someclientID'
const adminUser = {
@@ -560,6 +565,46 @@ describe('group', () => {
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
)
})
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
const dbGroup = await Group.create({
...group,
authProvider: AuthProviderType.LDAP
})
const dbUser = await userController.createUser({
...user,
username: 'ldapGroupUser'
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await User.create({
...user,
username: 'ldapUser',
authProvider: AuthProviderType.LDAP
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
})
describe('RemoveUser', () => {
@@ -611,6 +656,46 @@ describe('group', () => {
expect(res.body.groups).toEqual([])
})
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
const dbGroup = await Group.create({
...group,
authProvider: AuthProviderType.LDAP
})
const dbUser = await userController.createUser({
...user,
username: 'removeLdapGroupUser'
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await User.create({
...user,
username: 'removeLdapUser',
authProvider: AuthProviderType.LDAP
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')

View File

@@ -4,7 +4,12 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import {
generateAccessToken,
saveTokensInDB,
AuthProviderType
} from '../../../utils'
import User from '../../../model/User'
const clientId = 'someclientID'
const adminUser = {
@@ -110,16 +115,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
await controller.createUser(user)
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send(user)
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
@@ -226,6 +231,36 @@ describe('user', () => {
.expect(400)
})
it('should respond with Method Not Allowed, when updating username of user created by an external auth provider', async () => {
const dbUser = await User.create({
...user,
authProvider: AuthProviderType.LDAP
})
const accessToken = await generateAndSaveToken(dbUser!.id)
const newUsername = 'newUsername'
await request(app)
.patch(`/SASjsApi/user/${dbUser!.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ username: newUsername })
.expect(405)
})
it('should respond with Method Not Allowed, when updating displayName of user created by an external auth provider', async () => {
const dbUser = await User.create({
...user,
authProvider: AuthProviderType.LDAP
})
const accessToken = await generateAndSaveToken(dbUser!.id)
const newDisplayName = 'My new display Name'
await request(app)
.patch(`/SASjsApi/user/${dbUser!.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ displayName: newDisplayName })
.expect(405)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/user/1234')
@@ -254,7 +289,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
@@ -265,9 +300,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
@@ -349,7 +384,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
@@ -360,9 +395,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
})
@@ -446,7 +481,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
@@ -454,9 +489,9 @@ describe('user', () => {
.delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
.expect(401)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.text).toEqual('Invalid password.')
expect(res.body).toEqual({})
})
@@ -528,7 +563,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
@@ -536,9 +571,9 @@ describe('user', () => {
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
.expect(401)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.text).toEqual('Invalid password.')
expect(res.body).toEqual({})
})
})
@@ -652,16 +687,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if userId is incorrect', async () => {
it('should respond with Not Found if userId is incorrect', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User is not found.')
expect(res.text).toEqual('User is not found.')
expect(res.body).toEqual({})
})
@@ -731,16 +766,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is incorrect', async () => {
it('should respond with Not Found if username is incorrect', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user/by/username/randomUsername')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User is not found.')
expect(res.text).toEqual('User is not found.')
expect(res.body).toEqual({})
})
})

View File

@@ -23,7 +23,7 @@ userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
const response = await controller.createUser(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -33,7 +33,7 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllUsers()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -51,7 +51,7 @@ userRouter.get(
const response = await controller.getUserByUsername(req, username)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -64,7 +64,7 @@ userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const response = await controller.getUser(req, parseInt(userId))
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -91,7 +91,7 @@ userRouter.patch(
const response = await controller.updateUserByUsername(username, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -113,7 +113,7 @@ userRouter.patch(
const response = await controller.updateUser(parseInt(userId), body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -141,7 +141,7 @@ userRouter.delete(
await controller.deleteUserByUsername(username, data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -163,7 +163,7 @@ userRouter.delete(
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)

View File

@@ -18,6 +18,7 @@ export * from './getTokensFromDB'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'
export * from './ldapClient'
export * from './zipped'
export * from './parseLogToArray'
export * from './removeTokensInDB'

163
api/src/utils/ldapClient.ts Normal file
View File

@@ -0,0 +1,163 @@
import { createClient, Client } from 'ldapjs'
import { ReturnCode } from './verifyEnvVariables'
export interface LDAPUser {
uid: string
username: string
displayName: string
}
export interface LDAPGroup {
name: string
members: string[]
}
export class LDAPClient {
private ldapClient: Client
private static classInstance: LDAPClient | null
private constructor() {
process.logger.info('creating LDAP client')
this.ldapClient = createClient({ url: process.env.LDAP_URL as string })
this.ldapClient.on('error', (error) => {
process.logger.error(error.message)
})
}
static async init() {
if (!LDAPClient.classInstance) {
LDAPClient.classInstance = new LDAPClient()
process.logger.info('binding LDAP client')
await LDAPClient.classInstance.bind().catch((error) => {
LDAPClient.classInstance = null
throw error
})
}
return LDAPClient.classInstance
}
private async bind() {
const promise = new Promise<void>((resolve, reject) => {
const { LDAP_BIND_DN, LDAP_BIND_PASSWORD } = process.env
this.ldapClient.bind(LDAP_BIND_DN!, LDAP_BIND_PASSWORD!, (error) => {
if (error) reject(error)
resolve()
})
})
await promise.catch((error) => {
throw new Error(error.message)
})
}
async getAllLDAPUsers() {
const promise = new Promise<LDAPUser[]>((resolve, reject) => {
const { LDAP_USERS_BASE_DN } = process.env
const filter = `(objectClass=*)`
this.ldapClient.search(
LDAP_USERS_BASE_DN!,
{ filter },
(error, result) => {
if (error) reject(error)
const users: LDAPUser[] = []
result.on('searchEntry', (entry) => {
users.push({
uid: entry.object.uid as string,
username: entry.object.username as string,
displayName: entry.object.displayname as string
})
})
result.on('end', (result) => {
resolve(users)
})
}
)
})
return await promise
.then((res) => res)
.catch((error) => {
throw new Error(error.message)
})
}
async getAllLDAPGroups() {
const promise = new Promise<LDAPGroup[]>((resolve, reject) => {
const { LDAP_GROUPS_BASE_DN } = process.env
this.ldapClient.search(LDAP_GROUPS_BASE_DN!, {}, (error, result) => {
if (error) reject(error)
const groups: LDAPGroup[] = []
result.on('searchEntry', (entry) => {
const members =
typeof entry.object.memberuid === 'string'
? [entry.object.memberuid]
: entry.object.memberuid
groups.push({
name: entry.object.cn as string,
members
})
})
result.on('end', (result) => {
resolve(groups)
})
})
})
return await promise
.then((res) => res)
.catch((error) => {
throw new Error(error.message)
})
}
async verifyUser(username: string, password: string) {
const promise = new Promise<boolean>((resolve, reject) => {
const { LDAP_USERS_BASE_DN } = process.env
const filter = `(username=${username})`
this.ldapClient.search(
LDAP_USERS_BASE_DN!,
{ filter },
(error, result) => {
if (error) reject(error)
const items: any = []
result.on('searchEntry', (entry) => {
items.push(entry.object)
})
result.on('end', (result) => {
if (result?.status !== 0 || items.length === 0) return reject()
// pick the first found
const user = items[0]
this.ldapClient.bind(user.dn, password, (error) => {
if (error) return reject(error)
resolve(true)
})
})
}
)
})
return await promise
.then(() => true)
.catch(() => {
throw new Error('Invalid password.')
})
}
}

View File

@@ -8,6 +8,11 @@ export enum ModeType {
Desktop = 'desktop'
}
export enum AuthProviderType {
LDAP = 'ldap',
Internal = 'internal'
}
export enum ProtocolType {
HTTP = 'http',
HTTPS = 'https'
@@ -64,6 +69,8 @@ export const verifyEnvVariables = (): ReturnCode => {
errors.push(...verifyExecutablePaths())
errors.push(...verifyLDAPVariables())
if (errors.length) {
process.logger?.error(
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
@@ -104,13 +111,24 @@ const verifyMODE = (): string[] => {
}
if (process.env.MODE === ModeType.Server) {
const { DB_CONNECT } = process.env
const { DB_CONNECT, AUTH_MECHANISM } = process.env
if (process.env.NODE_ENV !== 'test')
if (process.env.NODE_ENV !== 'test') {
if (!DB_CONNECT)
errors.push(
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
)
if (AUTH_MECHANISM) {
const authMechanismTypes = Object.values(AuthProviderType)
if (!authMechanismTypes.includes(AUTH_MECHANISM as AuthProviderType))
errors.push(
`- AUTH_MECHANISM '${AUTH_MECHANISM}'\n - valid options ${authMechanismTypes}`
)
} else {
process.env.AUTH_MECHANISM = DEFAULTS.AUTH_MECHANISM
}
}
}
return errors
@@ -280,8 +298,56 @@ const verifyExecutablePaths = () => {
return errors
}
const verifyLDAPVariables = () => {
const errors: string[] = []
const {
LDAP_URL,
LDAP_BIND_DN,
LDAP_BIND_PASSWORD,
LDAP_USERS_BASE_DN,
LDAP_GROUPS_BASE_DN,
MODE,
AUTH_MECHANISM
} = process.env
if (MODE === ModeType.Server && AUTH_MECHANISM === AuthProviderType.LDAP) {
if (!LDAP_URL) {
errors.push(
`- LDAP_URL is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_BIND_DN) {
errors.push(
`- LDAP_BIND_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_BIND_PASSWORD) {
errors.push(
`- LDAP_BIND_PASSWORD is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_USERS_BASE_DN) {
errors.push(
`- LDAP_USERS_BASE_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_GROUPS_BASE_DN) {
errors.push(
`- LDAP_GROUPS_BASE_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
)
}
}
return errors
}
const DEFAULTS = {
MODE: ModeType.Desktop,
AUTH_MECHANISM: AuthProviderType.Internal,
PROTOCOL: ProtocolType.HTTP,
PORT: '5000',
HELMET_COEP: HelmetCoepType.TRUE,