CoreID/app/models/auth/User.model.js
garrettmills 62c818dc8d
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
Add ability to require e-mail verification
2021-05-04 11:28:55 -05:00

332 lines
10 KiB
JavaScript

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,
email: this.email,
tagline: this.tagline,
trap: this.trap,
group_ids: (await this.groups()).map(x => x.id),
}
}
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) ) 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