Flesh out Cobalt, LDAP groups, &c.
This commit is contained in:
43
app/controllers/Cobalt.controller.js
Normal file
43
app/controllers/Cobalt.controller.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const { Controller } = require('libflitter')
|
||||
|
||||
class CobaltController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'Vue']
|
||||
}
|
||||
|
||||
async listing(req, res, next) {
|
||||
return res.page('cobalt:listing', {
|
||||
...this.Vue.data({ resource: this._get_resource(req.params) }),
|
||||
...this.Vue.session(req)
|
||||
})
|
||||
}
|
||||
|
||||
async form(req, res, next) {
|
||||
const other = {
|
||||
mode: (req.query.id ? (req.query.mode === 'view' ? 'view' : 'update') : 'insert'),
|
||||
form_id: req.query.id || ''
|
||||
}
|
||||
|
||||
return res.page('cobalt:form', {
|
||||
...this.Vue.data({ resource: this._get_resource(req.params), ...other }),
|
||||
...this.Vue.session(req),
|
||||
})
|
||||
}
|
||||
|
||||
_get_resource(params) {
|
||||
const resource = params.resource
|
||||
delete params.resource
|
||||
|
||||
const parts = [resource]
|
||||
let i = 0
|
||||
while ( params[i] ) {
|
||||
parts.push(params[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = CobaltController
|
||||
@@ -2,7 +2,34 @@ const { Controller } = require('libflitter')
|
||||
|
||||
class AuthController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models', 'auth', 'MFA', 'output']
|
||||
return [...super.services, 'models', 'auth', 'MFA', 'output', 'configs']
|
||||
}
|
||||
|
||||
async get_users(req, res, next) {
|
||||
const User = this.models.get('auth:User')
|
||||
const users = await User.find()
|
||||
const data = []
|
||||
|
||||
for ( const user of users ) {
|
||||
if ( !req.user.can(`auth:user:${user.id}:view`) && req.user.id !== user.id) continue
|
||||
data.push(await user.to_api())
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_roles(req, res, next) {
|
||||
const role_config = this.configs.get('auth.roles')
|
||||
const data = []
|
||||
for ( const role_name in role_config ) {
|
||||
if ( !role_config.hasOwnProperty(role_name) ) continue
|
||||
data.push({
|
||||
role: role_name,
|
||||
permissions: role_config[role_name],
|
||||
})
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async validate_username(req, res, next) {
|
||||
|
||||
328
app/controllers/api/v1/LDAP.controller.js
Normal file
328
app/controllers/api/v1/LDAP.controller.js
Normal file
@@ -0,0 +1,328 @@
|
||||
const { Controller } = require('libflitter')
|
||||
const zxcvbn = require('zxcvbn')
|
||||
|
||||
class LDAPController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models', 'utility']
|
||||
}
|
||||
|
||||
async get_clients(req, res, next) {
|
||||
const Client = this.models.get('ldap:Client')
|
||||
const clients = await Client.find({active: true})
|
||||
const data = []
|
||||
|
||||
for ( const client of clients ) {
|
||||
if ( !req.user.can(`ldap:client:${client.id}:view`) ) continue
|
||||
data.push(await client.to_api())
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_groups(req, res, next) {
|
||||
const Group = this.models.get('ldap:Group')
|
||||
const groups = await Group.find({active: true})
|
||||
const data = []
|
||||
|
||||
for ( const group of groups ) {
|
||||
if ( !req.user.can(`ldap:group:${group.id}:view`) ) continue
|
||||
data.push(await group.to_api())
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_client(req, res, next) {
|
||||
const Client = this.models.get('ldap:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
return res.status(404)
|
||||
.message('No client found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:client:${client.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async get_group(req, res, next) {
|
||||
const Group = this.models.get('ldap:Group')
|
||||
const group = await Group.findById(req.params.id)
|
||||
|
||||
if ( !group || !group.active )
|
||||
return res.status(404)
|
||||
.message('No group found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:group:${group.id}:view`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
async create_client(req, res, next) {
|
||||
if ( !req.user.can('ldap:client:create') )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
// validate inputs
|
||||
const required_fields = ['name', 'uid', 'password']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
}
|
||||
|
||||
// Make sure the uid is free
|
||||
const User = this.models.get('auth:User')
|
||||
const existing_user = await User.findOne({ uid: req.body.uid })
|
||||
if ( existing_user )
|
||||
return res.status(400)
|
||||
.message('A user with that uid already exists.')
|
||||
.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()
|
||||
|
||||
// Create the client
|
||||
const Client = this.models.get('ldap:Client')
|
||||
const client = await Client.create({
|
||||
uid: req.body.uid,
|
||||
password: req.body.password,
|
||||
name: req.body.name,
|
||||
})
|
||||
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async create_group(req, res, next) {
|
||||
console.log(req.body)
|
||||
if ( !req.user.can(`ldap:group:create`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
// validate inputs
|
||||
const required_fields = ['role', 'name']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the group name is free
|
||||
const Group = this.models.get('ldap:Group')
|
||||
const User = this.models.get('auth:User')
|
||||
const existing_group = await Group.findOne({ name: req.body.name })
|
||||
if ( existing_group )
|
||||
return res.status(400)
|
||||
.message('A group already exists with that name.')
|
||||
.api()
|
||||
|
||||
// Make sure the role exists
|
||||
if ( !this.configs.get('auth.roles')[req.body.role] )
|
||||
return res.status(400)
|
||||
.message('Invalid role.')
|
||||
.api()
|
||||
|
||||
const group = new Group({
|
||||
name: req.body.name,
|
||||
role: req.body.role,
|
||||
})
|
||||
|
||||
if ( 'ldap_visible' in req.body ) group.ldap_visible = !!req.body.ldap_visible
|
||||
if ( 'user_ids' in req.body ) {
|
||||
// Attempt to parse the 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]
|
||||
|
||||
// Make sure all the user IDs are valid
|
||||
for ( const user_id of user_ids ) {
|
||||
const user = await User.findById(user_id)
|
||||
if ( !user )
|
||||
return res.status(400)
|
||||
.message(`Invalid user ID: ${user_id}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
group.user_ids = user_ids
|
||||
}
|
||||
|
||||
await group.save()
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
async update_client(req, res, next) {
|
||||
const Client = this.models.get('ldap:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
return res.status(404)
|
||||
.message('No client found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:client:${client.id}:update`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['name', 'uid']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
const user = await client.user()
|
||||
|
||||
// Update the name
|
||||
if ( req.body.name !== client.name ) {
|
||||
client.name = req.body.name
|
||||
user.first_name = req.body.name
|
||||
}
|
||||
|
||||
// Update the uid
|
||||
if ( req.body.uid !== user.uid ) {
|
||||
// Make sure the UID is free
|
||||
const User = this.models.get('auth:User')
|
||||
const existing_user = await User.findOne({ uid: req.body.uid })
|
||||
if ( existing_user )
|
||||
return res.status(400)
|
||||
.message('A user already exists with that uid.')
|
||||
.api()
|
||||
|
||||
user.uid = req.body.uid
|
||||
}
|
||||
|
||||
// Update the password
|
||||
if ( req.body.password && !(await user.check_password(req.body.password)) ) {
|
||||
// Verify the password's 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()
|
||||
|
||||
await user.reset_password(req.body.password)
|
||||
}
|
||||
|
||||
await user.save()
|
||||
await client.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async update_group(req, res, next) {
|
||||
const User = await this.models.get('auth:User')
|
||||
const Group = await this.models.get('ldap:Group')
|
||||
|
||||
const group = await Group.findById(req.params.id)
|
||||
if ( !group || !group.active )
|
||||
return res.status(404)
|
||||
.message('No group found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:group:${group.id}:update`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const required_fields = ['role', 'name']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the name is free
|
||||
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()
|
||||
|
||||
group.name = req.body.name
|
||||
group.role = req.body.name
|
||||
group.ldap_visible = !!req.body.ldap_visible
|
||||
|
||||
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: ${user_id}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
group.user_ids = user_ids
|
||||
} else {
|
||||
group.user_ids = []
|
||||
}
|
||||
|
||||
await group.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_client(req, res, next) {
|
||||
const Client = this.models.get('ldap: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(`ldap:client:${client.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
const user = await client.user()
|
||||
client.active = false
|
||||
user.active = false
|
||||
user.block_login = true
|
||||
|
||||
await user.save()
|
||||
await client.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_group(req, res, next) {
|
||||
const Group = this.models.get('ldap:Group')
|
||||
const group = await Group.findById(req.params.id)
|
||||
|
||||
if ( !group || !group.active )
|
||||
return res.status(404)
|
||||
.message('No group found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:group:${group.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message('Insufficient permissions.')
|
||||
.api()
|
||||
|
||||
group.active = false
|
||||
await group.save()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = LDAPController
|
||||
126
app/controllers/api/v1/SAML.controller.js
Normal file
126
app/controllers/api/v1/SAML.controller.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const { Controller } = require('libflitter')
|
||||
|
||||
class SAMLController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models']
|
||||
}
|
||||
|
||||
async get_providers(req, res, next) {
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
const providers = await ServiceProvider.find({ active: true })
|
||||
|
||||
const visible = providers.filter(x => req.user.can(`saml:provider:${x.id}:view`))
|
||||
.map(x => x.to_api())
|
||||
|
||||
return res.api(visible)
|
||||
}
|
||||
|
||||
async get_provider(req, res, next) {
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
const provider = await ServiceProvider.findById(req.params.id)
|
||||
|
||||
if ( !provider || !provider.active )
|
||||
return res.status(404).api()
|
||||
|
||||
if ( !req.user.can(`saml:provider:${provider.id}:view`) )
|
||||
return res.status(401).api()
|
||||
|
||||
return res.api(provider.to_api())
|
||||
}
|
||||
|
||||
async create_provider(req, res, next) {
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
const required_fields = ['name', 'entity_id', 'acs_url']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field]?.trim() )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// The entity_id must be unique
|
||||
const existing_provider = await ServiceProvider.findOne({
|
||||
entity_id: req.body.entity_id,
|
||||
active: true,
|
||||
})
|
||||
|
||||
if ( existing_provider )
|
||||
return res.status(400)
|
||||
.send(`A service provider with that entity_id already exists.`)
|
||||
.api()
|
||||
|
||||
const data = {
|
||||
name: req.body.name,
|
||||
entity_id: req.body.entity_id,
|
||||
acs_url: req.body.acs_url,
|
||||
active: true,
|
||||
}
|
||||
|
||||
if ( req.body.slo_url )
|
||||
data.slo_url = req.body.slo_url
|
||||
|
||||
const provider = new ServiceProvider(data)
|
||||
await provider.save()
|
||||
|
||||
req.user.allow(`saml:provider:${provider.id}`)
|
||||
await req.user.save()
|
||||
|
||||
return res.api(provider.to_api())
|
||||
}
|
||||
|
||||
async update_provider(req, res, next) {
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
const provider = await ServiceProvider.findById(req.params.id)
|
||||
|
||||
if ( !provider || !provider.active )
|
||||
return res.status(404).api()
|
||||
|
||||
if ( !req.user.can(`saml:provider:${provider.id}:update`) )
|
||||
return res.status(401).api()
|
||||
|
||||
const required_fields = ['name', 'entity_id', 'acs_url']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field].trim() )
|
||||
return res.status(400)
|
||||
.message(`Missing required field: ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the entity_id won't cause a collision
|
||||
const duplicate_providers = await ServiceProvider.find({
|
||||
entity_id: req.body.entity_id,
|
||||
_id: { $ne: provider._id },
|
||||
})
|
||||
|
||||
if ( duplicate_providers.length > 0 )
|
||||
return res.status(400)
|
||||
.message('A service provider already exists with that entity_id.')
|
||||
.api()
|
||||
|
||||
// Update the record
|
||||
provider.name = req.body.name
|
||||
provider.entity_id = req.body.entity_id
|
||||
provider.acs_url = req.body.acs_url
|
||||
provider.slo_url = req.body.slo_url
|
||||
|
||||
await provider.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_provider(req, res, next) {
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
const provider = await ServiceProvider.findById(req.params.id)
|
||||
|
||||
if ( !provider || !provider.active )
|
||||
return res.status(404).api()
|
||||
|
||||
if ( !req.user.can(`saml:provider:${provider.id}:delete`) )
|
||||
return res.status(401).api()
|
||||
|
||||
provider.active = false
|
||||
await provider.save()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = SAMLController
|
||||
@@ -2,23 +2,13 @@ const { Controller } = require('libflitter')
|
||||
|
||||
class SAMLController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'cobalt', 'models']
|
||||
return [...super.services, 'cobalt']
|
||||
}
|
||||
|
||||
async get_sp_listing(req, res, next) {
|
||||
const ServiceProvider = this.models.get('saml:ServiceProvider')
|
||||
const service_providers = await ServiceProvider.find()
|
||||
const formatted = service_providers.map(x => {
|
||||
return {
|
||||
name: x.name,
|
||||
entity_id: x.entity_id,
|
||||
acs_url: x.acs_url,
|
||||
has_slo: !!x.slo_url,
|
||||
}
|
||||
})
|
||||
|
||||
return this.cobalt.listing(req, res, {
|
||||
title: 'SAML Service Providers',
|
||||
resource: 'saml/Provider',
|
||||
columns: [
|
||||
{
|
||||
name: 'Provider Name',
|
||||
@@ -38,7 +28,49 @@ class SAMLController extends Controller {
|
||||
field: 'acs_url',
|
||||
},
|
||||
],
|
||||
data: formatted,
|
||||
actions: [
|
||||
{
|
||||
type: 'resource',
|
||||
position: 'main',
|
||||
action: 'insert',
|
||||
text: 'Create New',
|
||||
color: 'success',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async get_sp_form(req, res, next) {
|
||||
return this.cobalt.form(req, res, {
|
||||
item: 'SAML Service Provider',
|
||||
plural: 'SAML Service Providers',
|
||||
resource: 'saml/Provider',
|
||||
...(req.params.id ? { existing_id: req.params.id } : {}),
|
||||
fields: [
|
||||
{
|
||||
name: 'Provider Name',
|
||||
field: 'name',
|
||||
placeholder: 'Awesome External App',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'Entity ID',
|
||||
field: 'entity_id',
|
||||
placeholder: 'https://my.awesome.app/saml/metadata.xml',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'Assertion Consumer Service URL',
|
||||
field: 'acs_url',
|
||||
placeholder: 'https://my.awesome.app/saml/acs',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'Single-Logout URL',
|
||||
field: 'slo_url',
|
||||
placeholder: 'https://my.awesome.app/saml/logout',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user