SAML; Dashboard

This commit is contained in:
garrettmills
2020-05-03 20:16:54 -05:00
parent e3ecfb0d37
commit c389e151b5
1778 changed files with 148410 additions and 82 deletions

View File

@@ -10,7 +10,7 @@ class LDAPBase extends Model {
throw new ImplementationError()
}
to_ldap() {
async to_ldap() {
throw new ImplementationError()
}
}

View File

@@ -0,0 +1,39 @@
const { Model } = require('flitter-orm')
class MessageModel extends Model {
static get schema() {
return {
dismissed: {type: Boolean, default: false},
expires: {type: Date, default: () => {
const date = new Date
date.setYear(date.getUTCFullYear() + 1)
return date
}},
display_type: {type: String, default: 'info'},
message: String,
user_id: String,
}
}
static async for_user(user) {
return this.find({ user_id: user.id, dismissed: false, expires: { $gt: new Date } })
}
static async create(user, message, display_type = 'info') {
const msg = new this({
message,
display_type,
user_id: user.id,
})
await msg.save()
return msg
}
async dismiss() {
this.dismissed = true
return this.save()
}
}
module.exports = exports = MessageModel

13
app/models/Session.js Normal file
View File

@@ -0,0 +1,13 @@
const { Model } = require('flitter-orm')
class SessionModel extends Model {
static collection = 'flitter_sessions'
static get schema() {
return {
expires: Date,
session: Object,
}
}
}
module.exports = exports = SessionModel

View File

@@ -0,0 +1,28 @@
const { Model } = require('flitter-orm')
const bcrypt = require('bcrypt')
const uuid = require('uuid/v4')
class AppPasswordModel extends Model {
static get schema() {
return {
hash: String,
created: { type: Date, default: () => new Date },
expires: Date,
active: { type: Boolean, default: true },
name: String,
uuid: { type: String, default: uuid },
}
}
async set_hash(password) {
this.hash = await bcrypt.hash(password, 10)
if ( !this.uuid ) this.uuid = uuid()
return this
}
async verify(password) {
return bcrypt.compare(password, this.hash)
}
}
module.exports = exports = AppPasswordModel

View File

@@ -0,0 +1,24 @@
const { Model } = require('flitter-orm')
const bcrypt = require('bcrypt')
class PasswordResetModel extends Model {
static get schema() {
return {
reset_on: { type: Date, default: () => new Date },
old_hash: String,
hash: String,
reason: { type: String, default: 'user' }, // 'user' | 'lockout'
}
}
async set_hash(password) {
this.hash = await bcrypt.hash(password, 10)
return this
}
async check(password) {
return await bcrypt.compare(password, this.hash)
}
}
module.exports = exports = PasswordResetModel

View File

@@ -3,6 +3,9 @@ const LDAP = require('ldapjs')
const ActiveScope = require('../scopes/ActiveScope')
const MFAToken = require('./MFAToken.model')
const PasswordReset = require('./PasswordReset.model')
const AppPassword = require('./AppPassword.model')
const uuid = require('uuid/v4')
/*
* Auth user model. This inherits fields and methods from the default
@@ -11,7 +14,7 @@ const MFAToken = require('./MFAToken.model')
*/
class User extends AuthUser {
static get services() {
return [...super.services, 'auth', 'ldap_server', 'ldap_dn_format']
return [...super.services, 'auth', 'ldap_server', 'configs', 'models', 'app']
}
static get schema() {
@@ -19,11 +22,15 @@ class User extends AuthUser {
// other schema fields here
first_name: String,
last_name: String,
tagline: String,
email: String,
ldap_visible: {type: Boolean, default: true},
active: {type: Boolean, default: true},
mfa_token: MFAToken,
password_resets: [PasswordReset],
app_passwords: [AppPassword],
mfa_enabled: {type: Boolean, default: false},
mfa_enable_date: Date,
}}
}
@@ -40,6 +47,22 @@ class User extends AuthUser {
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
@@ -50,7 +73,45 @@ class User extends AuthUser {
return this.get_provider().check_user_auth(this, password)
}
to_ldap() {
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 ldap_groups() {
const Group = this.models.get('ldap:Group')
return await Group.find({
$or: [
{ user_ids: this.id },
{ role: { $in: this.roles } },
],
})
}
async to_ldap() {
const ldap_data = {
uid: this.uid,
uuid: this.uuid,
@@ -59,20 +120,27 @@ class User extends AuthUser {
gecos: `${this.first_name} ${this.last_name}`,
mail: this.email,
objectClass: 'inetOrgPerson',
dn: this.dn.format(this.ldap_dn_format),
}
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 ldap_groups = await this.ldap_groups()
if ( ldap_groups.length > 0 ) {
ldap_data.memberOf = ldap_groups.map(x => x.dn.format(this.configs.get('ldap:server.format')))
ldap_data.memberof = ldap_groups.map(x => x.dn.format(this.configs.get('ldap:server.format')))
}
return ldap_data
}
get dn() {
return LDAP.parseDN(`uid=${this.uid},${this.ldap_server.auth_dn().format(this.ldap_dn_format)}`)
return LDAP.parseDN(`uid=${this.uid},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`)
}
}

View File

@@ -0,0 +1,43 @@
const LDAPBase = require('../LDAPBase')
const LDAP = require('ldapjs')
class GroupModel extends LDAPBase {
static get services() {
return [...super.services, 'configs', 'ldap_server', 'models']
}
static get schema() {
return {
role: String,
user_ids: [String],
name: String,
ldap_visible: {type: Boolean, default: true},
}
}
get dn() {
return LDAP.parseDN(`cn=${this.name},${this.ldap_server.group_dn().format(this.configs.get('ldap:server.format'))}`)
}
async users() {
const User = this.models.get('auth:User')
return User.find({
$or: [
{ _id: { $in: this.user_ids.map(x => this.constructor.to_object_id(x)) } },
{ roles: this.role },
],
})
}
async to_ldap() {
const users = await this.users()
return {
cn: this.name,
dn: this.dn.format(this.configs.get('ldap:server.format')),
objectClass: 'groupOfNames',
member: users.map(x => x.dn.format(this.configs.get('ldap:server.format')))
}
}
}
module.exports = exports = GroupModel

View File

@@ -0,0 +1,15 @@
const { Model } = require('flitter-orm')
class ServiceProviderModel extends Model {
static get schema() {
return {
name: String,
entity_id: String,
acs_url: String,
active: { type: Boolean, default: true },
slo_url: String,
}
}
}
module.exports = exports = ServiceProviderModel

View File

@@ -0,0 +1,19 @@
const { Model } = require('flitter-orm')
const uuid = require('uuid/v4')
class SessionParticipantModel extends Model {
static get schema() {
return {
service_provider_id: String,
name_id: String,
name_id_format: { type: String, default: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' },
session_index: Number,
slo_url: String,
sp_cert: String, // TODO figure this out
active: { type: Boolean, default: true },
uuid: { type: String, default: uuid },
}
}
}
module.exports = exports = SessionParticipantModel