Group based roles

This commit is contained in:
Douglas Barone 2023-06-19 13:44:21 -04:00
parent 431700a888
commit c05c0d8f75
7 changed files with 93 additions and 50 deletions

View File

@ -1,28 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[serialNumber]` on the table `Printer` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Printer" ADD COLUMN "serialNumber" TEXT;
-- CreateTable
CREATE TABLE "PrinterStatus" (
"id" SERIAL NOT NULL,
"tonerBlackLevel" INTEGER NOT NULL,
"tonerCyanLevel" INTEGER,
"tonerMagentaLevel" INTEGER,
"tonerYellowLevel" INTEGER,
"counter" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"printerId" INTEGER NOT NULL,
CONSTRAINT "PrinterStatus_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Printer_serialNumber_key" ON "Printer"("serialNumber");
-- AddForeignKey
ALTER TABLE "PrinterStatus" ADD CONSTRAINT "PrinterStatus_printerId_fkey" FOREIGN KEY ("printerId") REFERENCES "Printer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -21,15 +21,40 @@ CREATE TABLE "User" (
-- CreateTable -- CreateTable
CREATE TABLE "Printer" ( CREATE TABLE "Printer" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,
"hostname" TEXT,
"friendlyName" TEXT, "friendlyName" TEXT,
"serialNumber" TEXT,
"hostname" TEXT,
"ip" TEXT NOT NULL, "ip" TEXT NOT NULL,
"model" "PrinterModel" NOT NULL, "model" "PrinterModel" NOT NULL,
"blackTonerModel" TEXT,
"cyanTonerModel" TEXT,
"magentaTonerModel" TEXT,
"yellowTonerModel" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Printer_pkey" PRIMARY KEY ("id") CONSTRAINT "Printer_pkey" PRIMARY KEY ("id")
); );
-- CreateTable
CREATE TABLE "PrinterStatus" (
"id" SERIAL NOT NULL,
"tonerBlackLevel" INTEGER NOT NULL,
"tonerCyanLevel" INTEGER,
"tonerMagentaLevel" INTEGER,
"tonerYellowLevel" INTEGER,
"counter" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"printerId" INTEGER NOT NULL,
CONSTRAINT "PrinterStatus_pkey" PRIMARY KEY ("id")
);
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "Printer_serialNumber_key" ON "Printer"("serialNumber");
-- AddForeignKey
ALTER TABLE "PrinterStatus" ADD CONSTRAINT "PrinterStatus_printerId_fkey" FOREIGN KEY ("printerId") REFERENCES "Printer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -15,6 +15,7 @@ type LdapUser = {
mail: string | null mail: string | null
displayName: string displayName: string
thumbnailPhoto: string | null thumbnailPhoto: string | null
groups?: string[]
} }
export class LdapController extends Client implements LdapClientInterface { export class LdapController extends Client implements LdapClientInterface {
@ -45,19 +46,25 @@ export class LdapController extends Client implements LdapClientInterface {
} }
} }
async getUser(username: string) { async getUser(username: string): Promise<LdapUser> {
return await this.adminBondOperation(async () => { return await this.adminBondOperation(async () => {
const { searchEntries } = await this.search(DN, { const { searchEntries } = await this.search(DN, {
scope: 'sub', scope: 'sub',
filter: `(sAMAccountName=${username})`, filter: `(sAMAccountName=${username})`,
attributes: ['mail', 'sAMAccountName', 'displayName', 'thumbnailPhoto'], attributes: [
'mail',
'sAMAccountName',
'displayName',
'thumbnailPhoto',
'dn'
],
explicitBufferAttributes: ['thumbnailPhoto'] explicitBufferAttributes: ['thumbnailPhoto']
}) })
if (!searchEntries.length) if (!searchEntries.length)
throw new Error('User not found on LDAP server.') throw new Error('User not found on LDAP server.')
const { sAMAccountName, displayName, mail, thumbnailPhoto } = const { sAMAccountName, displayName, mail, thumbnailPhoto, dn } =
searchEntries[0] searchEntries[0]
const ldapUser: LdapUser = { const ldapUser: LdapUser = {
@ -66,13 +73,26 @@ export class LdapController extends Client implements LdapClientInterface {
mail: mail.toString(), mail: mail.toString(),
thumbnailPhoto: `data:image/png;base64,${Buffer.from( thumbnailPhoto: `data:image/png;base64,${Buffer.from(
thumbnailPhoto as Buffer thumbnailPhoto as Buffer
).toString('base64')}` ).toString('base64')}`,
groups: await this.getGroupsForUser(dn.toString())
} }
return ldapUser return ldapUser
}) })
} }
async getGroupsForUser(dn: string) {
const { searchEntries } = await this.search(DN, {
scope: 'sub',
filter: `(member:1.2.840.113556.1.4.1941:=${dn})`,
attributes: ['cn']
})
if (!searchEntries.length) throw new Error('User not found on LDAP server.')
return searchEntries.map(entry => entry.cn.toString())
}
async authenticate(username: string, password: string) { async authenticate(username: string, password: string) {
await this.bind(`${DOMAIN}\\${username}`, password) await this.bind(`${DOMAIN}\\${username}`, password)

View File

@ -1,16 +1,35 @@
import { User } from '@prisma/client'
import { LdapController } from '../controllers/LdapController.js' import { LdapController } from '../controllers/LdapController.js'
import { prisma } from '../prisma.js' import { prisma } from '../prisma.js'
const ADMIN_GROUP = process.env.ADMIN_GROUP || 'PP-SERTI'
const INSPECTOR_GROUP = process.env.INSPECTOR_GROUP || 'inspector'
const USER_GROUP = process.env.USER_GROUP || 'G_SERVIDORES'
export class UserController { export class UserController {
static async importUser(username: string) { static async importUser(username: string) {
const ldap = new LdapController() const ldap = new LdapController()
const user = await ldap.getUser(username) const ldapUser = await ldap.getUser(username)
if (!user) throw new Error('User not found!') if (!ldapUser) throw new Error('User not found!')
const user = {
username: ldapUser.username,
displayName: ldapUser.displayName,
mail: ldapUser.mail,
thumbnailPhoto: ldapUser.thumbnailPhoto,
roles: [] as User['roles']
}
ldapUser.groups?.forEach(group => {
if (group === USER_GROUP) user.roles?.push('USER')
if (group === ADMIN_GROUP) user.roles?.push('ADMIN')
if (group === INSPECTOR_GROUP) user.roles?.push('INSPECTOR')
})
return await prisma.user.upsert({ return await prisma.user.upsert({
where: { username: user.username }, where: { username: ldapUser.username },
update: user, update: user,
create: user create: user
}) })

View File

@ -0,0 +1,18 @@
import { Request, Response } from 'express'
import { AuthenticationController } from '../AuthenticationController.js'
export class LoginRouteController {
static async login(req: Request, res: Response) {
const { username, password } = req.body
if (!username || !password)
return res.status(400).json({ error: 'Missing username or password' })
try {
const token = await AuthenticationController.login(username, password)
res.json({ token })
} catch (error: any) {
res.status(401).json({ error: error.message })
}
}
}

View File

@ -13,7 +13,7 @@ export class UserRouteController {
where: { username } where: { username }
}) })
if (!user) return await UserController.importUser(username) if (!user) return res.json(await UserController.importUser(username))
else UserController.importUser(username) else UserController.importUser(username)
res.json(user) res.json(user)

View File

@ -7,6 +7,7 @@ import { hasRolesMiddleware } from './middleware/hasRolesMiddleware.js'
import { UserRouteController } from './controllers/routes/UserRouteController.js' import { UserRouteController } from './controllers/routes/UserRouteController.js'
import { AuthenticationController } from './controllers/AuthenticationController.js' import { AuthenticationController } from './controllers/AuthenticationController.js'
import { LoginRouteController } from './controllers/routes/LoginRouteController.js'
export const app = express() export const app = express()
@ -22,19 +23,7 @@ app.get('/api/', async (req: Request, res: Response) => {
}) })
// Login route // Login route
app.post('/api/login', async (req: Request, res: Response) => { app.post('/api/login', LoginRouteController.login)
const { username, password } = req.body
if (!username || !password)
return res.status(400).json({ error: 'Missing username or password' })
try {
const token = await AuthenticationController.login(username, password)
res.json({ token })
} catch (error: any) {
res.status(401).json({ error: error.message })
}
})
app.get('/api/me', authMiddleware, async (req: Request, res: Response) => app.get('/api/me', authMiddleware, async (req: Request, res: Response) =>
res.json(res.locals.user) res.json(res.locals.user)