const AuthUser = require('flitter-auth/model/User') const LDAP = require('ldapjs') const ActiveScope = require('../scopes/ActiveScope') const MFAToken = require('./MFAToken.model') const PasswordReset = require('./PasswordReset.model') const AppAuthorization = require('./AppAuthorization.model') const AppPassword = require('./AppPassword.model') const uuid = require('uuid').v4 const NotifyConfig = require('../system/NotifyConfig.model') /* * Auth user model. This inherits fields and methods from the default * flitter-auth/model/User model, however you can override methods and * properties here as you need. */ class User extends AuthUser { static get services() { return [...super.services, 'auth', 'ldap_server', 'configs', 'models', 'app'] } static get schema() { return {...super.schema, ...{ // other schema fields here first_name: String, last_name: String, tagline: String, email: String, email_verified: {type: Boolean, default: false}, ldap_visible: {type: Boolean, default: true}, active: {type: Boolean, default: true}, mfa_token: MFAToken, password_resets: [PasswordReset], app_passwords: [AppPassword], app_authorizations: [AppAuthorization], mfa_enabled: {type: Boolean, default: false}, mfa_enable_date: Date, create_date: {type: Date, default: () => new Date}, photo_file_id: String, trap: String, notify_config: NotifyConfig, uid_number: Number, login_shell: String, is_default_user_for_coreid: { type: Boolean, default: false }, }} } async grant_defaults() { const default_user = await this.constructor.findOne({is_default_user_for_coreid: true, active: true}) this.login_shell = default_user.login_shell this.roles = default_user.roles this.permissions = default_user.permissions const groups = await default_user.groups() for ( const group of groups ) { group.user_ids.push(this.id) await group.save() } } async get_uid_number() { if ( !this.uid_number ) { const Setting = this.models.get('Setting') let last_uid = await Setting.get('ldap.last_alloc_uid') if ( last_uid < 1 ) { last_uid = this.configs.get('ldap:server.schema.start_uid') } this.uid_number = last_uid + 1 await Setting.set('ldap.last_alloc_uid', this.uid_number) await this.save() } return this.uid_number } async photo() { const File = this.models.get('upload::File') return File.findById(this.photo_file_id) } has_authorized(client) { return this.app_authorizations.some(x => x.client_id === client.id) } get_authorization(client) { for ( const auth of this.app_authorizations ) { if ( auth.client_id === client.id ) return auth } } authorize(client) { if ( !this.has_authorized(client) ) { const client_rec = new AppAuthorization({ client_id: client.id, api_scopes: client.api_scopes, }, this) this.app_authorizations.push(client_rec) } else { const client_rec = this.get_authorization(client) client_rec.api_scopes = client.api_scopes } } async to_api() { return { id: this.id, uid: this.uid, first_name: this.first_name, last_name: this.last_name, name: `${this.first_name} ${this.last_name}`, email: this.email, tagline: this.tagline, trap: this.trap, group_ids: (await this.groups()).map(x => x.id), profile_photo: `${this.configs.get('app.url')}api/v1/auth/users/${this.uid}/photo`, } } static scopes = [ new ActiveScope({}) ] static async ldap_directory() { return this.find({ldap_visible: true}) } // TODO just in case we need this later get can_login() { return true } async sessions() { const Session = require('../Session') this.app.di().inject(Session) return Session.find({ 'session.auth.user_id': this.id }) } async kickout() { // TODO handle SAML session participants const sessions = await this.sessions() for ( const session of sessions ) { delete session.session.auth delete session.session.mfa_remember await session.save() } } // Prefer soft delete because of the active scope async delete() { this.active = false await this.save() } async check_password(password) { return this.get_provider().check_user_auth(this, password) } async check_app_password(password) { for ( const pw of this.app_passwords ) { if ( await pw.verify(password) ) { pw.accessed = new Date await pw.save() return true } } return false } async reset_password(new_password, reason = 'user') { const reset = new PasswordReset({ reason, old_hash: this.password, }, this) await reset.set_hash(new_password) this.password = reset.hash this.password_resets.push(reset) return reset } async app_password(name) { const gen = uuid().replace(/-/g, '') const pw = new AppPassword({ name }, this) await pw.set_hash(gen) this.app_passwords.push(pw) return { password: gen, record: pw } } async groups() { const Group = this.models.get('auth:Group') return Group.find({ active: true, user_ids: this.id }) } async oidc_sessions() { const Session = this.models.get('openid:Session') return Session.find({ 'payload.account': this.id }) } async logout(request) { for ( const session of (await this.oidc_sessions()) ) { await session.delete() } this.get_provider().logout(request) } async has_sudo() { const groups = await this.groups() return groups.some(group => group.grants_sudo) } async to_sudo(iam_targets = []) { const Policy = this.models.get('iam:Policy') const granted = [] for ( const target of iam_targets ) { if ( await Policy.check_user_access(this, target, 'sudo') ) { granted.push(target) } } return { objectClass: ['sudoRole'], cn: `sudo_${this.uid.toLowerCase()}`, sudoUser: this.uid.toLowerCase(), ...(granted.length ? { iamtarget: granted, sudoHost: 'ALL', sudoRunAs: 'ALL', sudoCommand: 'ALL', } : {}) } } async to_ldap(iam_targets = []) { const Policy = this.models.get('iam:Policy') const uid_number = await this.get_uid_number() const shell = this.login_shell || this.configs.get('ldap:server.schema.default_shell') const domain = this.configs.get('ldap:server.schema.base_dc').split(',').map(x => x.replace('dc=', '')).join('.') const group_ids = [] for ( const group of await this.groups() ) { group_ids.push(await group.get_gid_number()) } const ldap_data = { uid: this.uid.toLowerCase(), uuid: this.uuid, cn: this.first_name, sn: this.last_name, gecos: `${this.first_name} ${this.last_name}`, mail: this.email, objectClass: ['inetOrgPerson', 'person', 'posixaccount'], objectclass: ['inetOrgPerson', 'person', 'posixaccount'], entryuuid: this.uuid, entryUUID: this.uuid, objectGuid: this.uuid, objectguid: this.uuid, uidNumber: uid_number, gidNumber: String(await this.get_uid_number()), // group_ids.map(x => String(x)), loginShell: shell, homeDirectory: `/home/${this.uid}@${domain}` } if ( this.tagline ) ldap_data.extras_tagline = this.tagline const addl_data = JSON.parse(this.data) for ( const key in addl_data ) { if ( !addl_data.hasOwnProperty(key) || !key.startsWith('ldap_') ) continue ldap_data[`data${key.substr(4)}`] = `${addl_data[key]}` } const groups = await this.groups() if ( groups.length > 0 ) { const group_data = groups.map(x => x.dn.format(this.configs.get('ldap:server.format'))) ldap_data.memberOf = group_data ldap_data.memberof = group_data } const iamtarget = [] for ( const target_id of iam_targets ) { if ( await Policy.check_user_access(this, target_id) ) { iamtarget.push(target_id) } } ldap_data.iamtarget = iamtarget return ldap_data } get dn() { return LDAP.parseDN(`uid=${this.uid.toLowerCase()},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`) } get sudo_dn() { return LDAP.parseDN(`cn=sudo_${this.uid.toLowerCase()},${this.ldap_server.sudo_dn().format(this.configs.get('ldap:server.format'))}`) } // The following are used by OpenID connect async claims(use, scope) { return { sub: this.id, email: this.email, email_verified: true, // TODO family_name: this.last_name, given_name: this.first_name, locale: 'en_US', // TODO name: `${this.first_name} ${this.last_name}`, preferred_username: this.uid.toLowerCase(), username: this.uid.toLowerCase(), } } static async findByLogin(login) { return this.findOne({ active: true, uid: login.toLowerCase(), }) } static async findAccount(ctx, id, token) { return this.findById(id) } get accountId() { return this.id } } module.exports = exports = User