diff --git a/TODO.text b/TODO.text index 82a33fa..e71f0ce 100644 --- a/TODO.text +++ b/TODO.text @@ -2,7 +2,4 @@ - Forgot password handling - Admin password reset mechanism -> flag users as needing PW resets - OAuth2 -> support refresh tokens -- Traps - - Allow setting user trap from web UI - - Don't allow external logins if trap is set - Trust token page -> force username of current user diff --git a/app/assets/app/resource/auth/Trap.resource.js b/app/assets/app/resource/auth/Trap.resource.js new file mode 100644 index 0000000..b119f4e --- /dev/null +++ b/app/assets/app/resource/auth/Trap.resource.js @@ -0,0 +1,13 @@ +import CRUDBase from '../CRUDBase.js' + +class TrapResource extends CRUDBase { + endpoint = '/api/v1/auth/traps' + required_fields = ['name', 'trap', 'redirect_to'] + permission_base = 'v1:auth:traps' + + item = 'Trap' + plural = 'Traps' +} + +const auth_trap = new TrapResource() +export { auth_trap } diff --git a/app/assets/app/resource/auth/User.resource.js b/app/assets/app/resource/auth/User.resource.js index 65dabe4..9d473e8 100644 --- a/app/assets/app/resource/auth/User.resource.js +++ b/app/assets/app/resource/auth/User.resource.js @@ -99,6 +99,16 @@ class UserResource extends CRUDBase { placeholder: 'Password', required: ['insert'], }, + { + name: 'Trap', + field: 'trap', + type: 'select.dynamic', + options: { + resource: 'auth/Trap', + display: 'name', + value: 'trap', + }, + }, ], } } diff --git a/app/controllers/api/v1/Auth.controller.js b/app/controllers/api/v1/Auth.controller.js index 5c1562a..d4d8dd4 100644 --- a/app/controllers/api/v1/Auth.controller.js +++ b/app/controllers/api/v1/Auth.controller.js @@ -7,6 +7,24 @@ class AuthController extends Controller { return [...super.services, 'models', 'auth', 'MFA', 'output', 'configs', 'utility'] } + async get_traps(req, res, next) { + const trap_config = this.configs.get('traps') + const data = [{ name: '(None)', trap: '', redirect_to: '/' }] + for ( const name in trap_config.types ) { + if ( !trap_config.types.hasOwnProperty(name) ) continue + data.push({ + name: name.replace(/_/g, ' ') + .split(' ') + .map(x => x.charAt(0).toUpperCase() + x.substr(1)) + .join(' '), + trap: name, + redirect_to: trap_config.types[name].redirect_to + }) + } + + return res.api(data) + } + async registration(req, res, next) { const User = this.models.get('auth:User') const required_fields = ['first_name', 'last_name', 'uid', 'email'] @@ -230,6 +248,15 @@ class AuthController extends Controller { if ( req.body.tagline ) user.tagline = req.body.tagline + if ( req.body.trap ) { + if ( !req.trap.trap_exists(req.body.trap) ) + return res.status(400) + .message('Invalid trap type.') + .api() + + user.trap = req.body.trap + } + await user.reset_password(req.body.password, 'create') await user.save() return res.api(await user.to_api()) @@ -340,6 +367,16 @@ class AuthController extends Controller { else user.tagline = '' + if ( req.body.trap ) { + if ( !req.trap.trap_exists(req.body.trap) ) + return res.status(400) + .message('Invalid trap type.') + .api() + + user.trap = req.body.trap + } else + user.trap = '' + await user.save() return res.api() } diff --git a/app/ldap/controllers/LDAPController.js b/app/ldap/controllers/LDAPController.js index 4627068..43897de 100644 --- a/app/ldap/controllers/LDAPController.js +++ b/app/ldap/controllers/LDAPController.js @@ -90,6 +90,11 @@ class LDAPController extends Injectable { return next(new LDAP.InvalidCredentialsError()) } + // Check if the resource has a trap. If so, deny access. + if ( item.trap ) { + return next(new LDAP.InvalidCredentialsError('This resource currently has a login trap set. Please visit the web UI to release.')) + } + this.output.info(`Successfully bound resource as DN: ${req.dn.format(this.configs.get('ldap:server.format'))}.`) res.end() return next() diff --git a/app/models/auth/User.model.js b/app/models/auth/User.model.js index 2f25ef6..bb13226 100644 --- a/app/models/auth/User.model.js +++ b/app/models/auth/User.model.js @@ -77,6 +77,7 @@ class User extends AuthUser { last_name: this.last_name, email: this.email, tagline: this.tagline, + trap: this.trap, group_ids: (await this.groups()).map(x => x.id), } } diff --git a/app/routing/Middleware.js b/app/routing/Middleware.js index 1b44cf5..a20b6b8 100644 --- a/app/routing/Middleware.js +++ b/app/routing/Middleware.js @@ -10,8 +10,8 @@ */ const Middleware = [ "auth:Utility", - "Traps", "auth:TrustTokenUtility", + "Traps", "SAMLUtility", // 'MiddlewareName', diff --git a/app/routing/middleware/Traps.middleware.js b/app/routing/middleware/Traps.middleware.js index 78ebf0e..5c242b6 100644 --- a/app/routing/middleware/Traps.middleware.js +++ b/app/routing/middleware/Traps.middleware.js @@ -48,6 +48,10 @@ class TrapUtility { else if ( this.user ) return this.user.trap } + trap_exists(name) { + return !!this.configs[name] + } + config() { return this.configs[this.get_trap()] } diff --git a/app/routing/routers/api/v1/auth.routes.js b/app/routing/routers/api/v1/auth.routes.js index 8711298..864e3da 100644 --- a/app/routing/routers/api/v1/auth.routes.js +++ b/app/routing/routers/api/v1/auth.routes.js @@ -8,6 +8,12 @@ const auth_routes = { get: { '/mfa/enable/date': ['middleware::auth:UserOnly', 'controller::api:v1:Auth.get_mfa_enable_date'], + '/traps': [ + 'middleware::auth:APIRoute', + ['middleware::api:Permission', { check: 'v1:auth:traps:list'}], + 'controller::api:v1:Auth.get_traps', + ], + '/roles': [ 'middleware::auth:APIRoute', ['middleware::api:Permission', { check: 'v1:auth:roles:list' }],