Implement OAuth2 server, link oauth:Client and auth::Oauth2Client, implement permission checks

This commit is contained in:
garrettmills
2020-05-16 23:55:08 -05:00
parent 6f621f5891
commit d558f21375
51 changed files with 2808 additions and 159 deletions

View 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

View File

@@ -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)

View 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

View 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

View 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

View File

@@ -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(),