Flesh out users OU (works with Gitea simple LDAP now!!)
This commit is contained in:
parent
68cc90899c
commit
175c335542
1
.gitignore
vendored
1
.gitignore
vendored
@ -143,3 +143,4 @@ fabric.properties
|
|||||||
# Android studio 3.1+ serialized cache file
|
# Android studio 3.1+ serialized cache file
|
||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
test*
|
||||||
|
@ -1,7 +1,105 @@
|
|||||||
const { Injectable } = require('flitter-di')
|
const { Injectable } = require('flitter-di')
|
||||||
|
const { ImplementationError } = require('libflitter')
|
||||||
|
const LDAP = require('ldapjs')
|
||||||
|
|
||||||
class LDAPController extends Injectable {
|
class LDAPController extends Injectable {
|
||||||
|
resource_type = 'items'
|
||||||
|
|
||||||
|
// TODO make use of this better
|
||||||
|
check_attribute = 'ldap_visible' // or false
|
||||||
|
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'ldap_server', 'ldap_dn_format']
|
||||||
|
}
|
||||||
|
|
||||||
|
async compare(req, res, next) {
|
||||||
|
if ( !req.user.can(`ldap:compare:${this.resource_type}`)) {
|
||||||
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the resource exists
|
||||||
|
const item = await this.get_resource_from_dn(req.dn)
|
||||||
|
if ( !item ) {
|
||||||
|
return next(LDAP.NoSuchObjectError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure it has the requested attribute
|
||||||
|
const value = item.to_ldap()[req.attribute]
|
||||||
|
if ( typeof value === 'undefined' ) {
|
||||||
|
return next(new LDAP.NoSuchAttributeError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it matches the request value
|
||||||
|
const values = Array.isArray(value) ? value : [value]
|
||||||
|
const matches = values.some(x => x === req.value)
|
||||||
|
|
||||||
|
res.end(matches)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
async bind(req, res, next) {
|
||||||
|
const auth_dn = this.ldap_server.auth_dn()
|
||||||
|
|
||||||
|
// Make sure the DN is valid
|
||||||
|
if ( !req.dn.childOf(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 ) {
|
||||||
|
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') ) {
|
||||||
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the password matches the resource record
|
||||||
|
if ( !await item.check_password(req.credentials) ) {
|
||||||
|
return next(new LDAP.InvalidCredentialsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.output.success(`Successfully bound resource as DN: ${req.dn.format(this.ldap_dn_format)}.`)
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(req, res, next) {
|
||||||
|
if ( !req.user.can(`ldap:delete:${this.resource_type}`) ) {
|
||||||
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the base DN
|
||||||
|
const base_dn = await this.get_base_dn()
|
||||||
|
|
||||||
|
// 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)}.`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the resource (error if not found)
|
||||||
|
const item = await this.get_resource_from_dn(req.dn)
|
||||||
|
if ( !item ) {
|
||||||
|
return next(new LDAP.NoSuchObjectError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete it - TODO full soft delete, or just ldap_visible = false?
|
||||||
|
await item.delete()
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_resource_from_dn(dn) {
|
||||||
|
throw new ImplementationError()
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_base_dn() {
|
||||||
|
throw new ImplementationError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = LDAPController
|
module.exports = exports = LDAPController
|
||||||
|
@ -1,41 +1,258 @@
|
|||||||
const LDAPController = require('./LDAPController')
|
const LDAPController = require('./LDAPController')
|
||||||
const LDAP = require('ldapjs')
|
const LDAP = require('ldapjs')
|
||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
|
||||||
class UsersController extends LDAPController {
|
class UsersController extends LDAPController {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'output', 'ldap_server', 'models']
|
return [
|
||||||
|
...super.services,
|
||||||
|
'output',
|
||||||
|
'ldap_server',
|
||||||
|
'models',
|
||||||
|
'ldap_dn_format',
|
||||||
|
'auth'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
async search_people(req, res, next) {
|
constructor() {
|
||||||
global.ireq = req
|
super()
|
||||||
|
this.User = this.models.get('auth:User')
|
||||||
}
|
}
|
||||||
|
|
||||||
async bind(req, res, next) {
|
// Might need to override compare to support special handling for userPassword
|
||||||
const auth_dn = this.ldap_server.auth_dn()
|
|
||||||
|
|
||||||
// Make sure the DN is valid
|
// TODO generalize some of the addition logic
|
||||||
if ( !req.dn.childOf(auth_dn) ) {
|
async add_people(req, res, next) {
|
||||||
return next(new LDAP.InvalidCredentialsError())
|
if ( !req.user.can('ldap:add:users') ) {
|
||||||
}
|
|
||||||
|
|
||||||
// Get the user
|
|
||||||
const user = await this.get_user_from_dn(req.dn)
|
|
||||||
if ( !user ) {
|
|
||||||
return next(new LDAP.InvalidCredentialsError())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the password matches the user record
|
|
||||||
if ( !await user.check_password(req.credentials) ) {
|
|
||||||
return next(new LDAP.InvalidCredentialsError())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the user has permission to bind
|
|
||||||
if ( !user.can('ldap:bind') ) {
|
|
||||||
return next(new LDAP.InsufficientAccessRightsError())
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
}
|
}
|
||||||
|
|
||||||
this.output.success(`Successfully bound user ${user.uid} as DN: ${req.dn.format({skipSpace: true})}.`)
|
// make sure the add DN is in the auth_dn
|
||||||
return res.end()
|
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)}`)
|
||||||
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the user object doesn't already exist
|
||||||
|
const existing_user = await this.get_resource_from_dn(req.dn)
|
||||||
|
if ( existing_user ) {
|
||||||
|
return next(new LDAP.EntryAlreadyExistsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the user object from the request attributes
|
||||||
|
const req_data = req.toObject().attributes
|
||||||
|
const register_data = {
|
||||||
|
first_name: req_data.cn ? req_data.cn[0] : '',
|
||||||
|
last_name: req_data.sn ? req_data.sn[0] : '',
|
||||||
|
email: req_data.mail ? req_data.mail[0] : '',
|
||||||
|
username: req_data.uid ? req_data.uid[0] : '',
|
||||||
|
password: req_data.userpassword ? req_data.userpassword[0] : '',
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add data fields
|
||||||
|
|
||||||
|
// Make sure the data uid matches the request DN
|
||||||
|
if ( this.get_uid_from_dn(req.dn) !== register_data.username ) {
|
||||||
|
this.output.error(`Attempted to register user where request DN's UID differs from the registration data: (${this.get_uid_from_dn(req.dn)} !== ${register_data.username})`)
|
||||||
|
return next(new LDAP.ObjectclassViolationError(`Attempted to register user where request DN's UID differs from the registration data!`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the auth provider!
|
||||||
|
const flitter = this.auth.get_provider('flitter')
|
||||||
|
|
||||||
|
// Make sure required fields are provided
|
||||||
|
const reg_errors = await flitter.validate_registration(register_data)
|
||||||
|
const other_required_fields = ['first_name', 'last_name', 'email']
|
||||||
|
for ( const field of other_required_fields ) {
|
||||||
|
if ( !register_data[field] ) reg_errors.push(`Missing field: ${field}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( reg_errors.length > 0 ) {
|
||||||
|
this.output.error(`Error(s) encountered during LDAP user registration: ${reg_errors.join('; ')}`)
|
||||||
|
return next(new LDAP.ObjectclassViolationError(`Object add validation failure: ${reg_errors.join('; ')}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the user object
|
||||||
|
const registration_args = await flitter.get_registration_args(register_data)
|
||||||
|
delete register_data.username
|
||||||
|
delete register_data.password
|
||||||
|
registration_args[1] = {...register_data, ...registration_args[1]}
|
||||||
|
|
||||||
|
const new_user = await flitter.register(...registration_args)
|
||||||
|
this.output.success(`Created new LDAP user: ${new_user.uid}`)
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO generalize some of the modification logic
|
||||||
|
async modify_people(req, res, next) {
|
||||||
|
if ( !req.user.can('ldap:modify:users') ) {
|
||||||
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( req.changes.length <= 0 ) {
|
||||||
|
return next(new LDAP.ProtocolError('Must specify at least one change.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)}`)
|
||||||
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the target user
|
||||||
|
const user = await this.get_resource_from_dn(req.dn)
|
||||||
|
if ( !user ) {
|
||||||
|
return next(new LDAP.NoSuchObjectError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the changes
|
||||||
|
const allow_replace = ['cn', 'sn', 'mail', 'userPassword']
|
||||||
|
for ( const change of req.changes ) {
|
||||||
|
const change_data = change.json.modification
|
||||||
|
if ( change.operation === 'add' ) {
|
||||||
|
if ( !change_data.type.startsWith('data_') ) {
|
||||||
|
return next(new LDAP.OperationsError(`Addition of non 'data_' fields is not permitted.`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `ldap${change_data.type.substr(4)}`
|
||||||
|
const val = change_data.vals
|
||||||
|
|
||||||
|
if ( typeof user.data_get(key) !== 'undefined' ) {
|
||||||
|
return next(new LDAP.AttributeOrValueExistsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
user.data_set(key, val)
|
||||||
|
} else if ( change.operation === 'replace' ) {
|
||||||
|
if ( !allow_replace.includes(change_data.type) || change_data.type.startsWith('data_') ) {
|
||||||
|
return next(new LDAP.OperationsError(`Modification of the ${change_data.type} field is not permitted.`))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( change_data.type.startsWith('data_') ) {
|
||||||
|
const key = `ldap${change_data.type.substr(4)}`
|
||||||
|
const val = change_data.vals
|
||||||
|
user.data_set(key, val)
|
||||||
|
} else if ( change_data.type === 'cn' ) {
|
||||||
|
user.first_name = change_data.vals[0]
|
||||||
|
} else if ( change_data.type === 'sn' ) {
|
||||||
|
user.last_name = change_data.vals[0]
|
||||||
|
} else if ( change_data.type === 'mail' ) {
|
||||||
|
// Validate the e-mail address
|
||||||
|
if ( !this.ldap_server.validate_email(change_data.vals[0]) ) {
|
||||||
|
return next(new LDAP.OperationsError(`Unable to make modification: mail must be a valid e-mail address.`))
|
||||||
|
}
|
||||||
|
|
||||||
|
user.email = change_data.vals[0]
|
||||||
|
} else if ( change_data.type === 'userPassword' ) {
|
||||||
|
// Validate the password
|
||||||
|
if ( change_data.vals[0].length < 8 ) {
|
||||||
|
return next(new LDAP.OperationsError(`Unable to make modification: userPassword must be at least 8 characters.`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the password before update
|
||||||
|
user.password = await bcrypt.hash(change_data.vals[0], 10)
|
||||||
|
}
|
||||||
|
} else if ( change.operation === 'delete' ) {
|
||||||
|
if ( !change_data.type.startsWith('data_') ) {
|
||||||
|
return next(new LDAP.OperationsError(`Deletion of non 'data_' fields is not permitted.`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `ldap${change_data.type.substr(4)}`
|
||||||
|
const json_obj = JSON.parse(user.data)
|
||||||
|
delete json_obj[key]
|
||||||
|
user.data = JSON.stringify(json_obj)
|
||||||
|
} else {
|
||||||
|
return next(new LDAP.ProtocolError(`Invalid/unknown modify operation: ${change.operation}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.save()
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_base_dn() {
|
||||||
|
return this.ldap_server.auth_dn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO flitter-orm chunk query
|
||||||
|
// TODO generalize scoped search logic
|
||||||
|
async search_people(req, res, next) {
|
||||||
|
if ( !req.user.can('ldap:search:users') ) {
|
||||||
|
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 users with DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||||
|
|
||||||
|
// Make sure the user is ldap visible && match the filter
|
||||||
|
if ( req.user.ldap_visible && req.filter.matches(req.user.to_ldap()) ) {
|
||||||
|
|
||||||
|
// If so, send the object
|
||||||
|
res.send({
|
||||||
|
dn: req.user.dn.format(this.ldap_dn_format),
|
||||||
|
attributes: req.user.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 users with DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||||
|
|
||||||
|
// Fetch the LDAP-visible users
|
||||||
|
const users = await this.User.ldap_directory()
|
||||||
|
for ( const user of users ) {
|
||||||
|
|
||||||
|
// Make sure the user os of the appropriate scope
|
||||||
|
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 so, send the object
|
||||||
|
res.send({
|
||||||
|
dn: user.dn.format(this.ldap_dn_format),
|
||||||
|
attributes: user.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 users with DN: ${req.dn.format(this.ldap_dn_format)}`)
|
||||||
|
|
||||||
|
// Fetch the users as LDAP objects
|
||||||
|
const users = await this.User.ldap_directory()
|
||||||
|
for ( const user of users ) {
|
||||||
|
|
||||||
|
// Make sure the user is of appropriate scope
|
||||||
|
if ( req.dn.equals(user.dn) || req.dn.parentOf(user.dn) ) {
|
||||||
|
|
||||||
|
// Check if filter matches
|
||||||
|
if ( req.filter.matches(user.to_ldap()) ) {
|
||||||
|
|
||||||
|
// If so, send the object
|
||||||
|
res.send({
|
||||||
|
dn: user.dn.format(this.ldap_dn_format),
|
||||||
|
attributes: user.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_uid_from_dn(dn) {
|
get_uid_from_dn(dn) {
|
||||||
@ -47,11 +264,11 @@ class UsersController extends LDAPController {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async get_user_from_dn(dn) {
|
async get_resource_from_dn(dn) {
|
||||||
const uid = this.get_uid_from_dn(dn)
|
const uid = this.get_uid_from_dn(dn)
|
||||||
if ( uid ) {
|
if ( uid ) {
|
||||||
const User = this.models.get('auth:User')
|
const User = this.models.get('auth:User')
|
||||||
return User.findOne({uid})
|
return User.findOne({uid, ldap_visible: true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ class BindUserMiddleware extends LDAPMiddleware {
|
|||||||
return next(new LDAP.InsufficientAccessRightsError())
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = this.user_controller().get_uid_from_dn(bind_dn)
|
const user = await this.user_controller().get_resource_from_dn(bind_dn)
|
||||||
if ( !user || !user.can('ldap:bind') ) {
|
if ( !user || !user.can('ldap:bind') ) {
|
||||||
return next(new LDAP.InvalidCredentialsError())
|
return next(new LDAP.InvalidCredentialsError())
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,12 @@ const LDAPMiddleware = require('./LDAPMiddleware')
|
|||||||
|
|
||||||
class LDAPLoggerMiddleware extends LDAPMiddleware {
|
class LDAPLoggerMiddleware extends LDAPMiddleware {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'app', 'output']
|
return [...super.services, 'app', 'output', 'ldap_dn_format']
|
||||||
}
|
}
|
||||||
|
|
||||||
async test(req, res, next) {
|
async test(req, res, next) {
|
||||||
let bind_dn = req.connection.ldap.bindDN
|
let bind_dn = req.connection.ldap.bindDN
|
||||||
this.output.info(`${req.json.protocolOp} - as ${bind_dn ? bind_dn.format({skipSpace: true}) : 'N/A'} - target ${req.dn.format({skipSpace: true})}`)
|
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)}`)
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,31 @@ const users_routes = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
add: {
|
add: {
|
||||||
|
'ou=people': [
|
||||||
|
'ldap_middleware::BindUser',
|
||||||
|
'ldap_controller::Users.add_people',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
del: {
|
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',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,15 @@ const { Model } = require('flitter-orm')
|
|||||||
const ImplementationError = require('libflitter/errors/ImplementationError')
|
const ImplementationError = require('libflitter/errors/ImplementationError')
|
||||||
|
|
||||||
class LDAPBase extends Model {
|
class LDAPBase extends Model {
|
||||||
toLDAP() {
|
static async ldap_directory() {
|
||||||
|
return this.find({ldap_visible: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
get dn() {
|
||||||
|
throw new ImplementationError()
|
||||||
|
}
|
||||||
|
|
||||||
|
to_ldap() {
|
||||||
throw new ImplementationError()
|
throw new ImplementationError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
const AuthUser = require('flitter-auth/model/User')
|
const AuthUser = require('flitter-auth/model/User')
|
||||||
|
const LDAP = require('ldapjs')
|
||||||
|
|
||||||
|
const ActiveScope = require('../scopes/ActiveScope')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Auth user model. This inherits fields and methods from the default
|
* Auth user model. This inherits fields and methods from the default
|
||||||
@ -7,21 +10,61 @@ const AuthUser = require('flitter-auth/model/User')
|
|||||||
*/
|
*/
|
||||||
class User extends AuthUser {
|
class User extends AuthUser {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'auth']
|
return [...super.services, 'auth', 'ldap_server', 'ldap_dn_format']
|
||||||
}
|
}
|
||||||
|
|
||||||
static get schema() {
|
static get schema() {
|
||||||
return {...super.schema, ...{
|
return {...super.schema, ...{
|
||||||
// other schema fields here
|
// other schema fields here
|
||||||
|
first_name: String,
|
||||||
|
last_name: String,
|
||||||
|
email: String,
|
||||||
|
ldap_visible: {type: Boolean, default: true},
|
||||||
|
active: {type: Boolean, default: true},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static scopes = [
|
||||||
|
new ActiveScope({})
|
||||||
|
]
|
||||||
|
|
||||||
|
static async ldap_directory() {
|
||||||
|
return this.find({ldap_visible: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer soft delete because of the active scope
|
||||||
|
async delete() {
|
||||||
|
this.active = false
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
async check_password(password) {
|
async check_password(password) {
|
||||||
return this.get_provider().check_user_auth(this, password)
|
return this.get_provider().check_user_auth(this, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
get_provider() {
|
to_ldap() {
|
||||||
return this.auth.get_provider(this.provider)
|
const ldap_data = {
|
||||||
|
uid: this.uid,
|
||||||
|
uuid: this.uuid,
|
||||||
|
cn: this.first_name,
|
||||||
|
sn: this.last_name,
|
||||||
|
gecos: `${this.first_name} ${this.last_name}`,
|
||||||
|
mail: this.email,
|
||||||
|
objectClass: 'inetOrgPerson',
|
||||||
|
dn: this.dn.format(this.ldap_dn_format),
|
||||||
|
}
|
||||||
|
|
||||||
|
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]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ldap_data
|
||||||
|
}
|
||||||
|
|
||||||
|
get dn() {
|
||||||
|
return LDAP.parseDN(`uid=${this.uid},${this.ldap_server.auth_dn().format(this.ldap_dn_format)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
13
app/models/scopes/ActiveScope.js
Normal file
13
app/models/scopes/ActiveScope.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const { Scope } = require('flitter-orm')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flitter-orm scope that enables soft-deletion by an active key.
|
||||||
|
* @extends {module:flitter-orm/src/model/Scope~Scope}
|
||||||
|
*/
|
||||||
|
class ActiveScope extends Scope {
|
||||||
|
async filter(to_filter) {
|
||||||
|
return to_filter.equal('active', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = ActiveScope
|
@ -92,6 +92,8 @@ class LDAPRoutingUnit extends CanonicalUnit {
|
|||||||
this.ldap_server.server[type]([route_prefix, suffix].join(','), ...route_functions)
|
this.ldap_server.server[type]([route_prefix, suffix].join(','), ...route_functions)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Unbind has a default handler, so don't warn about that.
|
||||||
|
if ( type !== 'unbind' )
|
||||||
this.output.warn(`Missing or invalid LDAP protocol definition ${type} in router ${name}. The protocol will be skipped.`)
|
this.output.warn(`Missing or invalid LDAP protocol definition ${type} in router ${name}. The protocol will be skipped.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,22 +10,64 @@ class LDAPServerUnit extends Unit {
|
|||||||
return [...super.services, 'configs', 'express', 'output']
|
return [...super.services, 'configs', 'express', 'output']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the standard format for LDAP DNs. Can be passed into
|
||||||
|
* ldapjs/DN.format().
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
standard_format() {
|
||||||
|
return {
|
||||||
|
skipSpace: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the LDAP.js DN for the user auth base.
|
||||||
|
* @returns {ldap/DN}
|
||||||
|
*/
|
||||||
auth_dn() {
|
auth_dn() {
|
||||||
return this.build_dn(this.config.schema.authentication_base)
|
return this.build_dn(this.config.schema.authentication_base)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the anonymous DN.
|
||||||
|
* @returns {ldap/DN}
|
||||||
|
*/
|
||||||
anonymous() {
|
anonymous() {
|
||||||
return LDAP.parseDN('cn=anonymous')
|
return LDAP.parseDN('cn=anonymous')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the string is a valid e-mail address.
|
||||||
|
*
|
||||||
|
* @see https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
|
||||||
|
* @param {string} email
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
validate_email(email) {
|
||||||
|
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
return re.test(String(email).toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an LDAP.js DN object from a set of string RDNs.
|
||||||
|
* @param {...string} parts
|
||||||
|
* @returns {ldap/DN}
|
||||||
|
*/
|
||||||
build_dn(...parts) {
|
build_dn(...parts) {
|
||||||
parts = parts.flat()
|
parts = parts.flat()
|
||||||
parts.push(this.config.schema.base_dc)
|
parts.push(this.config.schema.base_dc)
|
||||||
return LDAP.parseDN(parts.join(','))
|
return LDAP.parseDN(parts.join(','))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the LDAP server.
|
||||||
|
* @param {module:libflitter/app/FlitterApp~FlitterApp} app
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async go(app) {
|
async go(app) {
|
||||||
this.config = this.configs.get('ldap:server')
|
this.config = this.configs.get('ldap:server')
|
||||||
|
this.app.di().container.register_singleton('ldap_dn_format', this.standard_format())
|
||||||
const server_config = {}
|
const server_config = {}
|
||||||
|
|
||||||
// If Flitter is configured to use an SSL certificate,
|
// If Flitter is configured to use an SSL certificate,
|
||||||
@ -51,6 +93,11 @@ class LDAPServerUnit extends Unit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the LDAP server.
|
||||||
|
* @param {module:libflitter/app/FlitterApp~FlitterApp} app
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async cleanup(app) {
|
async cleanup(app) {
|
||||||
this.server.close()
|
this.server.close()
|
||||||
}
|
}
|
||||||
|
@ -169,6 +169,7 @@ const auth_config = {
|
|||||||
// Roles can be defined here as arrays of permissions:
|
// Roles can be defined here as arrays of permissions:
|
||||||
// 'role_name': [ 'permission1', 'permission2' ],
|
// 'role_name': [ 'permission1', 'permission2' ],
|
||||||
// Then, users with that role will automatically inherit the permissions.
|
// Then, users with that role will automatically inherit the permissions.
|
||||||
|
ldap_admin: ['ldap'],
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
5
flitter
5
flitter
@ -16,7 +16,8 @@ const units = require('./Units.flitter')
|
|||||||
*/
|
*/
|
||||||
units.App = CliAppUnit
|
units.App = CliAppUnit
|
||||||
|
|
||||||
const FlitterApp = require('libflitter/app/FlitterApp')
|
const { FlitterApp, RunLevelErrorHandler } = require('libflitter')
|
||||||
const flitter = new FlitterApp(units)
|
const flitter = new FlitterApp(units)
|
||||||
|
const rleh = new RunLevelErrorHandler()
|
||||||
|
|
||||||
flitter.run()
|
flitter.run().catch(rleh.handle)
|
||||||
|
5
index.js
5
index.js
@ -15,8 +15,9 @@ const units = require('./Units.flitter')
|
|||||||
* the initialization function that chains together the individual units. This
|
* the initialization function that chains together the individual units. This
|
||||||
* is why we pass it the units.
|
* is why we pass it the units.
|
||||||
*/
|
*/
|
||||||
const FlitterApp = require('libflitter/app/FlitterApp')
|
const { FlitterApp, RunLevelErrorHandler } = require('libflitter')
|
||||||
const flitter = new FlitterApp(units)
|
const flitter = new FlitterApp(units)
|
||||||
|
const rleh = new RunLevelErrorHandler()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Launch the server.
|
* Launch the server.
|
||||||
@ -24,4 +25,4 @@ const flitter = new FlitterApp(units)
|
|||||||
* This calls the first unit in the unit chain. This chain ends with the Flitter
|
* This calls the first unit in the unit chain. This chain ends with the Flitter
|
||||||
* server component which launches the Node HTTP server.
|
* server component which launches the Node HTTP server.
|
||||||
*/
|
*/
|
||||||
flitter.run()
|
flitter.run().catch(rleh.handle)
|
||||||
|
@ -16,14 +16,14 @@
|
|||||||
"author": "Garrett Mills <garrett@glmdev.tech> (https://garrettmills.dev/)",
|
"author": "Garrett Mills <garrett@glmdev.tech> (https://garrettmills.dev/)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flitter-auth": "^0.18.0",
|
"flitter-auth": "^0.18.2",
|
||||||
"flitter-cli": "^0.15.2",
|
"flitter-cli": "^0.16.0",
|
||||||
"flitter-di": "^0.4.1",
|
"flitter-di": "^0.5.0",
|
||||||
"flitter-flap": "^0.5.2",
|
"flitter-flap": "^0.5.2",
|
||||||
"flitter-forms": "^0.8.1",
|
"flitter-forms": "^0.8.1",
|
||||||
"flitter-orm": "^0.2.4",
|
"flitter-orm": "^0.2.4",
|
||||||
"flitter-upload": "^0.8.0",
|
"flitter-upload": "^0.8.0",
|
||||||
"ldapjs": "^1.0.2",
|
"ldapjs": "^1.0.2",
|
||||||
"libflitter": "^0.48.1"
|
"libflitter": "^0.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user