Flesh out Cobalt, LDAP groups, &c.
This commit is contained in:
parent
c389e151b5
commit
6f621f5891
198
app/assets/app/cobalt/Form.component.js
Normal file
198
app/assets/app/cobalt/Form.component.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import { Component } from '../../lib/vues6/vues6.js'
|
||||||
|
import { utility } from '../service/Utility.service.js'
|
||||||
|
import { location_service } from '../service/Location.service.js'
|
||||||
|
import { resource_service } from '../service/Resource.service.js'
|
||||||
|
import { action_service } from '../service/Action.service.js'
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="card col-12 col-sm-10 offset-sm-1 col-md-8 offset-md-2 col-xl-6 offset-xl-3">
|
||||||
|
<h3 class="card-title mb-4 mt-4">
|
||||||
|
<button class="btn-lg btn" @click="back"><i class="fa fa-arrow-left"></i></button> {{ title }}
|
||||||
|
</h3>
|
||||||
|
<form v-on:submit.prevent="do_nothing" v-if="is_ready">
|
||||||
|
<div class="form-group" v-for="field of definition.fields">
|
||||||
|
<span v-if="field.type.startsWith('select')">
|
||||||
|
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||||
|
<select
|
||||||
|
:id="uuid+field.field"
|
||||||
|
class="form-control"
|
||||||
|
v-model="data[field.field]"
|
||||||
|
:required="field.required"
|
||||||
|
:readonly="mode === 'view'"
|
||||||
|
:multiple="!!field.type.endsWith('.multiple')"
|
||||||
|
>
|
||||||
|
<option v-for="option of field.options" :value="option.value">{{ typeof option.display === 'function' ? option.display(option) : option.display }}</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||||
|
</span>
|
||||||
|
<span v-if="field.type === 'text'">
|
||||||
|
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:id="uuid+field.field"
|
||||||
|
v-model="data[field.field]"
|
||||||
|
:required="field.required"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
:readonly="mode === 'view'"
|
||||||
|
ref="input"
|
||||||
|
>
|
||||||
|
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="col-12 text-right mb-4 mr-0 mt-2">
|
||||||
|
<span style="color: darkred;" class="font-italic mr-3" v-if="error_message">{{ error_message }}</span>
|
||||||
|
<span class="font-italic mr-3 text-muted" v-if="other_message">{{ other_message }}</span>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
v-if="mode !== 'view'"
|
||||||
|
@click="save_click"
|
||||||
|
>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
const title_map = {
|
||||||
|
insert: 'Create',
|
||||||
|
update: 'Edit',
|
||||||
|
view: 'View',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FormComponent extends Component {
|
||||||
|
static get selector() {
|
||||||
|
return 'cobalt-form'
|
||||||
|
}
|
||||||
|
|
||||||
|
static get template() {
|
||||||
|
return template
|
||||||
|
}
|
||||||
|
|
||||||
|
static get props() {
|
||||||
|
return ['resource', 'form_id', 'initial_mode']
|
||||||
|
}
|
||||||
|
|
||||||
|
definition = {}
|
||||||
|
data = {}
|
||||||
|
uuid = ''
|
||||||
|
title = ''
|
||||||
|
error_message = ''
|
||||||
|
other_message = ''
|
||||||
|
|
||||||
|
is_ready = false
|
||||||
|
mode = ''
|
||||||
|
id = ''
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.definition = {}
|
||||||
|
this.data = {}
|
||||||
|
this.uuid = ''
|
||||||
|
this.title = ''
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
this.is_ready = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async vue_on_create(internal = false) {
|
||||||
|
if ( !internal ) {
|
||||||
|
this.mode = this.initial_mode
|
||||||
|
this.id = this.form_id
|
||||||
|
this.resource_class = await resource_service.get(this.resource)
|
||||||
|
} else {
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uuid = utility.uuid()
|
||||||
|
await this.load()
|
||||||
|
await this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
for ( const field of this.definition.fields ) {
|
||||||
|
if ( field.type.startsWith('select.dynamic') ) {
|
||||||
|
field._options = field._options || field.options
|
||||||
|
const rsc = await resource_service.get(field._options.resource)
|
||||||
|
|
||||||
|
field.options = (await rsc.list()).map(item => {
|
||||||
|
return {
|
||||||
|
display: typeof field._options.display === 'function' ? field._options.display(item) : item[field._options.display || 'display'],
|
||||||
|
value: typeof field._options.value === 'function' ? field._options.value(item) : item[field._options.value || 'display'],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( field.type.endsWith('.multiple') ) {
|
||||||
|
this.data[field.field] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.is_ready = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if ( this.mode !== 'view' ) this.$refs.input[0].focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.definition = this.resource_class.form_definition
|
||||||
|
if (this.mode !== 'insert') {
|
||||||
|
this.data = await this.resource_class.get(this.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.title = title_map[this.mode] + ' ' + this.resource_class.item
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_create() {
|
||||||
|
this.id = this.data.id
|
||||||
|
this.mode = 'update'
|
||||||
|
await this.vue_on_create(true)
|
||||||
|
location_service.set_query(`mode=update&id=${this.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async save_click() {
|
||||||
|
if ( !this.validate() ) return
|
||||||
|
try {
|
||||||
|
if (this.mode === 'insert') {
|
||||||
|
this.data = await this.resource_class.create(this.data)
|
||||||
|
await this.on_create()
|
||||||
|
} else if (this.mode === 'update') {
|
||||||
|
await this.resource_class.update(this.id, this.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = `The ${this.resource_class.item} was saved.`
|
||||||
|
} catch (e) {
|
||||||
|
if ( e.response && e.response.data && e.response.data.message )
|
||||||
|
this.error_message = e.response.data.message
|
||||||
|
else this.error_message = 'An unknown error occurred while saving the form.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
let valid = true
|
||||||
|
for ( const field of this.definition.fields ) {
|
||||||
|
if ( field.required && (!(field.field in this.data) || !this.data[field.field]) ) {
|
||||||
|
field.error = 'This field is required.'
|
||||||
|
valid = false
|
||||||
|
} else {
|
||||||
|
field.error = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$forceUpdate()
|
||||||
|
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
back() {
|
||||||
|
return action_service.perform({
|
||||||
|
text: '',
|
||||||
|
type: 'resource',
|
||||||
|
resource: this.resource,
|
||||||
|
action: 'list',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
do_nothing() {}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,45 @@
|
|||||||
import { Component } from '../../lib/vues6/vues6.js'
|
import { Component } from '../../lib/vues6/vues6.js'
|
||||||
|
import { action_service } from '../service/Action.service.js'
|
||||||
|
import { message_service } from '../service/Message.service.js'
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div>
|
<div>
|
||||||
<h2 class="mb-4" v-if="definition.title">{{ definition.title }}</h2>
|
<div class="row mb-4">
|
||||||
|
<div class="col-10"><h3>{{ resource_class.plural }}</h3></div>
|
||||||
|
<div class="col-2 text-right" v-if="definition.actions">
|
||||||
|
<button
|
||||||
|
:class="['btn', 'btn-'+(action.color || 'secondary'), 'btn-sm']"
|
||||||
|
type="button"
|
||||||
|
v-for="action of definition.actions"
|
||||||
|
@click="perform($event, action)"
|
||||||
|
v-if="action.position === 'main'"
|
||||||
|
>{{ action.text }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">#</th>
|
<th scope="col">#</th>
|
||||||
<th scope="col" v-for="col of definition.columns">{{ col.name }}</th>
|
<th scope="col" v-for="col of definition.columns">{{ col.name }}</th>
|
||||||
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(row, index) of definition.data">
|
<tr v-for="(row, index) of data">
|
||||||
<th scope="row">{{ index + 1 }}</th>
|
<th scope="row">{{ index + 1 }}</th>
|
||||||
<td v-for="col of definition.columns">
|
<td v-for="col of definition.columns">
|
||||||
<span v-if="col.renderer === 'boolean'">{{ col.field ? 'Yes' : 'No' }}</span>
|
<span v-if="typeof col.renderer === 'function'">{{ col.renderer(row[col.field]) }}</span>
|
||||||
<span v-if="col.renderer !== 'boolean'">{{ col.field in row ? row[col.field] : '-' }}</span>
|
<span v-if="col.renderer === 'boolean'">{{ row[col.field] ? 'Yes' : 'No' }}</span>
|
||||||
|
<span v-if="col.renderer !== 'boolean' && typeof col.renderer !== 'function'">{{ col.field in row ? row[col.field] : '-' }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="['mr-2', 'btn', 'btn-sm', 'btn-'+(action.color || 'secondary')]"
|
||||||
|
v-for="action of definition.actions"
|
||||||
|
v-if="action.position === 'row'"
|
||||||
|
@click="perform($event, action, row)"
|
||||||
|
><i :class="action.icon" v-if="action.icon"></i> {{ action.text }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -26,5 +50,55 @@ const template = `
|
|||||||
export default class ListingComponent extends Component {
|
export default class ListingComponent extends Component {
|
||||||
static get selector() { return 'cobalt-listing' }
|
static get selector() { return 'cobalt-listing' }
|
||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return ['definition'] }
|
static get props() { return ['resource'] }
|
||||||
|
|
||||||
|
definition = {}
|
||||||
|
data = []
|
||||||
|
resource_class = {}
|
||||||
|
|
||||||
|
async vue_on_create() {
|
||||||
|
// Load the resource
|
||||||
|
const resource_mod = await import(`../resource/${this.resource}.resource.js`)
|
||||||
|
if ( !resource_mod )
|
||||||
|
throw new Error('Unable to load Cobalt listing resource.')
|
||||||
|
|
||||||
|
const rsc_name = this.resource.toLowerCase().replace(/\//g, '_')
|
||||||
|
if ( !resource_mod[rsc_name] )
|
||||||
|
throw new Error('Unable to extract resource object from module.')
|
||||||
|
|
||||||
|
this.resource_class = resource_mod[rsc_name]
|
||||||
|
await this.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.definition = this.resource_class.listing_definition
|
||||||
|
this.data = await this.resource_class.list()
|
||||||
|
}
|
||||||
|
|
||||||
|
async perform($event, action, row = undefined) {
|
||||||
|
if ( action.confirm ) {
|
||||||
|
message_service.modal({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message: `You are about to ${action.action}${row ? ' this '+this.resource_class.item : ''}. Do you want to continue?`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
type: 'close',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Continue',
|
||||||
|
class: ['btn', 'btn-primary'],
|
||||||
|
type: 'close',
|
||||||
|
on_click: async () => {
|
||||||
|
await action_service.perform({...action, resource: this.resource, ...(row ? {id: row.id} : {})})
|
||||||
|
await this.load()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await action_service.perform({...action, resource: this.resource, ...(row ? {id: row.id} : {})})
|
||||||
|
await this.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import EditProfileComponent from './dash/profile/EditProfile.component.js'
|
|||||||
import AppPasswordFormComponent from './dash/profile/form/AppPassword.component.js'
|
import AppPasswordFormComponent from './dash/profile/form/AppPassword.component.js'
|
||||||
|
|
||||||
import ListingComponent from './cobalt/Listing.component.js'
|
import ListingComponent from './cobalt/Listing.component.js'
|
||||||
|
import FormComponent from './cobalt/Form.component.js'
|
||||||
|
|
||||||
const dash_components = {
|
const dash_components = {
|
||||||
SideBarComponent,
|
SideBarComponent,
|
||||||
@ -14,6 +15,7 @@ const dash_components = {
|
|||||||
AppPasswordFormComponent,
|
AppPasswordFormComponent,
|
||||||
|
|
||||||
ListingComponent,
|
ListingComponent,
|
||||||
|
FormComponent,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { dash_components }
|
export { dash_components }
|
||||||
|
@ -37,17 +37,17 @@ export default class SideBarComponent extends Component {
|
|||||||
{
|
{
|
||||||
text: 'Groups',
|
text: 'Groups',
|
||||||
action: 'redirect',
|
action: 'redirect',
|
||||||
next: '/dash/groups',
|
next: '/dash/c/listing/ldap/Group',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'LDAP Clients',
|
text: 'LDAP Clients',
|
||||||
action: 'redirect',
|
action: 'redirect',
|
||||||
next: '/dash/ldap/clients',
|
next: '/dash/c/listing/ldap/Client',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'SAML Service Providers',
|
text: 'SAML Service Providers',
|
||||||
action: 'redirect',
|
action: 'redirect',
|
||||||
next: '/dash/saml/service-providers',
|
next: '/dash/c/listing/saml/Provider',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Settings',
|
text: 'Settings',
|
||||||
|
@ -26,7 +26,7 @@ const template = `
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
ref="modal"
|
ref="modal"
|
||||||
>
|
>
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">{{ modal.title }}</h5>
|
<h5 class="modal-title">{{ modal.title }}</h5>
|
||||||
|
5
app/assets/app/resource/APIParseError.js
Normal file
5
app/assets/app/resource/APIParseError.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default class APIParseError extends Error {
|
||||||
|
constructor(msg = 'An unknown error occurred while parsing the API response.') {
|
||||||
|
super(msg)
|
||||||
|
}
|
||||||
|
}
|
55
app/assets/app/resource/CRUDBase.js
Normal file
55
app/assets/app/resource/CRUDBase.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import APIParseError from './APIParseError.js'
|
||||||
|
|
||||||
|
export default class CRUDBase {
|
||||||
|
endpoint = '/api/v1'
|
||||||
|
required_fields = []
|
||||||
|
|
||||||
|
listing_definition = {}
|
||||||
|
form_definition = {}
|
||||||
|
|
||||||
|
item = ''
|
||||||
|
plural = ''
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
const results = await axios.get(this._endpoint())
|
||||||
|
if ( results && results.data && Array.isArray(results.data.data) ) return results.data.data
|
||||||
|
else throw new APIParseError()
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id) {
|
||||||
|
const results = await axios.get(this._endpoint(id))
|
||||||
|
if ( results && results.data && results.data.data ) return results.data.data
|
||||||
|
else throw new APIParseError()
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(properties) {
|
||||||
|
for ( const field of this.required_fields ) {
|
||||||
|
if ( !properties[field] ) throw new Error(`Missing required field: ${field}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await axios.post(this._endpoint(), properties)
|
||||||
|
if ( results && results.data && results.data.data ) return results.data.data
|
||||||
|
else throw new APIParseError()
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, properties) {
|
||||||
|
for ( const field of this.required_fields ) {
|
||||||
|
if ( !properties[field] ) throw new Error(`Missing required field: ${field}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.patch(this._endpoint(id), properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
await axios.delete(this._endpoint(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
_endpoint(sub = '/') {
|
||||||
|
let first = this.endpoint
|
||||||
|
if ( !first.startsWith('/') ) first = `/${first}`
|
||||||
|
if ( first.endsWith('/') ) first = first.slice(0, -1)
|
||||||
|
|
||||||
|
if ( !sub.startsWith('/') ) sub = `/${sub}`
|
||||||
|
return `${first}${sub}`
|
||||||
|
}
|
||||||
|
}
|
12
app/assets/app/resource/auth/Role.resource.js
Normal file
12
app/assets/app/resource/auth/Role.resource.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
|
class RoleResource extends CRUDBase {
|
||||||
|
endpoint = '/api/v1/auth/roles'
|
||||||
|
required_fields = ['role', 'permissions']
|
||||||
|
|
||||||
|
item = 'Role'
|
||||||
|
plural = 'Roles'
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth_role = new RoleResource()
|
||||||
|
export { auth_role }
|
12
app/assets/app/resource/auth/User.resource.js
Normal file
12
app/assets/app/resource/auth/User.resource.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
|
class UserResource extends CRUDBase {
|
||||||
|
endpoint = '/api/v1/auth/users'
|
||||||
|
required_fields = ['uid', 'first_name', 'last_name', 'email', 'password']
|
||||||
|
|
||||||
|
item = 'User'
|
||||||
|
plural = 'Users'
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth_user = new UserResource()
|
||||||
|
export { auth_user }
|
66
app/assets/app/resource/ldap/Client.resource.js
Normal file
66
app/assets/app/resource/ldap/Client.resource.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
|
class ClientResource extends CRUDBase {
|
||||||
|
endpoint = '/api/v1/ldap/clients'
|
||||||
|
required_fields = ['name', 'uid', 'password']
|
||||||
|
|
||||||
|
item = 'LDAP Client'
|
||||||
|
plural = 'LDAP Clients'
|
||||||
|
|
||||||
|
listing_definition = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'Client Name',
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User ID',
|
||||||
|
field: 'uid',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'resource',
|
||||||
|
position: 'main',
|
||||||
|
action: 'insert',
|
||||||
|
text: 'Create New',
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'resource',
|
||||||
|
position: 'row',
|
||||||
|
action: 'update',
|
||||||
|
icon: 'fa fa-edit',
|
||||||
|
color: 'primary',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
form_definition = {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'Provider Name',
|
||||||
|
field: 'name',
|
||||||
|
placeholder: 'Awesome External App',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User ID',
|
||||||
|
field: 'uid',
|
||||||
|
placeholder: 'some_username',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Password',
|
||||||
|
field: 'password',
|
||||||
|
required: ['insert'],
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ldap_client = new ClientResource()
|
||||||
|
export { ldap_client }
|
98
app/assets/app/resource/ldap/Group.resource.js
Normal file
98
app/assets/app/resource/ldap/Group.resource.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
|
class GroupResource extends CRUDBase {
|
||||||
|
endpoint = '/api/v1/ldap/groups'
|
||||||
|
required_fields = ['name', 'role']
|
||||||
|
|
||||||
|
item = 'LDAP Group'
|
||||||
|
plural = 'LDAP Groups'
|
||||||
|
|
||||||
|
listing_definition = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'Group Name',
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Role',
|
||||||
|
field: 'role',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '# of Users',
|
||||||
|
field: 'user_ids',
|
||||||
|
renderer: (user_ids) => Array.isArray(user_ids) ? user_ids.length : 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'resource',
|
||||||
|
position: 'main',
|
||||||
|
action: 'insert',
|
||||||
|
text: 'Create New',
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'resource',
|
||||||
|
position: 'row',
|
||||||
|
action: 'update',
|
||||||
|
icon: 'fa fa-edit',
|
||||||
|
color: 'primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'resource',
|
||||||
|
position: 'row',
|
||||||
|
action: 'delete',
|
||||||
|
icon: 'fa fa-times',
|
||||||
|
color: 'danger',
|
||||||
|
confirm: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
form_definition = {
|
||||||
|
// back_action: {
|
||||||
|
// text: 'Back',
|
||||||
|
// action: 'back',
|
||||||
|
// },
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'Group Name',
|
||||||
|
field: 'name',
|
||||||
|
placeholder: 'External App Users',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Role',
|
||||||
|
field: 'role',
|
||||||
|
placeholder: 'external_app',
|
||||||
|
required: true,
|
||||||
|
type: 'select.dynamic',
|
||||||
|
options: {
|
||||||
|
resource: 'auth/Role',
|
||||||
|
display: 'role',
|
||||||
|
value: 'role',
|
||||||
|
},
|
||||||
|
// options: [
|
||||||
|
// { value: 1, display: 'One' },
|
||||||
|
// { value: 2, display: 'Two' },
|
||||||
|
// { value: 3, display: 'Three' },
|
||||||
|
// ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Users',
|
||||||
|
field: 'user_ids',
|
||||||
|
placeholder: 'John Doe',
|
||||||
|
type: 'select.dynamic.multiple',
|
||||||
|
options: {
|
||||||
|
resource: 'auth/User',
|
||||||
|
display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`,
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ldap_group = new GroupResource()
|
||||||
|
export { ldap_group }
|
82
app/assets/app/resource/saml/Provider.resource.js
Normal file
82
app/assets/app/resource/saml/Provider.resource.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
|
class ProviderResource extends CRUDBase {
|
||||||
|
endpoint = '/api/v1/saml/providers'
|
||||||
|
required_fields = ['name', 'acs_url', 'entity_id']
|
||||||
|
|
||||||
|
item = 'SAML Service Provider'
|
||||||
|
plural = 'SAML Service Providers'
|
||||||
|
|
||||||
|
listing_definition = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'Provider Name',
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Entity ID',
|
||||||
|
field: 'entity_id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Has SLO?',
|
||||||
|
field: 'slo_url',
|
||||||
|
renderer: 'boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ACS URL',
|
||||||
|
field: 'acs_url',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'resource',
|
||||||
|
position: 'main',
|
||||||
|
action: 'insert',
|
||||||
|
text: 'Create New',
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'resource',
|
||||||
|
position: 'row',
|
||||||
|
action: 'update',
|
||||||
|
icon: 'fa fa-edit',
|
||||||
|
color: 'primary',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
form_definition = {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'Provider Name',
|
||||||
|
field: 'name',
|
||||||
|
placeholder: 'Awesome External App',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Entity ID',
|
||||||
|
field: 'entity_id',
|
||||||
|
placeholder: 'https://my.awesome.app/saml/metadata.xml',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Assertion Consumer Service URL',
|
||||||
|
field: 'acs_url',
|
||||||
|
placeholder: 'https://my.awesome.app/saml/acs',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Single-Logout URL',
|
||||||
|
field: 'slo_url',
|
||||||
|
placeholder: 'https://my.awesome.app/saml/logout',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saml_provider = new ProviderResource()
|
||||||
|
export { saml_provider }
|
@ -1,10 +1,27 @@
|
|||||||
import { location_service } from './Location.service.js'
|
import { location_service } from './Location.service.js'
|
||||||
|
import { resource_service } from './Resource.service.js'
|
||||||
|
|
||||||
class ActionService {
|
class ActionService {
|
||||||
async perform({ text, action, ...args }) {
|
async perform({ text = '', action, ...args }) {
|
||||||
if ( action === 'redirect' ) {
|
if ( action === 'redirect' ) {
|
||||||
if ( args.next ) {
|
if ( args.next ) {
|
||||||
return location_service.redirect(args.next, args.delay || 1500)
|
return location_service.redirect(args.next, args.delay || 0)
|
||||||
|
}
|
||||||
|
} else if ( action === 'back' ) {
|
||||||
|
return location_service.back()
|
||||||
|
} else if ( args.type === 'resource' ) {
|
||||||
|
const { resource } = args
|
||||||
|
if ( action === 'insert' ) {
|
||||||
|
return location_service.redirect(`/dash/c/form/${resource}`, 0)
|
||||||
|
} else if ( action === 'update' ) {
|
||||||
|
const { id } = args
|
||||||
|
return location_service.redirect(`/dash/c/form/${resource}?id=${id}`, 0)
|
||||||
|
} else if ( action === 'delete' ) {
|
||||||
|
const { id } = args
|
||||||
|
const rsc = await resource_service.get(resource)
|
||||||
|
await rsc.delete(id)
|
||||||
|
} else if ( action === 'list' ) {
|
||||||
|
return location_service.redirect(`/dash/c/listing/${resource}`, 0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError(`Unknown action type: ${action}`)
|
throw new TypeError(`Unknown action type: ${action}`)
|
||||||
|
@ -20,6 +20,13 @@ class LocationService {
|
|||||||
}, delay)
|
}, delay)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_query(query) {
|
||||||
|
if (history.pushState) {
|
||||||
|
const new_url = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + query;
|
||||||
|
window.history.pushState({path: new_url}, '', new_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const location_service = new LocationService()
|
const location_service = new LocationService()
|
||||||
|
18
app/assets/app/service/Resource.service.js
Normal file
18
app/assets/app/service/Resource.service.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
class ResourceService {
|
||||||
|
async get(name) {
|
||||||
|
const resource_mod = await import(`../resource/${name}.resource.js`)
|
||||||
|
if ( !resource_mod ) throw new Error(`Unable to fetch resource ${name}.`)
|
||||||
|
|
||||||
|
if ( !this.object_name(name) in resource_mod )
|
||||||
|
throw new Error(`Unable to retrieve resource from module (${this.object_name(name)}).`)
|
||||||
|
|
||||||
|
return resource_mod[this.object_name(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
object_name(name) {
|
||||||
|
return name.toLowerCase().replace(/\//g, '_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource_service = new ResourceService()
|
||||||
|
export { resource_service }
|
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 {
|
class AuthController extends Controller {
|
||||||
static get services() {
|
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) {
|
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 {
|
class SAMLController extends Controller {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'cobalt', 'models']
|
return [...super.services, 'cobalt']
|
||||||
}
|
}
|
||||||
|
|
||||||
async get_sp_listing(req, res, next) {
|
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, {
|
return this.cobalt.listing(req, res, {
|
||||||
title: 'SAML Service Providers',
|
title: 'SAML Service Providers',
|
||||||
|
resource: 'saml/Provider',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'Provider Name',
|
name: 'Provider Name',
|
||||||
@ -38,7 +28,49 @@ class SAMLController extends Controller {
|
|||||||
field: 'acs_url',
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,17 @@ class User extends AuthUser {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async to_api() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
uid: this.uid,
|
||||||
|
first_name: this.first_name,
|
||||||
|
last_name: this.last_name,
|
||||||
|
email: this.email,
|
||||||
|
tagline: this.tagline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static scopes = [
|
static scopes = [
|
||||||
new ActiveScope({})
|
new ActiveScope({})
|
||||||
]
|
]
|
||||||
|
60
app/models/ldap/Client.model.js
Normal file
60
app/models/ldap/Client.model.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
const { Model } = require('flitter-orm')
|
||||||
|
|
||||||
|
class ClientModel extends Model {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'models', 'configs']
|
||||||
|
}
|
||||||
|
|
||||||
|
static get schema() {
|
||||||
|
return {
|
||||||
|
name: String,
|
||||||
|
user_id: String,
|
||||||
|
active: { type: Boolean, default: true },
|
||||||
|
last_invocation: Date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create({ name, uid, password }) {
|
||||||
|
const User = this.prototype.models.get('auth:User')
|
||||||
|
const user = new User({
|
||||||
|
first_name: name,
|
||||||
|
last_name: '(LDAP Agent)',
|
||||||
|
uid,
|
||||||
|
roles: ['ldap_client'],
|
||||||
|
})
|
||||||
|
|
||||||
|
await user.reset_password(password, 'create')
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
const client = new this({
|
||||||
|
name,
|
||||||
|
user_id: user.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.save()
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
async user() {
|
||||||
|
const User = this.models.get('auth:User')
|
||||||
|
return User.findById(this.user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async to_api() {
|
||||||
|
const User = this.models.get('auth:User')
|
||||||
|
const user = await User.findById(this.user_id)
|
||||||
|
|
||||||
|
const role_permissions = user.roles.map(x => this.configs.get('auth.roles')[x])
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
user_id: user.id,
|
||||||
|
uid: user.uid,
|
||||||
|
last_invocation: this.last_invocation,
|
||||||
|
permissions: [...user.permissions, ...role_permissions],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = ClientModel
|
@ -11,10 +11,21 @@ class GroupModel extends LDAPBase {
|
|||||||
role: String,
|
role: String,
|
||||||
user_ids: [String],
|
user_ids: [String],
|
||||||
name: String,
|
name: String,
|
||||||
|
active: {type: Boolean, default: true},
|
||||||
ldap_visible: {type: Boolean, default: true},
|
ldap_visible: {type: Boolean, default: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async to_api() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
role: this.role,
|
||||||
|
user_ids: this.user_ids,
|
||||||
|
name: this.name,
|
||||||
|
ldap_visible: this.ldap_visible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get dn() {
|
get dn() {
|
||||||
return LDAP.parseDN(`cn=${this.name},${this.ldap_server.group_dn().format(this.configs.get('ldap:server.format'))}`)
|
return LDAP.parseDN(`cn=${this.name},${this.ldap_server.group_dn().format(this.configs.get('ldap:server.format'))}`)
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,16 @@ class ServiceProviderModel extends Model {
|
|||||||
slo_url: String,
|
slo_url: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
to_api() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
entity_id: this.entity_id,
|
||||||
|
acs_url: this.acs_url,
|
||||||
|
slo_url: this.slo_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = ServiceProviderModel
|
module.exports = exports = ServiceProviderModel
|
||||||
|
14
app/routing/middleware/api/Permission.middleware.js
Normal file
14
app/routing/middleware/api/Permission.middleware.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const { Middleware } = require('libflitter')
|
||||||
|
|
||||||
|
class PermissionMiddleware extends Middleware {
|
||||||
|
async test(req, res, next, { check }) {
|
||||||
|
if ( !req.user.can(check) )
|
||||||
|
return res.status(401)
|
||||||
|
.message('Insufficient permissions.')
|
||||||
|
.api()
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = PermissionMiddleware
|
@ -7,6 +7,15 @@ const auth_routes = {
|
|||||||
|
|
||||||
get: {
|
get: {
|
||||||
'/mfa/enable/date': ['middleware::auth:UserOnly', 'controller::api:v1:Auth.get_mfa_enable_date'],
|
'/mfa/enable/date': ['middleware::auth:UserOnly', 'controller::api:v1:Auth.get_mfa_enable_date'],
|
||||||
|
|
||||||
|
'/roles': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:auth:roles:list' }],
|
||||||
|
'controller::api:v1:Auth.get_roles',
|
||||||
|
],
|
||||||
|
'/users': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:auth:users:list' }],
|
||||||
|
'controller::api:v1:Auth.get_users',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
post: {
|
post: {
|
||||||
|
61
app/routing/routers/api/v1/ldap.routes.js
Normal file
61
app/routing/routers/api/v1/ldap.routes.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
const ldap_routes = {
|
||||||
|
prefix: '/api/v1/ldap',
|
||||||
|
|
||||||
|
middleware: [
|
||||||
|
'auth:UserOnly',
|
||||||
|
],
|
||||||
|
|
||||||
|
get: {
|
||||||
|
'/clients': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:clients:list' }],
|
||||||
|
'controller::api:v1:LDAP.get_clients',
|
||||||
|
],
|
||||||
|
'/clients/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:clients:get' }],
|
||||||
|
'controller::api:v1:LDAP.get_client',
|
||||||
|
],
|
||||||
|
'/groups': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:groups:list' }],
|
||||||
|
'controller::api:v1:LDAP.get_groups',
|
||||||
|
],
|
||||||
|
'/groups/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:groups:get' }],
|
||||||
|
'controller::api:v1:LDAP.get_group',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
post: {
|
||||||
|
'/clients': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:clients:create' }],
|
||||||
|
'controller::api:v1:LDAP.create_client',
|
||||||
|
],
|
||||||
|
'/groups': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:groups:create' }],
|
||||||
|
'controller::api:v1:LDAP.create_group',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
patch: {
|
||||||
|
'/clients/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:clients:update' }],
|
||||||
|
'controller::api:v1:LDAP.update_client',
|
||||||
|
],
|
||||||
|
'/groups/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:groups:update' }],
|
||||||
|
'controller::api:v1:LDAP.update_group',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: {
|
||||||
|
'/clients/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:clients:delete' }],
|
||||||
|
'controller::api:v1:LDAP.delete_client',
|
||||||
|
],
|
||||||
|
'/groups/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:ldap:groups:delete' }],
|
||||||
|
'controller::api:v1:LDAP.delete_group',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = ldap_routes
|
41
app/routing/routers/api/v1/saml.routes.js
Normal file
41
app/routing/routers/api/v1/saml.routes.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const saml_routes = {
|
||||||
|
prefix: '/api/v1/saml',
|
||||||
|
|
||||||
|
middleware: [
|
||||||
|
'auth:UserOnly',
|
||||||
|
],
|
||||||
|
|
||||||
|
get: {
|
||||||
|
'/providers': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:saml:providers:list' }],
|
||||||
|
'controller::api:v1:SAML.get_providers',
|
||||||
|
],
|
||||||
|
'/providers/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:saml:providers:get' }],
|
||||||
|
'controller::api:v1:SAML.get_provider',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
post: {
|
||||||
|
'/providers': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:saml:providers:create' }],
|
||||||
|
'controller::api:v1:SAML.create_provider',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
patch: {
|
||||||
|
'/providers/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:saml:providers:update' }],
|
||||||
|
'controller::api:v1:SAML.update_provider',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: {
|
||||||
|
'/providers/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:saml:providers:delete' }],
|
||||||
|
'controller::api:v1:SAML.delete_provider',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = saml_routes
|
18
app/routing/routers/dash/cobalt.routes.js
Normal file
18
app/routing/routers/dash/cobalt.routes.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const cobalt_routes = {
|
||||||
|
prefix: '/dash/c',
|
||||||
|
|
||||||
|
middleware: [
|
||||||
|
'auth:UserOnly',
|
||||||
|
],
|
||||||
|
|
||||||
|
get: {
|
||||||
|
'/listing/:resource*': [
|
||||||
|
'controller::Cobalt.listing',
|
||||||
|
],
|
||||||
|
'/form/:resource*': [
|
||||||
|
'controller::Cobalt.form',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = cobalt_routes
|
@ -5,9 +5,23 @@ class CobaltService extends Service {
|
|||||||
return [...super.services, 'Vue']
|
return [...super.services, 'Vue']
|
||||||
}
|
}
|
||||||
|
|
||||||
listing(req, res, { title = '', columns, data }) {
|
listing(req, res, { title = '', columns, data = [], resource = '', actions = [] }) {
|
||||||
return res.page('cobalt:listing', {
|
return res.page('cobalt:listing', {
|
||||||
...this.Vue.data({ definition: { title, columns, data } }),
|
...this.Vue.data({ definition: { title, columns, data, resource, actions } }),
|
||||||
|
...this.Vue.session(req),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
form(req, res, { item, plural = undefined, resource, fields }) {
|
||||||
|
return res.page('cobalt:form', {
|
||||||
|
...this.Vue.data({
|
||||||
|
definition: {
|
||||||
|
item,
|
||||||
|
plural: plural ?? `${item}s`,
|
||||||
|
resource,
|
||||||
|
fields,
|
||||||
|
}
|
||||||
|
}),
|
||||||
...this.Vue.session(req),
|
...this.Vue.session(req),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const Unit = require('libflitter/Unit')
|
const Unit = require('libflitter/Unit')
|
||||||
const LDAP = require('ldapjs')
|
const LDAP = require('ldapjs')
|
||||||
const Validator = require('email-validator')
|
const Validator = require('email-validator')
|
||||||
|
const net = require('net')
|
||||||
|
|
||||||
// TODO support logging ALL ldap requests when in DEBUG, not just routed ones
|
// TODO support logging ALL ldap requests when in DEBUG, not just routed ones
|
||||||
// TODO need to support LDAP server auto-discovery/detection features
|
// TODO need to support LDAP server auto-discovery/detection features
|
||||||
@ -89,12 +90,28 @@ class LDAPServerUnit extends Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.output.info(`Will listen on ${this.config.interface}:${this.config.port}`)
|
this.output.info(`Will listen on ${this.config.interface}:${this.config.port}`)
|
||||||
|
if ( await this.port_free() ) {
|
||||||
await new Promise((res, rej) => {
|
await new Promise((res, rej) => {
|
||||||
this.server.listen(this.config.port, this.config.interface, () => {
|
this.server.listen(this.config.port, this.config.interface, (err) => {
|
||||||
this.output.success(`LDAP server listening on port ${this.config.port}...`)
|
this.output.success(`LDAP server listening on port ${this.config.port}...`)
|
||||||
res()
|
res()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
this.output.error(`LDAP server port ${this.config.port} is not available. The LDAP server was not started.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async port_free() {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
const server = net.createServer()
|
||||||
|
server.once('error', rej)
|
||||||
|
server.once('listening', () => {
|
||||||
|
server.close()
|
||||||
|
res()
|
||||||
|
})
|
||||||
|
server.listen(this.config.port)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
8
app/views/cobalt/form.pug
Normal file
8
app/views/cobalt/form.pug
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
extends ../theme/dash/base
|
||||||
|
|
||||||
|
block content
|
||||||
|
.cobalt-container
|
||||||
|
.row.pad-top
|
||||||
|
.col-12
|
||||||
|
cobalt-form(v-if="form_id" :resource="resource" :form_id="form_id" :initial_mode="mode")
|
||||||
|
cobalt-form(v-if="!form_id" :resource="resource" :initial_mode="mode")
|
@ -4,4 +4,4 @@ block content
|
|||||||
.cobalt-container
|
.cobalt-container
|
||||||
.row.pad-top
|
.row.pad-top
|
||||||
.col-12
|
.col-12
|
||||||
cobalt-listing(:definition="definition")
|
cobalt-listing(:resource="resource")
|
||||||
|
@ -175,6 +175,7 @@ const auth_config = {
|
|||||||
// Then, users with that role will automatically inherit the permissions.
|
// Then, users with that role will automatically inherit the permissions.
|
||||||
ldap_admin: ['ldap'],
|
ldap_admin: ['ldap'],
|
||||||
coreid_base: ['my:profile'],
|
coreid_base: ['my:profile'],
|
||||||
|
saml_admin: ['v1:saml', 'saml'],
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user