SAML; Dashboard
This commit is contained in:
115
app/ldap/controllers/Groups.controller.js
Normal file
115
app/ldap/controllers/Groups.controller.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const LDAPController = require('./LDAPController')
|
||||
|
||||
class GroupsController extends LDAPController {
|
||||
static get services() {
|
||||
return [
|
||||
...super.services,
|
||||
'output',
|
||||
'ldap_server',
|
||||
'models',
|
||||
'configs',
|
||||
'auth'
|
||||
]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.Group = this.models.get('ldap:Group')
|
||||
}
|
||||
|
||||
// TODO flitter-orm chunk query
|
||||
// TODO generalize scoped search logic
|
||||
async search_groups(req, res, next) {
|
||||
if ( !req.user.can('ldap:search:groups') ) {
|
||||
return next(new LDAP.InsufficientAccessRightsError())
|
||||
}
|
||||
|
||||
if ( req.scope === 'base' ) {
|
||||
// If scope is base, check if the base DN matches the filter.
|
||||
// If so, return it. Else, return empty.
|
||||
this.output.debug(`Running base DN search for groups with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
|
||||
const group = await this.get_resource_from_dn(req.dn)
|
||||
|
||||
// Make sure the user is ldap visible && match the filter
|
||||
if ( group && group.ldap_visible && req.filter.matches(await group.to_ldap()) ) {
|
||||
|
||||
// If so, send the object
|
||||
res.send({
|
||||
dn: group.dn.format(this.configs.get('ldap:server.format')),
|
||||
attributes: await group.to_ldap(),
|
||||
})
|
||||
}
|
||||
} else if ( req.scope === 'one' ) {
|
||||
// If scope is one, find all entries that are the immediate
|
||||
// subordinates of the base DN that match the filter.
|
||||
this.output.debug(`Running one DN search for groups with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
|
||||
// Fetch the LDAP-visible users
|
||||
const groups = await this.Group.ldap_directory()
|
||||
for ( const group of groups ) {
|
||||
|
||||
// Make sure the user os of the appropriate scope
|
||||
if ( req.dn.equals(group.dn) || group.dn.parent().equals(req.dn) ) {
|
||||
|
||||
// Check if the filter matches
|
||||
if ( req.filter.matches(await group.to_ldap()) ) {
|
||||
|
||||
// If so, send the object
|
||||
res.send({
|
||||
dn: group.dn.format(this.configs.get('ldap:server.format')),
|
||||
attributes: await group.to_ldap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if ( req.scope === 'sub' ) {
|
||||
// If scope is sub, find all entries that are subordinates
|
||||
// of the base DN at any level and match the filter.
|
||||
this.output.debug(`Running sub DN search for groups with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
|
||||
// Fetch the users as LDAP objects
|
||||
const groups = await this.Group.ldap_directory()
|
||||
for ( const group of groups ) {
|
||||
|
||||
// Make sure the user is of appropriate scope
|
||||
if ( req.dn.equals(group.dn) || req.dn.parentOf(group.dn) ) {
|
||||
|
||||
// Check if filter matches
|
||||
if ( req.filter.matches(await group.to_ldap()) ) {
|
||||
|
||||
// If so, send the object
|
||||
res.send({
|
||||
dn: group.dn.format(this.configs.get('ldap:server.format')),
|
||||
attributes: await group.to_ldap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.output.error(`Attempted to perform LDAP search with invalid scope: ${req.scope}`)
|
||||
return next(new LDAP.OtherError('Attempted to perform LDAP search with invalid scope.'))
|
||||
}
|
||||
|
||||
res.end()
|
||||
return next()
|
||||
}
|
||||
|
||||
get_cn_from_dn(dn) {
|
||||
try {
|
||||
if ( typeof dn === 'string' ) dn = LDAP.parseDN(dn)
|
||||
return dn.rdns[0].attrs.cn.value
|
||||
} catch (e) { console.log('Error parsing CN from DN', e) }
|
||||
}
|
||||
|
||||
async get_resource_from_dn(dn) {
|
||||
const cn = this.get_cn_from_dn(dn)
|
||||
console.log({cn, dn})
|
||||
if ( cn ) {
|
||||
return this.Group.findOne({name: cn, ldap_visible: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = GroupsController
|
||||
@@ -9,7 +9,7 @@ class LDAPController extends Injectable {
|
||||
check_attribute = 'ldap_visible' // or false
|
||||
|
||||
static get services() {
|
||||
return [...super.services, 'ldap_server', 'ldap_dn_format']
|
||||
return [...super.services, 'ldap_server', 'configs']
|
||||
}
|
||||
|
||||
async compare(req, res, next) {
|
||||
@@ -24,7 +24,7 @@ class LDAPController extends Injectable {
|
||||
}
|
||||
|
||||
// Make sure it has the requested attribute
|
||||
const value = item.to_ldap()[req.attribute]
|
||||
const value = (await item.to_ldap())[req.attribute]
|
||||
if ( typeof value === 'undefined' ) {
|
||||
return next(new LDAP.NoSuchAttributeError())
|
||||
}
|
||||
@@ -42,26 +42,55 @@ class LDAPController extends Injectable {
|
||||
|
||||
// Make sure the DN is valid
|
||||
if ( !req.dn.childOf(auth_dn) ) {
|
||||
this.output.debug(`Bind failure: ${req.dn} not in ${auth_dn}`)
|
||||
return next(new LDAP.InvalidCredentialsError('Cannot bind to DN outsize of the authentication base.'))
|
||||
}
|
||||
|
||||
// Get the item
|
||||
const item = await this.get_resource_from_dn(req.dn)
|
||||
if ( !item ) {
|
||||
this.output.debug(`Bind failure: ${req.dn} not found`)
|
||||
return next(new LDAP.NoSuchObject())
|
||||
}
|
||||
|
||||
// If the object is can-able, make sure it can bind
|
||||
if ( typeof item.can === 'function' && !item.can('ldap:bind') ) {
|
||||
this.output.debug(`Bind failure: User not allowed to bind`)
|
||||
return next(new LDAP.InsufficientAccessRightsError())
|
||||
}
|
||||
|
||||
// Make sure the password matches the resource record
|
||||
if ( !await item.check_password(req.credentials) ) {
|
||||
// Check if the credentials are an app_password
|
||||
const app_password_verified = Array.isArray(item.app_passwords)
|
||||
&& item.app_passwords.length > 0
|
||||
&& await item.check_app_password(req.credentials)
|
||||
|
||||
// Check if the user has MFA enabled.
|
||||
// If so, split the incoming password to fetch the MFA code
|
||||
// e.g. normalPassword:123456
|
||||
if ( !app_password_verified && item.mfa_enabled ) {
|
||||
const parts = req.credentials.split(':')
|
||||
const mfa_code = parts.pop()
|
||||
const actual_password = parts.join(':')
|
||||
|
||||
// Check the credentials
|
||||
if ( !await item.check_password(actual_password) ) {
|
||||
this.output.debug(`Bind failure: user w/ MFA provided invalid credentials`)
|
||||
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Make sure MFA code is included at the end of your password (e.g. password:123456)'))
|
||||
}
|
||||
|
||||
// Now, check the MFA code
|
||||
if ( !item.mfa_token.verify(mfa_code) ) {
|
||||
this.output.debug(`Bind failure: user w/ MFA provided invalid MFA token`)
|
||||
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Verification of the MFA token failed.'))
|
||||
}
|
||||
|
||||
// If not MFA, just check the credentials
|
||||
} else if (!app_password_verified && !await item.check_password(req.credentials)) {
|
||||
this.output.debug(`Bind failure: user w/ simple auth provided invalid credentials`)
|
||||
return next(new LDAP.InvalidCredentialsError())
|
||||
}
|
||||
|
||||
this.output.success(`Successfully bound resource as DN: ${req.dn.format(this.ldap_dn_format)}.`)
|
||||
this.output.info(`Successfully bound resource as DN: ${req.dn.format(this.configs.get('ldap:server.format'))}.`)
|
||||
res.end()
|
||||
return next()
|
||||
}
|
||||
@@ -76,8 +105,8 @@ class LDAPController extends Injectable {
|
||||
|
||||
// Make sure it's a parent of the request DN
|
||||
if ( !base_dn.parentOf(req.dn) ) {
|
||||
this.output.warn(`Attempted to perform resource deletion on invalid DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||
return next(new LDAP.InsufficientAccessRightsError(`Target DN must be a member of the base DN: ${base_dn.format(this.ldap_dn_format)}.`))
|
||||
this.output.warn(`Attempted to perform resource deletion on invalid DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
return next(new LDAP.InsufficientAccessRightsError(`Target DN must be a member of the base DN: ${base_dn.format(this.configs.get('ldap:server.format'))}.`))
|
||||
}
|
||||
|
||||
// Fetch the resource (error if not found)
|
||||
|
||||
@@ -9,7 +9,7 @@ class UsersController extends LDAPController {
|
||||
'output',
|
||||
'ldap_server',
|
||||
'models',
|
||||
'ldap_dn_format',
|
||||
'configs',
|
||||
'auth'
|
||||
]
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class UsersController extends LDAPController {
|
||||
// make sure the add DN is in the auth_dn
|
||||
const auth_dn = this.ldap_server.auth_dn()
|
||||
if ( !auth_dn.parentOf(req.dn) ) {
|
||||
this.output.warn(`Attempted to perform user insertion on invalid DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||
this.output.warn(`Attempted to perform user insertion on invalid DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
return next(new LDAP.InsufficientAccessRightsError())
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class UsersController extends LDAPController {
|
||||
// Make sure it's under the user auth DN
|
||||
const auth_dn = this.ldap_server.auth_dn()
|
||||
if ( !auth_dn.parentOf(req.dn) ) {
|
||||
this.output.warn(`Attempted to perform user modify on invalid DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||
this.output.warn(`Attempted to perform user modify on invalid DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
return next(new LDAP.InsufficientAccessRightsError())
|
||||
}
|
||||
|
||||
@@ -188,21 +188,31 @@ class UsersController extends LDAPController {
|
||||
if ( req.scope === 'base' ) {
|
||||
// If scope is base, check if the base DN matches the filter.
|
||||
// If so, return it. Else, return empty.
|
||||
this.output.debug(`Running base DN search for users with DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||
this.output.debug(`Running base DN search for users with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
|
||||
const user = await this.get_resource_from_dn(req.dn)
|
||||
|
||||
// Make sure the user is ldap visible && match the filter
|
||||
if ( req.user.ldap_visible && req.filter.matches(req.user.to_ldap()) ) {
|
||||
if ( user && user.ldap_visible && req.filter.matches(await user.to_ldap()) ) {
|
||||
|
||||
// If so, send the object
|
||||
res.send({
|
||||
dn: req.user.dn.format(this.ldap_dn_format),
|
||||
attributes: req.user.to_ldap(),
|
||||
dn: user.dn,//.format(this.configs.get('ldap:server.format')),
|
||||
attributes: await user.to_ldap(),
|
||||
})
|
||||
|
||||
this.output.debug({
|
||||
dn: user.dn.format(this.configs.get('ldap:server.format')),
|
||||
attributes: await user.to_ldap(),
|
||||
})
|
||||
} else {
|
||||
this.output.debug(`User base search failed: either user not found, not visible, or filter mismatch`)
|
||||
global.ireq = req
|
||||
}
|
||||
} else if ( req.scope === 'one' ) {
|
||||
// If scope is one, find all entries that are the immediate
|
||||
// subordinates of the base DN that match the filter.
|
||||
this.output.debug(`Running one DN search for users with DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||
this.output.debug(`Running one DN search for users with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
|
||||
// Fetch the LDAP-visible users
|
||||
const users = await this.User.ldap_directory()
|
||||
@@ -212,12 +222,12 @@ class UsersController extends LDAPController {
|
||||
if ( req.dn.equals(user.dn) || user.dn.parent().equals(req.dn) ) {
|
||||
|
||||
// Check if the filter matches
|
||||
if ( req.filter.matches(user.to_ldap()) ) {
|
||||
if ( req.filter.matches(await user.to_ldap()) ) {
|
||||
|
||||
// If so, send the object
|
||||
res.send({
|
||||
dn: user.dn.format(this.ldap_dn_format),
|
||||
attributes: user.to_ldap(),
|
||||
dn: user.dn,//.format(this.configs.get('ldap:server.format')),
|
||||
attributes: await user.to_ldap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -226,7 +236,7 @@ class UsersController extends LDAPController {
|
||||
} else if ( req.scope === 'sub' ) {
|
||||
// If scope is sub, find all entries that are subordinates
|
||||
// of the base DN at any level and match the filter.
|
||||
this.output.debug(`Running sub DN search for users with DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||
this.output.debug(`Running sub DN search for users with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
|
||||
// Fetch the users as LDAP objects
|
||||
const users = await this.User.ldap_directory()
|
||||
@@ -236,12 +246,12 @@ class UsersController extends LDAPController {
|
||||
if ( req.dn.equals(user.dn) || req.dn.parentOf(user.dn) ) {
|
||||
|
||||
// Check if filter matches
|
||||
if ( req.filter.matches(user.to_ldap()) ) {
|
||||
if ( req.filter.matches(await user.to_ldap()) ) {
|
||||
|
||||
// If so, send the object
|
||||
res.send({
|
||||
dn: user.dn.format(this.ldap_dn_format),
|
||||
attributes: user.to_ldap(),
|
||||
dn: user.dn,//.format(this.configs.get('ldap:server.format')),
|
||||
attributes: await user.to_ldap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ const LDAPMiddleware = require('./LDAPMiddleware')
|
||||
|
||||
class LDAPLoggerMiddleware extends LDAPMiddleware {
|
||||
static get services() {
|
||||
return [...super.services, 'app', 'output', 'ldap_dn_format']
|
||||
return [...super.services, 'app', 'output', 'configs']
|
||||
}
|
||||
|
||||
async test(req, res, next) {
|
||||
let bind_dn = req.connection.ldap.bindDN
|
||||
this.output.info(`${req.json.protocolOp} - as ${bind_dn ? bind_dn.format(this.ldap_dn_format) : 'N/A'} - target ${req.dn.format(this.ldap_dn_format)}`)
|
||||
this.output.info(`${req.json.protocolOp} - as ${bind_dn ? bind_dn.format(this.configs.get('ldap:server.format')) : 'N/A'} - target ${req.dn.format(this.configs.get('ldap:server.format'))}`)
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
50
app/ldap/routes/groups.routes.js
Normal file
50
app/ldap/routes/groups.routes.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const groups_routes = {
|
||||
|
||||
prefix: false, // false | string
|
||||
|
||||
middleware: [
|
||||
'Logger'
|
||||
],
|
||||
|
||||
search: {
|
||||
'ou=groups': [
|
||||
'ldap_middleware::BindUser',
|
||||
'ldap_controller::Groups.search_groups',
|
||||
],
|
||||
},
|
||||
|
||||
/*bind: {
|
||||
'ou=groups': ['ldap_controller::Users.bind'],
|
||||
},*/
|
||||
|
||||
/*add: {
|
||||
'ou=groups': [
|
||||
'ldap_middleware::BindUser',
|
||||
'ldap_controller::Groups.add_group',
|
||||
],
|
||||
},
|
||||
|
||||
del: {
|
||||
'ou=people': [
|
||||
'ldap_middleware::BindUser',
|
||||
'ldap_controller::Users.delete',
|
||||
],
|
||||
},
|
||||
|
||||
modify: {
|
||||
'ou=people': [
|
||||
'ldap_middleware::BindUser',
|
||||
'ldap_controller::Users.modify_people',
|
||||
],
|
||||
},
|
||||
|
||||
compare: {
|
||||
'ou=people': [
|
||||
'ldap_middleware::BindUser',
|
||||
'ldap_controller::Users.compare',
|
||||
],
|
||||
},*/
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = groups_routes
|
||||
Reference in New Issue
Block a user