428 lines
11 KiB
JavaScript
Executable File
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 }
|