ifms-pti/server/src/classes/User.js
2022-02-01 11:35:37 -04:00

428 lines
11 KiB
JavaScript
Executable File

import { addSeconds, differenceInSeconds, addHours } from 'date-fns'
import { ad } from '../lib/activeDirectory'
import prisma from '../prisma'
import { Change, createClient } from 'ldapjs'
import { encodePassword } from '../lib/activeDirectory/encodePassword'
import config from '../lib/activeDirectory/config'
import jwt from 'jsonwebtoken'
import { pubsub, AUTH_UPDATED } from '../pubsub'
import { logError, logInfo, logSuccess, logWarning } from '../lib/logger'
import { performance } from 'perf_hooks'
class User {
constructor(username) {
this.username = username
}
/**
* Initializes the object, since asynchronous tasks can't run in the constructor
* @return {Promise<User>}
*/
async init(forceAD = false) {
try {
let user = await prisma.user.findUnique({
where: { sAMAccountName: this.username }
})
if (forceAD || !user || !user.roles || !user.groups)
user = await User.upsertUser(this.username)
else User.upsertUser(this.username)
Object.assign(this, user)
return this
} catch (e) {
throw new Error(e.message)
}
}
/**
* Checks if user is a Super Admin
* @return {Promise<boolean>}
*/
static async isSuperAdmin(username) {
const superAdminGroup = process.env.SUPER_ADMIN_GROUP || 'PP-SERTI'
return ad.isUserMemberOf(username, superAdminGroup)
}
/**
* Checks if user is a Token Generator
* @return {Promise<boolean>}
*/
static async isTokenCreator(username) {
const tokenCreatorGroup = process.env.TOKEN_CREATOR_GROUP || 'CEREL'
return await ad.isUserMemberOf(username, tokenCreatorGroup)
}
/**
* Checks if user is a Servant
* @return {Promise<boolean>}
*/
static async isServant(username) {
const servantGroup = process.env.SERVANT_GROUP || 'G_SERVIDORES'
return ad.isUserMemberOf(username, servantGroup)
}
/**
* Checks if user is a Student
* @return {Promise<boolean>}
*/
static async isStudent(username) {
const studentGroup = process.env.STUDENT_GROUP || 'Estudantes'
return ad.isUserMemberOf(username, studentGroup)
}
/**
* Returns the a user role(s)
* @param username
* @returns {Promise<[String]>}
*/
static async getRoles(username, groups = null) {
const rolesGroups = [
{
role: 'superAdmin',
adGroup: process.env.SUPER_ADMIN_GROUP || 'PP-SERTI'
},
{
role: 'tokenCreator',
adGroup: process.env.TOKEN_CREATOR_GROUP || 'PTI-TokenGen'
},
{
role: 'student',
adGroup: process.env.STUDENT_GROUP || 'Estudantes'
},
{
role: 'servant',
adGroup: process.env.SERVANT_GROUP || 'G_SERVIDORES'
},
{
role: 'watcher',
adGroup: process.env.WATCHER_GROUP || 'PTI-Vigias'
}
]
const userGroups = groups || (await User.getGroupsMembership(username))
const userRoles = rolesGroups
.filter(roleGroup =>
userGroups.some(userGroup => userGroup.cn === roleGroup.adGroup)
)
.map(role => role.role)
if (userRoles.some(role => role === 'superAdmin'))
userRoles.push('tokenCreator', 'watcher')
return [...new Set(userRoles)]
}
/**
* Checks if user has group membership
* @param {String} group
* @return {Promise<boolean>}
*/
async isMemberOf(group) {
return ad.isUserMemberOf(this.username, group)
}
/**
* Updates the user password.
* @param oldPassword
* @param newPassword
* @returns {Promise<User>}
*/
updatePassword(oldPassword, newPassword) {
if (oldPassword === newPassword)
throw new Error('A nova senha não pode ser igual à antiga')
return new Promise((resolve, reject) => {
const client = createClient(config)
client.on('error', err => {
client.unbind(() => {
logError({
message: 'Cliente LDAP desconectado devido à um erro.',
data: err,
tags: ['ldap']
})
})
reject(new Error(err.message))
})
client.bind(`ifms\\${this.username}`, oldPassword, (err, result) => {
if (err) {
client.bind(config.username, config.password, (err, result) => {
if (err) reject(new Error('Problemas técnicos, tente mais tarde'))
else reject(new Error('Usuário ou senha antiga incorretos.'))
})
} else
client.search(
config.baseDN,
{
filter: `(sAMAccountName=${this.username})`,
attributes: 'dn',
scope: 'sub'
},
(err, res) => {
res.on('error', err => {
reject(new Error(err.message))
})
res.on('searchEntry', entry => {
const userDN = entry.object.dn
client.bind(userDN, oldPassword, () => {
client.modify(
userDN,
[
new Change({
operation: 'delete',
modification: {
unicodePwd: encodePassword(oldPassword)
}
}),
new Change({
operation: 'add',
modification: {
unicodePwd: encodePassword(newPassword)
}
})
],
(err, result) => {
if (err) {
client.unbind(() => {
logError({
message:
'Cliente LDAP desconectado devido à um erro ao atualizar uma senha.',
data: err,
tags: ['ldap']
})
})
reject(new Error(err.message))
} else {
User.upsertUser(this.username, true).then(() => {
client.unbind(() => {
logSuccess({
message: `Usuário ${this.username} alterou sua senha.`,
data: result,
tags: ['ldap']
})
})
resolve(User.login(this.username, newPassword))
})
}
}
)
})
})
}
)
})
})
}
static async getGroupsMembership(username) {
return ad.getGroupMembershipForUser(
{ attributes: ['cn', 'dn', 'name'] },
username
)
}
/**
* Creates or updates a user in the database
*
* @return {Object}
*/
static async upsertUser(username, forceAD = false) {
const DEBOUNCE_TIME_IN_SECONDS = 30
const oldUserData = await prisma.user.findUnique({
where: { sAMAccountName: username }
})
const now = new Date()
if (
!forceAD &&
oldUserData &&
differenceInSeconds(
now,
addSeconds(oldUserData.updatedAt, DEBOUNCE_TIME_IN_SECONDS)
) < 0
)
return oldUserData
const adUser = await ad.findUser(username)
if (!adUser) throw new Error('Usuário não encontrado')
const groups = await User.getGroupsMembership(username)
const roles = await User.getRoles(username, groups)
const user = {
...adUser,
updatedAt: now,
roles,
groups
}
const newUserData = await prisma.user.upsert({
where: {
sAMAccountName: user.sAMAccountName
},
update: user,
create: user
})
pubsub.publish(AUTH_UPDATED, { authUpdated: newUserData })
return newUserData
}
/**
* Attemps to login user and returns AuthPayload data
* @param username
* @param password
* @returns {Promise<{expiresIn: String, user: User, token: String}>}
*/
static async login(username, password) {
logInfo({
tags: ['user', 'login'],
message: `Usuário ${username} está tentando logar`
})
try {
await ad.authenticate(`ifms\\${username}`, password)
} catch (err) {
if (await ad.checkBinding()) {
logWarning({
tags: ['user', 'login', 'password'],
message: `Usuário ${username} tentou logar, mas falhou.`
})
throw new Error('Usuário ou senha inválidos.')
} else {
logError({
tags: ['user', 'login', 'password'],
message: `Não é possível conectar ao AD.`,
data: err
})
throw new Error('Problemas técnicos ao autenticar. Tente mais tarde.')
}
}
try {
const user = await new User(username).init()
await prisma.user.update({
where: { sAMAccountName: user.sAMAccountName },
data: {
lastLoginPrior: user.lastLogin || new Date(),
lastLogin: new Date()
}
})
const payload = {
sAMAccountName: user.sAMAccountName,
pwdLastSet: user.pwdLastSet
}
const jwtSecret = process.env.JWT_SECRET || 'a good secret'
const options = { expiresIn: process.env.JWT_EXPIRATION || '72h' }
const token = jwt.sign(payload, jwtSecret, options)
logSuccess({
tags: ['user', 'login'],
message: `Usuário ${user.displayName} (${user.sAMAccountName}) logou com sucesso.`
})
return {
user,
token,
expiresIn: options.expiresIn,
expiresOn: addHours
}
} catch (e) {
throw new Error(
'Erro ao atualizar dados do usuário. Informe o departamento de TI.'
) // is DB connection OK?!
}
}
static async importAllUsers() {
if (this.working) {
logWarning({
tags: ['user', 'AD'],
message:
'Importação de usuários abortada! Já há uma importação em andamento.'
})
return 0
}
this.working = true
try {
logInfo({
tags: ['user', 'AD'],
message: 'Obtendo usuários do Active Directory.'
})
const allAdUsers = await ad.findUsers({
paged: true,
filter: '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' // Only active users
})
logInfo({
tags: ['user', 'AD'],
message: 'Importando usuários para o Banco de Dados local'
})
const startTime = performance.now()
// Do not promise.all, because it freezes the app
for (const [index, user] of allAdUsers.entries()) {
const dbUser = await prisma.user.upsert({
where: {
sAMAccountName: user.sAMAccountName
},
update: user,
create: user
})
logSuccess({
message: `Importado ${index + 1}/${allAdUsers.length} (${
user.sAMAccountName
}) ${user.displayName}`,
data: dbUser
})
}
const endTime = performance.now()
logSuccess({
tags: ['user', 'AD'],
message: `${
allAdUsers.length
} usuários importados do Active Directory em ${(
(endTime - startTime) /
1000
).toFixed(2)}s`
})
return allAdUsers.length
} catch (e) {
logError({
message: `Erro ao importar usuários. ${e.message}`,
tags: ['user', 'AD']
})
throw new Error('Erro ao importar usuários: ' + e)
} finally {
this.working = false
}
}
}
export { User }