SAML; Dashboard
This commit is contained in:
@@ -10,7 +10,7 @@ class LDAPBase extends Model {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
to_ldap() {
|
||||
async to_ldap() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
}
|
||||
|
||||
39
app/models/Message.model.js
Normal file
39
app/models/Message.model.js
Normal 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
13
app/models/Session.js
Normal 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
|
||||
28
app/models/auth/AppPassword.model.js
Normal file
28
app/models/auth/AppPassword.model.js
Normal 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
|
||||
24
app/models/auth/PasswordReset.model.js
Normal file
24
app/models/auth/PasswordReset.model.js
Normal 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
|
||||
@@ -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'))}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
app/models/ldap/Group.model.js
Normal file
43
app/models/ldap/Group.model.js
Normal 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
|
||||
15
app/models/saml/ServiceProvider.model.js
Normal file
15
app/models/saml/ServiceProvider.model.js
Normal 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
|
||||
19
app/models/saml/SessionParticipant.model.js
Normal file
19
app/models/saml/SessionParticipant.model.js
Normal 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
|
||||
Reference in New Issue
Block a user