Implement OAuth2 server, link oauth:Client and auth::Oauth2Client, implement permission checks
This commit is contained in:
272
app/controllers/api/v1/App.controller.js
Normal file
272
app/controllers/api/v1/App.controller.js
Normal file
@@ -0,0 +1,272 @@
|
||||
const { Controller } = require('libflitter')
|
||||
|
||||
class AppController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models', 'utility']
|
||||
}
|
||||
|
||||
async get_applications(req, res, next) {
|
||||
const Application = this.models.get('Application')
|
||||
const applications = await Application.find({ active: true })
|
||||
const data = []
|
||||
|
||||
for ( const app of applications ) {
|
||||
if ( req.user.can(`app:${app.id}:view`) ) {
|
||||
data.push(await app.to_api())
|
||||
}
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_application(req, res, next) {
|
||||
const Application = this.models.get('Application')
|
||||
const application = await Application.findById(req.params.id)
|
||||
|
||||
if ( !application || !application.active )
|
||||
return res.status(404)
|
||||
.message('Application not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`app:${application.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
return res.api(await application.to_api())
|
||||
}
|
||||
|
||||
async create_application(req, res, next) {
|
||||
const Application = this.models.get('Application')
|
||||
|
||||
if ( !req.user.can('app:create') )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['name', 'identifier']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the identifier is properly formatted
|
||||
if ( !(new RegExp('^[a-zA-Z0-9_]*$')).test(req.body.identifier) )
|
||||
return res.status(400)
|
||||
.message('Improperly formatted field: identifier (alphanumeric with underscores only)')
|
||||
.api()
|
||||
|
||||
// Make sure the identifier is unique
|
||||
const existing_app = await Application.findOne({ identifier: req.body.identifier })
|
||||
if ( existing_app )
|
||||
return res.status(400)
|
||||
.message('An Application with that identifier already exists.')
|
||||
.api()
|
||||
|
||||
const application = new Application({
|
||||
name: req.body.name,
|
||||
identifier: req.body.identifier,
|
||||
description: req.body.description,
|
||||
})
|
||||
|
||||
// Verify LDAP client IDs
|
||||
const LDAPClient = this.models.get('ldap:Client')
|
||||
if ( req.body.ldap_client_ids ) {
|
||||
const parsed = typeof req.body.ldap_client_ids === 'string' ? this.utility.infer(req.body.ldap_client_ids) : req.body.ldap_client_ids
|
||||
const ldap_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of ldap_client_ids ) {
|
||||
const client = await LDAPClient.findById(id)
|
||||
if ( !client || !client.active || !req.user.can(`ldap:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`Invalid ldap_client_id: ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ ldap_client_ids: client.id })
|
||||
if ( other_assoc_app )
|
||||
return res.status(400)
|
||||
.message(`The LDAP client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.ldap_client_ids = ldap_client_ids
|
||||
}
|
||||
|
||||
// Verify OAuth client IDs
|
||||
const OAuthClient = this.models.get('oauth:Client')
|
||||
if ( req.body.oauth_client_ids ) {
|
||||
const parsed = typeof req.body.oauth_client_ids === 'string' ? this.utility.infer(req.body.oauth_client_ids) : req.body.oauth_client_ids
|
||||
const oauth_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of oauth_client_ids ) {
|
||||
const client = await OAuthClient.findById(id)
|
||||
if ( !client || !client.active || !req.user.can(`oauth:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`Invalid oauth_client_id: ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ oauth_client_ids: client.id })
|
||||
if ( other_assoc_app )
|
||||
return res.status(400)
|
||||
.message(`The OAuth2 client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.oauth_client_ids = oauth_client_ids
|
||||
}
|
||||
|
||||
// Verify SAML service provider IDs
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
if ( req.body.saml_service_provider_ids ) {
|
||||
const parsed = typeof req.body.saml_service_provider_ids === 'string' ? this.utility.infer(req.body.saml_service_provider_ids) : req.body.saml_service_provider_ids
|
||||
const saml_service_provider_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of saml_service_provider_ids ) {
|
||||
const provider = await ServiceProvider.findById(id)
|
||||
if ( !provider || !provider.active || !req.user.can(`saml:provider:${provider.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`Invalid saml_service_provider_id: ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ saml_service_provider_ids: provider.id })
|
||||
if ( other_assoc_app )
|
||||
return res.status(400)
|
||||
.message(`The SAML service provider ${provider.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.saml_service_provider_ids = saml_service_provider_ids
|
||||
}
|
||||
|
||||
await application.save()
|
||||
return res.api(await application.to_api())
|
||||
}
|
||||
|
||||
async update_application(req, res, next) {
|
||||
const Application = this.models.get('Application')
|
||||
const application = await Application.findById(req.params.id)
|
||||
|
||||
if ( !application || !application.active )
|
||||
return res.status(404)
|
||||
.message('Application not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`app:${application.id}:update`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['name', 'identifier']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the identifier is properly formatted
|
||||
if ( !(new RegExp('^[a-zA-Z0-9_]*$')).test(req.body.identifier) )
|
||||
return res.status(400)
|
||||
.message('Improperly formatted field: identifier (alphanumeric with underscores only)')
|
||||
.api()
|
||||
|
||||
// Make sure the identifier is unique
|
||||
const existing_app = await Application.findOne({ identifier: req.body.identifier })
|
||||
if ( existing_app && existing_app.id !== application.id )
|
||||
return res.status(400)
|
||||
.message('An Application with that identifier already exists.')
|
||||
.api()
|
||||
|
||||
// Verify LDAP client IDs
|
||||
const LDAPClient = this.models.get('ldap:Client')
|
||||
if ( req.body.ldap_client_ids ) {
|
||||
const parsed = typeof req.body.ldap_client_ids === 'string' ? this.utility.infer(req.body.ldap_client_ids) : req.body.ldap_client_ids
|
||||
const ldap_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of ldap_client_ids ) {
|
||||
const client = await LDAPClient.findById(id)
|
||||
if ( !client || !client.active || !req.user.can(`ldap:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`Invalid ldap_client_id: ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ ldap_client_ids: client.id })
|
||||
if ( other_assoc_app && other_assoc_app.id !== application.id )
|
||||
return res.status(400)
|
||||
.message(`The LDAP client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.ldap_client_ids = ldap_client_ids
|
||||
} else application.ldap_client_ids = []
|
||||
|
||||
// Verify OAuth client IDs
|
||||
const OAuthClient = this.models.get('oauth:Client')
|
||||
if ( req.body.oauth_client_ids ) {
|
||||
const parsed = typeof req.body.oauth_client_ids === 'string' ? this.utility.infer(req.body.oauth_client_ids) : req.body.oauth_client_ids
|
||||
const oauth_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of oauth_client_ids ) {
|
||||
const client = await OAuthClient.findById(id)
|
||||
if ( !client || !client.active || !req.user.can(`oauth:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`Invalid oauth_client_id: ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ oauth_client_ids: client.id })
|
||||
if ( other_assoc_app && other_assoc_app.id !== application.id )
|
||||
return res.status(400)
|
||||
.message(`The OAuth2 client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.oauth_client_ids = oauth_client_ids
|
||||
} else application.oauth_client_ids = []
|
||||
|
||||
// Verify SAML service provider IDs
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
if ( req.body.saml_service_provider_ids ) {
|
||||
const parsed = typeof req.body.saml_service_provider_ids === 'string' ? this.utility.infer(req.body.saml_service_provider_ids) : req.body.saml_service_provider_ids
|
||||
const saml_service_provider_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of saml_service_provider_ids ) {
|
||||
const provider = await ServiceProvider.findById(id)
|
||||
if ( !provider || !provider.active || !req.user.can(`saml:provider:${provider.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`Invalid saml_service_provider_id: ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ saml_service_provider_ids: provider.id })
|
||||
if ( other_assoc_app && other_assoc_app.id !== application.id )
|
||||
return res.status(400)
|
||||
.message(`The SAML service provider ${provider.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.saml_service_provider_ids = saml_service_provider_ids
|
||||
} else application.saml_service_provider_ids = []
|
||||
|
||||
application.name = req.body.name
|
||||
application.identifier = req.body.identifier
|
||||
application.description = req.body.description
|
||||
await application.save()
|
||||
return res.api(await application.to_api())
|
||||
}
|
||||
|
||||
async delete_application(req, res, next) {
|
||||
const Application = this.models.get('Application')
|
||||
const application = await Application.findById(req.params.id)
|
||||
|
||||
if ( !application || !application.active )
|
||||
return res.status(404)
|
||||
.message('Application not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`app:${application.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
application.active = false
|
||||
await application.save()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = AppController
|
||||
@@ -1,8 +1,9 @@
|
||||
const { Controller } = require('libflitter')
|
||||
const zxcvbn = require('zxcvbn')
|
||||
|
||||
class AuthController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models', 'auth', 'MFA', 'output', 'configs']
|
||||
return [...super.services, 'models', 'auth', 'MFA', 'output', 'configs', 'utility']
|
||||
}
|
||||
|
||||
async get_users(req, res, next) {
|
||||
@@ -18,6 +19,20 @@ class AuthController extends Controller {
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_groups(req, res, next) {
|
||||
const Group = this.models.get('auth:Group')
|
||||
const groups = await Group.find({active: true})
|
||||
const data = []
|
||||
|
||||
for ( const group of groups ) {
|
||||
if ( !req.user.can(`auth:group:${group.id}:view`) ) continue
|
||||
data.push(await group.to_api())
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
|
||||
async get_roles(req, res, next) {
|
||||
const role_config = this.configs.get('auth.roles')
|
||||
const data = []
|
||||
@@ -32,6 +47,291 @@ class AuthController extends Controller {
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_group(req, res, next) {
|
||||
const Group = this.models.get('auth:Group')
|
||||
const group = await Group.findById(req.params.id)
|
||||
|
||||
if ( !group || !group.active )
|
||||
return res.status(404)
|
||||
.message('Group not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`auth:group:${group.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
async get_user(req, res, next) {
|
||||
if ( req.params.id === 'me' )
|
||||
return res.api(await req.user.to_api())
|
||||
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findById(req.params.id)
|
||||
|
||||
if ( !user )
|
||||
return res.status(404)
|
||||
.message('User not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`auth:user:${user.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
return res.api(await user.to_api())
|
||||
}
|
||||
|
||||
async create_group(req, res, next) {
|
||||
if ( !req.user.can(`auth:group:create`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
if ( !req.body.name )
|
||||
return res.status(400)
|
||||
.message('Missing required field: name')
|
||||
.api()
|
||||
|
||||
const Group = this.models.get('auth:Group')
|
||||
|
||||
// Make sure the name is free
|
||||
const existing_group = await Group.findOne({ name: req.body.name })
|
||||
if ( existing_group )
|
||||
return res.status(400)
|
||||
.message('A group with that name already exists.')
|
||||
.api()
|
||||
|
||||
const group = new Group({ name: req.body.name })
|
||||
|
||||
// Validate user ids
|
||||
const User = this.models.get('auth:User')
|
||||
if ( req.body.user_ids ) {
|
||||
const parsed = typeof req.body.user_ids === 'string' ? this.utility.infer(req.body.user_ids) : req.body.user_ids
|
||||
const user_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const user_id of user_ids ) {
|
||||
const user = await User.findById(user_id)
|
||||
if ( !user )
|
||||
return res.status(400)
|
||||
.message('Invalid user_id.')
|
||||
.api()
|
||||
}
|
||||
|
||||
group.user_ids = user_ids
|
||||
}
|
||||
|
||||
await group.save()
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
async create_user(req, res, next) {
|
||||
if ( !req.user.can('auth:user:create') )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['uid', 'first_name', 'last_name', 'email', 'password']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure uid & email are unique
|
||||
const User = this.models.get('auth:User')
|
||||
const unique_fields = ['uid', 'email']
|
||||
for ( const field of unique_fields ) {
|
||||
const filter = {}
|
||||
filter[field] = req.body[field]
|
||||
const existing_user = await User.findOne(filter)
|
||||
if ( existing_user )
|
||||
return res.status(400)
|
||||
.message(`A user already exists with that ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Verify password complexity
|
||||
const min_score = 3
|
||||
const result = zxcvbn(req.body.password)
|
||||
if ( result.score < min_score )
|
||||
return res.status(400)
|
||||
.message(`Password does not meet the minimum complexity score of ${min_score}.`)
|
||||
.api()
|
||||
|
||||
const user = new User({
|
||||
uid: req.body.uid,
|
||||
email: req.body.email,
|
||||
first_name: req.body.first_name,
|
||||
last_name: req.body.last_name,
|
||||
})
|
||||
|
||||
if ( req.body.tagline )
|
||||
user.tagline = req.body.tagline
|
||||
|
||||
await user.reset_password(req.body.password, 'create')
|
||||
await user.save()
|
||||
return res.api(await user.to_api())
|
||||
}
|
||||
|
||||
async update_group(req, res, next) {
|
||||
const Group = this.models.get('auth:Group')
|
||||
const User = this.models.get('auth:User')
|
||||
|
||||
const group = await Group.findById(req.params.id)
|
||||
if ( !group )
|
||||
return res.status(404)
|
||||
.message('Group not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`auth:group:${group.id}:update`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
if ( !req.body.name )
|
||||
return res.status(400)
|
||||
.message('Missing required field: name')
|
||||
.api()
|
||||
|
||||
// Make sure the group name is unique
|
||||
const existing_group = await Group.findOne({ name: req.body.name })
|
||||
if ( existing_group && existing_group.id !== group.id )
|
||||
return res.status(400)
|
||||
.message('A group with that name already exists.')
|
||||
.api()
|
||||
|
||||
// Validate user_ids
|
||||
if ( req.body.user_ids ) {
|
||||
const parsed = typeof req.body.user_ids === 'string' ? this.utility.infer(req.body.user_ids) : req.body.user_ids
|
||||
const user_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const user_id of user_ids ) {
|
||||
const user = await User.findById(user_id)
|
||||
if ( !user )
|
||||
return res.status(400)
|
||||
.message('Invalid user_id.')
|
||||
.api()
|
||||
}
|
||||
|
||||
group.user_ids = user_ids
|
||||
} else {
|
||||
group.user_ids = []
|
||||
}
|
||||
|
||||
group.name = req.body.name
|
||||
await group.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async update_user(req, res, next) {
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findById(req.params.id)
|
||||
|
||||
if ( !user )
|
||||
return res.status(404)
|
||||
.message('User not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`auth:user:${user.id}:update`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['uid', 'first_name', 'last_name', 'email']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the uid/email are unique
|
||||
const unique_fields = ['uid', 'email']
|
||||
for ( const field of unique_fields ) {
|
||||
const filter = {}
|
||||
filter[field] = req.body[field]
|
||||
const existing_user = await User.findOne(filter)
|
||||
if ( existing_user && existing_user.id !== user.id )
|
||||
return res.status(400)
|
||||
.message(`A user already exists with that ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Verify password complexity
|
||||
if ( req.body.password ) {
|
||||
const min_score = 3
|
||||
const result = zxcvbn(req.body.password)
|
||||
if (result.score < min_score)
|
||||
return res.status(400)
|
||||
.message(`Password does not meet the minimum complexity score of ${min_score}.`)
|
||||
.api()
|
||||
|
||||
await user.reset_password(req.body.password, 'api')
|
||||
}
|
||||
|
||||
user.first_name = req.body.first_name
|
||||
user.last_name = req.body.last_name
|
||||
user.uid = req.body.uid
|
||||
user.email = req.body.email
|
||||
|
||||
if ( req.body.tagline )
|
||||
user.tagline = req.body.tagline
|
||||
else
|
||||
user.tagline = ''
|
||||
|
||||
await user.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_group(req, res, next) {
|
||||
const Group = this.models.get('auth:Group')
|
||||
const group = await Group.findById(req.params.id)
|
||||
|
||||
if ( !group )
|
||||
return res.status(404)
|
||||
.message('Group not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`auth:group:${group.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
group.active = false
|
||||
await group.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_user(req, res, next) {
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findById(req.params.id)
|
||||
|
||||
if ( !user )
|
||||
return res.status(404)
|
||||
.message('User not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`auth:user:${user.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
// check if the user is an LDAP client. if so, delete the client
|
||||
const Client = this.models.get('ldap:Client')
|
||||
const matching_client = await Client.findOne({ user_id: user.id })
|
||||
if ( matching_client ) {
|
||||
matching_client.active = false
|
||||
await matching_client.save()
|
||||
}
|
||||
|
||||
user.active = false
|
||||
await user.kickout()
|
||||
await user.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async validate_username(req, res, next) {
|
||||
let is_valid = true
|
||||
|
||||
@@ -73,6 +373,18 @@ class AuthController extends Controller {
|
||||
success: false,
|
||||
})
|
||||
|
||||
// Make sure the user can sign in.
|
||||
// Sign-in is NOT allowed for LDAP clients
|
||||
const Client = this.models.get('ldap:Client')
|
||||
const client = await Client.findOne({ user_id: user.id })
|
||||
if ( client )
|
||||
return res.status(200)
|
||||
.message(`Invalid username or password.`)
|
||||
.api({
|
||||
message: `Invalid username or password.`,
|
||||
success: false,
|
||||
})
|
||||
|
||||
if ( req.body.create_session )
|
||||
await flitter.session(req, user)
|
||||
|
||||
|
||||
236
app/controllers/api/v1/IAM.controller.js
Normal file
236
app/controllers/api/v1/IAM.controller.js
Normal file
@@ -0,0 +1,236 @@
|
||||
const { Controller } = require('libflitter')
|
||||
|
||||
class IAMController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models']
|
||||
}
|
||||
|
||||
async check_entity_access(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
|
||||
if ( !req.body.entity_id && !req.body.target_id )
|
||||
return res.status(400)
|
||||
.message('Missing one or more required fields: entity_id, target_id')
|
||||
.api()
|
||||
|
||||
return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id))
|
||||
}
|
||||
|
||||
async check_user_access(req, res, next) {
|
||||
const User = this.models.get('auth:User')
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
|
||||
if ( !req.body.target_id )
|
||||
return res.status(400)
|
||||
.message('Missing required field: target_id')
|
||||
.api()
|
||||
|
||||
let user = req.user
|
||||
if ( req.body.user_id && req.body.user_id !== 'me' )
|
||||
user = await User.findById(req.body.user_id)
|
||||
|
||||
if ( !user )
|
||||
return res.status(404)
|
||||
.message('User not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`auth:user:${user.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
return res.api(await Policy.check_user_access(user, req.body.target_id))
|
||||
}
|
||||
|
||||
async get_policies(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const policies = await Policy.find({ active: true })
|
||||
const data = []
|
||||
|
||||
for ( const policy of policies ) {
|
||||
if ( req.user.can(`iam:policy:${policy.id}:view`) ) {
|
||||
data.push(await policy.to_api())
|
||||
}
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_policy(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const policy = await Policy.findById(req.params.id)
|
||||
|
||||
if ( !policy )
|
||||
return res.status(404)
|
||||
.message('Policy not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`iam:policy:${policy.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
return res.api(await policy.to_api())
|
||||
}
|
||||
|
||||
async create_policy(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
|
||||
const required_fields = ['entity_type', 'entity_id', 'access_type', 'target_type', 'target_id']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !['user', 'group'].includes(req.body.entity_type) )
|
||||
return res.status(400)
|
||||
.message('Invalid entity_type. Must be one of: user, group.')
|
||||
.api()
|
||||
|
||||
// Make sure the entity_id is valid
|
||||
if ( req.body.entity_type === 'user' ) {
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findById(req.body.entity_id)
|
||||
if ( !user || !req.user.can(`auth:user:${user.id}:view`) )
|
||||
return res.status(400)
|
||||
.message('Invalid entity_id.')
|
||||
.api()
|
||||
} else if ( req.body.entity_type === 'group' ) {
|
||||
const Group = this.models.get('auth:Group')
|
||||
const group = await Group.findById(req.body.entity_id)
|
||||
if ( !group || !group.active || !req.user.can(`auth:group:${group.id}:view`) )
|
||||
return res.status(400)
|
||||
.message('Invalid entity_id.')
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !['allow', 'deny'].includes(req.body.access_type) )
|
||||
return res.status(400)
|
||||
.message('Invalid access_type. Must be one of: allow, deny.')
|
||||
.api()
|
||||
|
||||
if ( !['application'].includes(req.body.target_type) )
|
||||
return res.status(400)
|
||||
.message('Invalid target_type. Must be one of: application.')
|
||||
.api()
|
||||
|
||||
// Make sure the target_id is valid
|
||||
if ( req.body.target_type === 'application' ) {
|
||||
const Application = this.models.get('Application')
|
||||
const app = await Application.findById(req.body.target_id)
|
||||
if ( !app || !app.active || !req.user.can(`app:${app.id}:view`) )
|
||||
return res.status(400)
|
||||
.message('Invalid target_id.')
|
||||
.api()
|
||||
}
|
||||
|
||||
const policy = new Policy({
|
||||
entity_type: req.body.entity_type,
|
||||
entity_id: req.body.entity_id,
|
||||
access_type: req.body.access_type,
|
||||
target_type: req.body.target_type,
|
||||
target_id: req.body.target_id,
|
||||
})
|
||||
|
||||
await policy.save()
|
||||
req.user.allow(`iam:policy:${policy.id}`)
|
||||
await req.user.save()
|
||||
return res.api(await policy.to_api())
|
||||
}
|
||||
|
||||
async update_policy(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const policy = await Policy.findById(req.params.id)
|
||||
|
||||
if ( !policy || !policy.active )
|
||||
return res.status(404)
|
||||
.message('Policy not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`iam:policy:${policy.id}:update`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['entity_type', 'entity_id', 'access_type', 'target_type', 'target_id']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !['user', 'group'].includes(req.body.entity_type) )
|
||||
return res.status(400)
|
||||
.message('Invalid entity_type. Must be one of: user, group.')
|
||||
.api()
|
||||
|
||||
// Make sure the entity_id is valid
|
||||
if ( req.body.entity_type === 'user' ) {
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findById(req.body.entity_id)
|
||||
if ( !user || !req.user.can(`auth:user:${user.id}:view`) )
|
||||
return res.status(400)
|
||||
.message('Invalid entity_id.')
|
||||
.api()
|
||||
} else if ( req.body.entity_type === 'group' ) {
|
||||
const Group = this.models.get('auth:Group')
|
||||
const group = await Group.findById(req.body.entity_id)
|
||||
if ( !group || !group.active || !req.user.can(`auth:group:${group.id}:view`) )
|
||||
return res.status(400)
|
||||
.message('Invalid entity_id.')
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !['allow', 'deny'].includes(req.body.access_type) )
|
||||
return res.status(400)
|
||||
.message('Invalid access_type. Must be one of: allow, deny.')
|
||||
.api()
|
||||
|
||||
if ( !['application'].includes(req.body.target_type) )
|
||||
return res.status(400)
|
||||
.message('Invalid target_type. Must be one of: application.')
|
||||
.api()
|
||||
|
||||
// Make sure the target_id is valid
|
||||
if ( req.body.target_type === 'application' ) {
|
||||
const Application = this.models.get('Application')
|
||||
const app = await Application.findById(req.body.target_id)
|
||||
if ( !app || !app.active || !req.user.can(`app:${app.id}:view`) )
|
||||
return res.status(400)
|
||||
.message('Invalid target_id.')
|
||||
.api()
|
||||
}
|
||||
|
||||
policy.entity_type = req.body.entity_type
|
||||
policy.entity_id = req.body.entity_id
|
||||
policy.access_type = req.body.access_type
|
||||
policy.target_type = req.body.target_type
|
||||
policy.target_id = req.body.target_id
|
||||
await policy.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_policy(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const policy = await Policy.findById(req.params.id)
|
||||
|
||||
if ( !policy || !policy.active )
|
||||
return res.status(404)
|
||||
.message('Policy not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`iam:policy:${policy.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
policy.active = false
|
||||
await policy.save()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = IAMController
|
||||
136
app/controllers/api/v1/OAuth.controller.js
Normal file
136
app/controllers/api/v1/OAuth.controller.js
Normal file
@@ -0,0 +1,136 @@
|
||||
const { Controller } = require('libflitter')
|
||||
const is_absolute_url = require('is-absolute-url')
|
||||
|
||||
class OAuthController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models']
|
||||
}
|
||||
|
||||
async get_clients(req, res, next) {
|
||||
const Client = this.models.get('oauth:Client')
|
||||
const clients = await Client.find({ active: true })
|
||||
const data = []
|
||||
|
||||
for ( const client of clients ) {
|
||||
if ( req.user.can(`oauth:client:${client.id}:view`) ) {
|
||||
data.push(await client.to_api())
|
||||
}
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_client(req, res, next) {
|
||||
const Client = this.models.get('oauth:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
return res.status(404)
|
||||
.message('Client not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`oauth:client:${client.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async create_client(req, res, next) {
|
||||
if ( !req.user.can('oauth:client:create') )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['name', 'api_scopes', 'redirect_url']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !Array.isArray(req.body.api_scopes) ) {
|
||||
return res.status(400)
|
||||
.message(`Improperly formatted field: api_scopes (should be array)`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !is_absolute_url(req.body.redirect_url) )
|
||||
return res.status(400)
|
||||
.message(`Improperly formatted field: redirect_url (should be absolute URL)`)
|
||||
.api()
|
||||
|
||||
const Client = this.models.get('oauth:Client')
|
||||
const client = new Client({
|
||||
name: req.body.name,
|
||||
api_scopes: req.body.api_scopes,
|
||||
redirect_url: req.body.redirect_url,
|
||||
})
|
||||
|
||||
await client.save()
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async update_client(req, res, next) {
|
||||
const Client = this.models.get('oauth:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
return res.status(404)
|
||||
.message('Client not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`oauth:client:${client.id}:update`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['name', 'api_scopes', 'redirect_url']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !Array.isArray(req.body.api_scopes) )
|
||||
return res.status(400)
|
||||
.message(`Improperly formatted field: api_scopes (should be array)`)
|
||||
.api()
|
||||
|
||||
if ( !is_absolute_url(req.body.redirect_url) )
|
||||
return res.status(400)
|
||||
.message(`Improperly formatted field: redirect_url (should be absolute URL)`)
|
||||
.api()
|
||||
|
||||
client.name = req.body.name
|
||||
client.api_scopes = req.body.api_scopes
|
||||
client.redirect_url = req.body.redirect_url
|
||||
|
||||
await client.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_client(req, res, next) {
|
||||
const Client = this.models.get('oauth:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
return res.status(404)
|
||||
.message('Client not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`oauth:client:${client.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
client.active = false
|
||||
await client.save()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = OAuthController
|
||||
184
app/controllers/api/v1/Reflect.controller.js
Normal file
184
app/controllers/api/v1/Reflect.controller.js
Normal file
@@ -0,0 +1,184 @@
|
||||
const { Controller } = require('libflitter')
|
||||
const uuid = require('uuid/v4')
|
||||
|
||||
class ReflectController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'routers', 'models']
|
||||
}
|
||||
|
||||
async get_tokens(req, res, next) {
|
||||
const Oauth2BearerToken = this.models.get('auth::Oauth2BearerToken')
|
||||
const tokens = await Oauth2BearerToken.find({
|
||||
expires: { $gt: new Date },
|
||||
userID: req.user.id,
|
||||
})
|
||||
|
||||
const Client = this.models.get('oauth:Client')
|
||||
const data = []
|
||||
for ( const token of tokens ) {
|
||||
const client = await Client.findOne({ uuid: token.clientID })
|
||||
let client_display = client && client.active ? client.name : '(Non-existent Client)'
|
||||
|
||||
data.push({
|
||||
id: token.id,
|
||||
token: token.accessToken,
|
||||
client_id: token.clientID,
|
||||
client_display,
|
||||
expires: token.expires,
|
||||
user_id: token.userID,
|
||||
})
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_token(req, res, next) {
|
||||
const Oauth2BearerToken = this.models.get('auth::Oauth2BearerToken')
|
||||
const token = await Oauth2BearerToken.findById(req.params.id)
|
||||
|
||||
if ( !token || token.userID !== req.user.id || token.expires <= new Date )
|
||||
return res.status(404)
|
||||
.message('Token not found with that ID, or expired.')
|
||||
.api()
|
||||
|
||||
return res.api({
|
||||
id: token.id,
|
||||
token: token.accessToken,
|
||||
client_id: token.clientID,
|
||||
expires: token.expires,
|
||||
user_id: token.userID,
|
||||
})
|
||||
}
|
||||
|
||||
async create_token(req, res, next) {
|
||||
const Oauth2BearerToken = this.models.get('auth::Oauth2BearerToken')
|
||||
|
||||
if ( !req.body.client_id )
|
||||
return res.status(400)
|
||||
.message('Missing required field: client_id')
|
||||
.api()
|
||||
|
||||
const Client = this.models.get('oauth:Client')
|
||||
const client = await Client.findOne({uuid: req.body.client_id})
|
||||
if ( !client || !client.active )
|
||||
return res.status(400)
|
||||
.message('Invalid client_id.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`oauth:client:${client.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const expires = new Date()
|
||||
expires.setDate(expires.getDate() + 7)
|
||||
|
||||
const token = new Oauth2BearerToken({
|
||||
accessToken: String(uuid()).replace(/-/g, ''),
|
||||
clientID: client.uuid,
|
||||
expires,
|
||||
userID: req.user.id,
|
||||
})
|
||||
|
||||
await token.save()
|
||||
return res.api({
|
||||
id: token.id,
|
||||
token: token.accessToken,
|
||||
client_id: token.clientID,
|
||||
expires: token.expires,
|
||||
user_id: token.userID,
|
||||
})
|
||||
}
|
||||
|
||||
async update_token(req, res, next) {
|
||||
const Oauth2BearerToken = this.models.get('auth::Oauth2BearerToken')
|
||||
const token = await Oauth2BearerToken.findById(req.params.id)
|
||||
|
||||
if ( !token || token.userID !== req.user.id || token.expires <= new Date )
|
||||
return res.status(404)
|
||||
.message('Token not found with that ID, or expired.')
|
||||
.api()
|
||||
|
||||
if ( !req.body.client_id )
|
||||
return res.status(400)
|
||||
.message('Missing required field: client_id')
|
||||
.api()
|
||||
|
||||
const Client = this.models.get('oauth:Client')
|
||||
const client = await Client.findOne({uuid: req.body.client_id})
|
||||
|
||||
if ( !client || !client.active || !req.user.can(`oauth:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message('Invalid client_id.')
|
||||
.api()
|
||||
|
||||
token.client_id = client.uuid
|
||||
await token.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_token(req, res, next) {
|
||||
const Oauth2BearerToken = this.models.get('auth::Oauth2BearerToken')
|
||||
const token = await Oauth2BearerToken.findById(req.params.id)
|
||||
|
||||
if ( !token || token.userID !== req.user.id || token.expires <= new Date )
|
||||
return res.status(404)
|
||||
.message('Token not found with that ID, or expired.')
|
||||
.api()
|
||||
|
||||
await token.delete()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async get_scopes(req, res, next) {
|
||||
const routers = this.routers.canonical_items
|
||||
const scopes = []
|
||||
|
||||
for ( const prefix in routers ) {
|
||||
if ( !routers.hasOwnProperty(prefix) ) continue
|
||||
const router = routers[prefix].schema
|
||||
|
||||
const supported_verbs = ['get', 'post', 'put', 'delete', 'copy', 'patch']
|
||||
for ( const verb of supported_verbs ) {
|
||||
if ( typeof router[verb] === 'object' ) {
|
||||
const defs = router[verb]
|
||||
for ( const def of Object.values(defs) ) {
|
||||
if ( Array.isArray(def) ) {
|
||||
for ( const layer of def ) {
|
||||
if ( Array.isArray(layer) && layer.length > 1 && layer[0] === 'middleware::api:Permission' ) {
|
||||
if ( typeof layer[1] === 'object' && layer[1].check ) {
|
||||
scopes.push(layer[1].check)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scopes.sort()
|
||||
return res.api(scopes.map(x => {
|
||||
return { scope: x }
|
||||
}))
|
||||
}
|
||||
|
||||
async check_permissions(req, res, next) {
|
||||
if ( !req.body.permissions )
|
||||
return res.status(400)
|
||||
.message('Missing permissions to check.')
|
||||
.api()
|
||||
|
||||
const parsed = typeof req.body.permissions === 'string' ? this.utility.infer(req.body.permissions) : req.body.permissions
|
||||
const permissions = Array.isArray(parsed) ? parsed : [parsed]
|
||||
|
||||
const returns = {}
|
||||
for ( const permission of permissions ) {
|
||||
returns[permission] = req.user.can(permission)
|
||||
}
|
||||
|
||||
return res.api(returns)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ReflectController
|
||||
@@ -4,7 +4,7 @@ const samlp = require('samlp')
|
||||
|
||||
class SAMLController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'saml', 'output', 'Vue', 'configs']
|
||||
return [...super.services, 'saml', 'output', 'Vue', 'configs', 'models']
|
||||
}
|
||||
|
||||
async get_metadata(req, res, next) {
|
||||
@@ -20,10 +20,24 @@ class SAMLController extends Controller {
|
||||
}
|
||||
|
||||
// TODO some sort of first-logon flow
|
||||
// TODO Also, customize logon continue message
|
||||
async get_sso(req, res, next) {
|
||||
const index = await req.saml.participants.issue({ service_provider: req.saml_request.service_provider })
|
||||
|
||||
// Apply the appropriate IAM policy if this SAML SP is associated with an App
|
||||
// If the SAML service provider has no associated application, just allow it
|
||||
// TODO test this
|
||||
const associated_app = await req.saml_request.service_provider.application()
|
||||
if ( associated_app ) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const can_access = await Policy.check_user_access(req.user, associated_app.id)
|
||||
if ( !can_access ) {
|
||||
return this.Vue.auth_message(res, {
|
||||
message: `Sorry, you don't have permission to access this application. Please ask your administrator to grant you access to ${associated_app.name}.`,
|
||||
next_destination: '/dash',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return samlp.auth({
|
||||
issuer: this.saml.config().provider_name,
|
||||
cert: await this.saml.public_cert(),
|
||||
|
||||
Reference in New Issue
Block a user