Implement OAuth2 server, link oauth:Client and auth::Oauth2Client, implement permission checks
This commit is contained in:
@@ -40,7 +40,6 @@ export default class MFADisableComponent extends Component {
|
||||
|
||||
vue_on_create() {
|
||||
this.app_name = session.get('app.name')
|
||||
console.log({session})
|
||||
}
|
||||
|
||||
async back_click() {
|
||||
|
||||
@@ -5,52 +5,111 @@ 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 class="card col-12 col-sm-10 offset-sm-1 col-md-8 offset-md-2 col-xl-6 offset-xl-3 mb-5">
|
||||
<span v-if="!can_access">
|
||||
<div class="row m-5">
|
||||
<div class="col-12 text-center">
|
||||
<h5>{{ access_msg }}</h5>
|
||||
</div>
|
||||
</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>
|
||||
</span>
|
||||
<span v-if="can_access">
|
||||
<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>
|
||||
<div class="row" v-if="!is_ready">
|
||||
<div class="col-12 text-center pad-top mb-5">
|
||||
<h4>Loading...</h4>
|
||||
</div>
|
||||
</div>
|
||||
<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 === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"
|
||||
v-html="typeof field.display === 'function' ? field.display(data) : field.display"
|
||||
></span>
|
||||
<span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<select
|
||||
:id="uuid+field.field"
|
||||
class="form-control"
|
||||
v-model="data[field.field]"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
:multiple="!!field.type.endsWith('.multiple')"
|
||||
ref="input"
|
||||
>
|
||||
<option
|
||||
v-for="option of field.options"
|
||||
:value="option.value"
|
||||
:selected="data[field.field] && data[field.field].includes(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' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:id="uuid+field.field"
|
||||
v-model="data[field.field]"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:placeholder="field.placeholder"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
ref="input"
|
||||
>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
<span v-if="field.type === 'textarea' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
:id="uuid+field.field"
|
||||
v-model="data[field.field]"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:placeholder="field.placeholder"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
ref="input"
|
||||
></textarea>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
<span v-if="field.type === 'password' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
:id="uuid+field.field"
|
||||
v-model="data[field.field]"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:placeholder="field.placeholder"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
ref="input"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
:id="uuid+field.field+'-confirm'"
|
||||
v-model="data[field.field+'-confirm']"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:placeholder="'Confirm ' + field.name"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -80,6 +139,9 @@ export default class FormComponent extends Component {
|
||||
error_message = ''
|
||||
other_message = ''
|
||||
|
||||
access_msg = ''
|
||||
can_access = false
|
||||
|
||||
is_ready = false
|
||||
mode = ''
|
||||
id = ''
|
||||
@@ -99,49 +161,62 @@ export default class FormComponent extends Component {
|
||||
this.mode = this.initial_mode
|
||||
this.id = this.form_id
|
||||
this.resource_class = await resource_service.get(this.resource)
|
||||
|
||||
if ( await this.resource_class.can(this.mode) ) {
|
||||
this.can_access = true
|
||||
this.access_msg = true
|
||||
} else {
|
||||
this.can_access = false
|
||||
this.access_msg = 'Sorry, you do not have permission to ' + this.mode + ' this resource.'
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
this.uuid = utility.uuid()
|
||||
await this.load()
|
||||
await this.init()
|
||||
await this.load()
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.definition = this.resource_class.form_definition
|
||||
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)
|
||||
const other_params = field._options.other_params || {}
|
||||
|
||||
field.options = (await rsc.list()).map(item => {
|
||||
field.options = (await rsc.list(other_params)).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') ) {
|
||||
async load() {
|
||||
if (this.mode !== 'insert') {
|
||||
this.data = await this.resource_class.get(this.id)
|
||||
}
|
||||
|
||||
for ( const field of this.definition.fields ) {
|
||||
if ( field.type.endsWith('.multiple') && !this.data[field.field] ) {
|
||||
this.data[field.field] = []
|
||||
}
|
||||
}
|
||||
|
||||
this.title = title_map[this.mode] + ' ' + this.resource_class.item
|
||||
|
||||
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'
|
||||
@@ -171,9 +246,12 @@ export default class FormComponent extends Component {
|
||||
validate() {
|
||||
let valid = true
|
||||
for ( const field of this.definition.fields ) {
|
||||
if ( field.required && (!(field.field in this.data) || !this.data[field.field]) ) {
|
||||
if ( (Array.isArray(field.required) ? field.required.includes(this.mode) : field.required) && (!(field.field in this.data) || !this.data[field.field]) ) {
|
||||
field.error = 'This field is required.'
|
||||
valid = false
|
||||
} else if ( field.type === 'password' && this.data[field.field] !== this.data[field.field + '-confirm'] ) {
|
||||
field.error = field.name + ' confirmation does not match.'
|
||||
valid = false
|
||||
} else {
|
||||
field.error = ''
|
||||
}
|
||||
|
||||
@@ -1,49 +1,62 @@
|
||||
import { Component } from '../../lib/vues6/vues6.js'
|
||||
import { action_service } from '../service/Action.service.js'
|
||||
import { message_service } from '../service/Message.service.js'
|
||||
import { resource_service } from '../service/Resource.service.js'
|
||||
|
||||
const template = `
|
||||
<div>
|
||||
<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>
|
||||
<span v-if="!can_access">
|
||||
<div class="row m-5">
|
||||
<div class="col-12 text-center">
|
||||
<h4 class="pad-top">{{ access_msg }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col" v-for="col of definition.columns">{{ col.name }}</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) of data">
|
||||
<th scope="row">{{ index + 1 }}</th>
|
||||
<td v-for="col of definition.columns">
|
||||
<span v-if="typeof col.renderer === 'function'">{{ col.renderer(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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
<span v-if="can_access">
|
||||
<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>
|
||||
<div class="row mb-4" v-if="definition.display">
|
||||
<div class="col-12" v-html="definition.display"></div>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col" v-for="col of definition.columns">{{ col.name }}</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) of data">
|
||||
<th scope="row">{{ index + 1 }}</th>
|
||||
<td v-for="col of definition.columns">
|
||||
<span v-if="typeof col.renderer === 'function'">{{ col.renderer(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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -56,17 +69,23 @@ export default class ListingComponent extends Component {
|
||||
data = []
|
||||
resource_class = {}
|
||||
|
||||
access_msg = ''
|
||||
can_access = false
|
||||
|
||||
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.')
|
||||
this.resource_class = await resource_service.get(this.resource)
|
||||
|
||||
const rsc_name = this.resource.toLowerCase().replace(/\//g, '_')
|
||||
if ( !resource_mod[rsc_name] )
|
||||
throw new Error('Unable to extract resource object from module.')
|
||||
// Make sure we have permission
|
||||
if ( !(await this.resource_class.can('list')) ) {
|
||||
this.access_msg = 'Sorry, you do not have permission to view this resource.'
|
||||
this.can_access = false
|
||||
return
|
||||
} else {
|
||||
this.access_msg = ''
|
||||
this.can_access = true
|
||||
}
|
||||
|
||||
this.resource_class = resource_mod[rsc_name]
|
||||
await this.load()
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const template = `
|
||||
>
|
||||
<h6 class="dropdown-header">Hello, {{ first_name }}.</h6>
|
||||
<a href="/dash/profile" class="dropdown-item">My Profile</a>
|
||||
<a href="/dash/c/listing/reflect/Token" v-if="can.api_tokens" class="dropdown-item">API Tokens</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="/auth/logout" class="dropdown-item">Sign-Out of {{ app_name }}</a>
|
||||
</div>
|
||||
@@ -51,6 +52,8 @@ export default class NavBarComponent extends Component {
|
||||
static get template() { return template }
|
||||
static get props() { return [] }
|
||||
|
||||
can = {}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.toggle_event = event_bus.event('sidebar.toggle')
|
||||
@@ -59,6 +62,10 @@ export default class NavBarComponent extends Component {
|
||||
this.app_name = session.get('app.name')
|
||||
}
|
||||
|
||||
async vue_on_create() {
|
||||
this.can.api_tokens = await session.check_permissions('v1:reflect:tokens:list')
|
||||
}
|
||||
|
||||
toggle_sidebar() {
|
||||
this.toggle_event.fire()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component } from '../../lib/vues6/vues6.js'
|
||||
import { event_bus } from '../service/EventBus.service.js'
|
||||
import { action_service } from '../service/Action.service.js'
|
||||
import { resource_service } from '../service/Resource.service.js'
|
||||
import { session } from '../service/Session.service.js'
|
||||
|
||||
const template = `
|
||||
<div class="bg-light border-right coreid-sidebar-wrapper" id="sidebar-wrapper" v-bind:class="{ collapsed: isCollapsed }">
|
||||
@@ -23,7 +25,9 @@ export default class SideBarComponent extends Component {
|
||||
static get props() { return ['app_name'] }
|
||||
static get template() { return template }
|
||||
|
||||
actions = [
|
||||
actions = []
|
||||
|
||||
possible_actions = [
|
||||
{
|
||||
text: 'Profile',
|
||||
action: 'redirect',
|
||||
@@ -31,23 +35,45 @@ export default class SideBarComponent extends Component {
|
||||
},
|
||||
{
|
||||
text: 'Users',
|
||||
action: 'redirect',
|
||||
next: '/dash/users',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'auth/User',
|
||||
},
|
||||
{
|
||||
text: 'Groups',
|
||||
action: 'redirect',
|
||||
next: '/dash/c/listing/ldap/Group',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'auth/Group',
|
||||
},
|
||||
{
|
||||
text: 'Applications',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'App',
|
||||
},
|
||||
{
|
||||
text: 'IAM Policy',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'iam/Policy',
|
||||
},
|
||||
{
|
||||
text: 'LDAP Clients',
|
||||
action: 'redirect',
|
||||
next: '/dash/c/listing/ldap/Client',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'ldap/Client',
|
||||
},
|
||||
{
|
||||
text: 'OAuth2 Clients',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'oauth/Client',
|
||||
},
|
||||
{
|
||||
text: 'SAML Service Providers',
|
||||
action: 'redirect',
|
||||
next: '/dash/c/listing/saml/Provider',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'saml/Provider',
|
||||
},
|
||||
{
|
||||
text: 'Settings',
|
||||
@@ -63,6 +89,32 @@ export default class SideBarComponent extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
async vue_on_create() {
|
||||
const new_actions = []
|
||||
|
||||
const perm_lookups = []
|
||||
for ( const action of this.possible_actions ) {
|
||||
if ( action.resource ) {
|
||||
action.rsc = await resource_service.get(action.resource)
|
||||
perm_lookups.push(`${action.rsc.permission_base}:list`)
|
||||
}
|
||||
}
|
||||
|
||||
const perms = await session.check_permissions(...perm_lookups)
|
||||
|
||||
for ( const action of this.possible_actions ) {
|
||||
if ( action.resource ) {
|
||||
if ( perms[`${action.rsc.permission_base}:list`] ) {
|
||||
new_actions.push(action)
|
||||
}
|
||||
} else {
|
||||
new_actions.push(action)
|
||||
}
|
||||
}
|
||||
|
||||
this.actions = new_actions
|
||||
}
|
||||
|
||||
isCollapsed = false
|
||||
|
||||
toggle() {
|
||||
|
||||
@@ -175,8 +175,6 @@ export default class EditProfileComponent extends Component {
|
||||
this.form_message = 'Saving...'
|
||||
save()
|
||||
}
|
||||
|
||||
console.log('profile form', this)
|
||||
}
|
||||
|
||||
get_submit_data() {
|
||||
|
||||
@@ -80,7 +80,6 @@ export default class AppPasswordFormComponent extends Component {
|
||||
|
||||
vue_on_create() {
|
||||
this.uuid = utility.uuid()
|
||||
console.log({auth_api})
|
||||
}
|
||||
|
||||
async on_name_change(event) {
|
||||
|
||||
112
app/assets/app/resource/App.resource.js
Normal file
112
app/assets/app/resource/App.resource.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import CRUDBase from './CRUDBase.js'
|
||||
import { session } from '../service/Session.service.js'
|
||||
|
||||
class AppResource extends CRUDBase {
|
||||
endpoint = '/api/v1/applications'
|
||||
required_fields = ['name', 'identifier']
|
||||
permission_base = 'v1:applications'
|
||||
|
||||
item = 'Application'
|
||||
plural = 'Applications'
|
||||
|
||||
listing_definition = {
|
||||
display: `
|
||||
An application is anything that can authenticate users against ${session.get('app.name')}. Applications can have any number of associated LDAP clients, SAML service providers, and OAuth2 clients.
|
||||
`,
|
||||
columns: [
|
||||
{
|
||||
name: 'Name',
|
||||
field: 'name',
|
||||
},
|
||||
{
|
||||
name: 'Identifier',
|
||||
field: 'identifier',
|
||||
},
|
||||
{
|
||||
name: 'Description',
|
||||
field: 'description',
|
||||
},
|
||||
],
|
||||
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 = {
|
||||
fields: [
|
||||
{
|
||||
name: 'Name',
|
||||
field: 'name',
|
||||
placeholder: 'Awesome App',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Identifier',
|
||||
field: 'identifier',
|
||||
placeholder: 'awesome_app',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Description',
|
||||
field: 'description',
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
name: 'Associated LDAP Clients',
|
||||
field: 'ldap_client_ids',
|
||||
type: 'select.dynamic.multiple',
|
||||
options: {
|
||||
resource: 'ldap/Client',
|
||||
display: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Associated OAuth2 Clients',
|
||||
field: 'oauth_client_ids',
|
||||
type: 'select.dynamic.multiple',
|
||||
options: {
|
||||
resource: 'oauth/Client',
|
||||
display: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Associated SAML Service Providers',
|
||||
field: 'saml_service_provider_ids',
|
||||
type: 'select.dynamic.multiple',
|
||||
options: {
|
||||
resource: 'saml/Provider',
|
||||
display: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const app = new AppResource()
|
||||
export { app }
|
||||
@@ -1,8 +1,10 @@
|
||||
import APIParseError from './APIParseError.js'
|
||||
import { session } from '../service/Session.service.js'
|
||||
|
||||
export default class CRUDBase {
|
||||
endpoint = '/api/v1'
|
||||
required_fields = []
|
||||
permission_base = ''
|
||||
|
||||
listing_definition = {}
|
||||
form_definition = {}
|
||||
@@ -10,14 +12,18 @@ export default class CRUDBase {
|
||||
item = ''
|
||||
plural = ''
|
||||
|
||||
async list() {
|
||||
const results = await axios.get(this._endpoint())
|
||||
async can(action) {
|
||||
return session.check_permissions(`${this.permission_base}:${action}`)
|
||||
}
|
||||
|
||||
async list(other_params = {}) {
|
||||
const results = await axios.get(this._endpoint(), { params: other_params })
|
||||
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))
|
||||
async get(id, other_params = {}) {
|
||||
const results = await axios.get(this._endpoint(id), { params: other_params })
|
||||
if ( results && results.data && results.data.data ) return results.data.data
|
||||
else throw new APIParseError()
|
||||
}
|
||||
@@ -40,8 +46,8 @@ export default class CRUDBase {
|
||||
await axios.patch(this._endpoint(id), properties)
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
await axios.delete(this._endpoint(id))
|
||||
async delete(id, other_params = {}) {
|
||||
await axios.delete(this._endpoint(id), { params: other_params })
|
||||
}
|
||||
|
||||
_endpoint(sub = '/') {
|
||||
|
||||
77
app/assets/app/resource/auth/Group.resource.js
Normal file
77
app/assets/app/resource/auth/Group.resource.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
import { session } from '../../service/Session.service.js'
|
||||
|
||||
class GroupResource extends CRUDBase {
|
||||
endpoint = '/api/v1/auth/groups'
|
||||
required_fields = ['name']
|
||||
permission_base = 'v1:auth:groups'
|
||||
|
||||
item = 'Group'
|
||||
plural = 'Groups'
|
||||
|
||||
listing_definition = {
|
||||
display: `
|
||||
In ${session.get('app.name')}, groups are simply a tool for organizing users and assigning permissions and access in bulk. After creating and assigning users to a group, you can manage permissions for that group, and its policies will be applied to all users in that group.
|
||||
`,
|
||||
columns: [
|
||||
{
|
||||
name: 'Name',
|
||||
field: 'name',
|
||||
},
|
||||
{
|
||||
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 = {
|
||||
fields: [
|
||||
{
|
||||
name: 'Name',
|
||||
field: 'name',
|
||||
placeholder: 'Some Cool Users',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Users',
|
||||
field: 'user_ids',
|
||||
type: 'select.dynamic.multiple',
|
||||
options: {
|
||||
resource: 'auth/User',
|
||||
display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`,
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const auth_group = new GroupResource()
|
||||
export { auth_group }
|
||||
@@ -3,6 +3,7 @@ import CRUDBase from '../CRUDBase.js'
|
||||
class RoleResource extends CRUDBase {
|
||||
endpoint = '/api/v1/auth/roles'
|
||||
required_fields = ['role', 'permissions']
|
||||
permission_base = 'v1:auth:roles'
|
||||
|
||||
item = 'Role'
|
||||
plural = 'Roles'
|
||||
|
||||
@@ -1,11 +1,106 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
import { session } from '../../service/Session.service.js'
|
||||
|
||||
class UserResource extends CRUDBase {
|
||||
endpoint = '/api/v1/auth/users'
|
||||
required_fields = ['uid', 'first_name', 'last_name', 'email', 'password']
|
||||
required_fields = ['uid', 'first_name', 'last_name', 'email']
|
||||
permission_base = 'v1:auth:users'
|
||||
|
||||
item = 'User'
|
||||
plural = 'Users'
|
||||
|
||||
listing_definition = {
|
||||
display: `
|
||||
Users can be assigned permissions and, if granted, can manage their ${session.get('app.name')} accounts from the Profile page, as well as login to the external applications they've been given access to.
|
||||
`,
|
||||
columns: [
|
||||
{
|
||||
name: 'UID',
|
||||
field: 'uid',
|
||||
},
|
||||
{
|
||||
name: 'Last Name',
|
||||
field: 'last_name',
|
||||
},
|
||||
{
|
||||
name: 'First Name',
|
||||
field: 'first_name',
|
||||
},
|
||||
{
|
||||
name: 'E-Mail',
|
||||
field: 'email',
|
||||
},
|
||||
],
|
||||
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 = {
|
||||
fields: [
|
||||
{
|
||||
name: 'First Name',
|
||||
field: 'first_name',
|
||||
placeholder: 'John',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Last Name',
|
||||
field: 'last_name',
|
||||
placeholder: 'Doe',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Username',
|
||||
field: 'uid',
|
||||
placeholder: 'john.doe',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'E-Mail',
|
||||
field: 'email',
|
||||
placeholder: 'john@contoso.com',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Tagline',
|
||||
field: 'tagline',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Password',
|
||||
field: 'password',
|
||||
type: 'password',
|
||||
placeholder: 'Password',
|
||||
required: ['insert'],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const auth_user = new UserResource()
|
||||
|
||||
140
app/assets/app/resource/iam/Policy.resource.js
Normal file
140
app/assets/app/resource/iam/Policy.resource.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
import { session } from '../../service/Session.service.js'
|
||||
|
||||
class PolicyResource extends CRUDBase {
|
||||
endpoint = '/api/v1/iam/policy'
|
||||
required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type']
|
||||
permission_base = 'v1:iam:policy'
|
||||
|
||||
item = 'IAM Policy'
|
||||
plural = 'IAM Policies'
|
||||
|
||||
listing_definition = {
|
||||
display: `
|
||||
Identity & Access Management (IAM) policies give you fine grained control over which ${session.get('app.name')} users and groups are allowed to access which applications.
|
||||
<br><br>
|
||||
An IAM policy has three parts. First, is the subject. The subject is who the policy applies to and is either a user or a group. The second part is the access type. This is either an allowance or a denial. That is, the policy either grants a subject access to a resource, or explicitly denies them access. The final part of the policy is the target. This is the application that the subject is being granted or denied access to.
|
||||
<br><br>
|
||||
Note that IAM policies can be overlapping. So, ${session.get('app.name')}'s policy engine follows a few basic rules when deciding what policies take precedence:
|
||||
<br><br>
|
||||
<ol>
|
||||
<li>User policy takes precedence over group policy.</li>
|
||||
<li>Denials take precedence over approvals.</li>
|
||||
<li>Denials by default.</li>
|
||||
</ol>
|
||||
This means, for example, that if a user's group is allowed access, but a user is denied access, the user will be denied access. Likewise, if there are two policies for a subject, one granting them access and one denying them access, the denial will take precedence.
|
||||
`,
|
||||
columns: [
|
||||
{
|
||||
name: 'Subject',
|
||||
field: 'entity_display',
|
||||
},
|
||||
{
|
||||
name: 'Access Type',
|
||||
field: 'access_type',
|
||||
renderer: access_type => access_type === 'deny' ? '...is denied access to...' : '...is granted access to...',
|
||||
},
|
||||
{
|
||||
name: 'Target',
|
||||
field: 'target_display',
|
||||
},
|
||||
],
|
||||
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 = {
|
||||
fields: [
|
||||
{
|
||||
name: 'Subject Type',
|
||||
field: 'entity_type',
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{ display: 'User', value: 'user' },
|
||||
{ display: 'Group', value: 'group' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Subject',
|
||||
field: 'entity_id',
|
||||
required: true,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'auth/User',
|
||||
display: user => `User: ${user.last_name}, ${user.first_name} (${user.uid})`,
|
||||
value: 'id',
|
||||
},
|
||||
if: (form_data) => form_data.entity_type === 'user',
|
||||
},
|
||||
{
|
||||
name: 'Subject',
|
||||
field: 'entity_id',
|
||||
required: true,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'auth/Group',
|
||||
display: group => `Group: ${group.name} (${group.user_ids.length} users)`,
|
||||
value: 'id',
|
||||
},
|
||||
if: (form_data) => form_data.entity_type === 'group',
|
||||
},
|
||||
{
|
||||
name: 'Access Type',
|
||||
field: 'access_type',
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{ display: '...is granted access to...', value: 'allow' },
|
||||
{ display: '...is denied access to...', value: 'deny' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Target Type',
|
||||
field: 'target_type',
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{ display: 'Application', value: 'application' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Target',
|
||||
field: 'target_id',
|
||||
required: true,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'App',
|
||||
display: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
if: (form_data) => form_data.target_type === 'application'
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const iam_policy = new PolicyResource()
|
||||
export { iam_policy }
|
||||
@@ -1,13 +1,20 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
import { session } from '../../service/Session.service.js'
|
||||
|
||||
class ClientResource extends CRUDBase {
|
||||
endpoint = '/api/v1/ldap/clients'
|
||||
required_fields = ['name', 'uid', 'password']
|
||||
permission_base = 'v1:ldap:clients'
|
||||
|
||||
item = 'LDAP Client'
|
||||
plural = 'LDAP Clients'
|
||||
|
||||
listing_definition = {
|
||||
display: `
|
||||
LDAP Clients are special user accounts that external applications can use to bind to ${session.get('app.name')}'s built-in LDAP server to allow these applications to authenticate users.
|
||||
<br><br>
|
||||
These special accounts are permitted to bind to the LDAP server, but are not allowed to sign-in to ${session.get('app.name')}.
|
||||
`,
|
||||
columns: [
|
||||
{
|
||||
name: 'Client Name',
|
||||
@@ -33,6 +40,14 @@ class ClientResource extends CRUDBase {
|
||||
icon: 'fa fa-edit',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
type: 'resource',
|
||||
position: 'row',
|
||||
action: 'delete',
|
||||
icon: 'fa fa-times',
|
||||
color: 'danger',
|
||||
confirm: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import CRUDBase from '../CRUDBase.js'
|
||||
class GroupResource extends CRUDBase {
|
||||
endpoint = '/api/v1/ldap/groups'
|
||||
required_fields = ['name', 'role']
|
||||
permission_base = 'v1:ldap:groups'
|
||||
|
||||
item = 'LDAP Group'
|
||||
plural = 'LDAP Groups'
|
||||
|
||||
107
app/assets/app/resource/oauth/Client.resource.js
Normal file
107
app/assets/app/resource/oauth/Client.resource.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
import { session } from '../../service/Session.service.js';
|
||||
|
||||
class ClientResource extends CRUDBase {
|
||||
endpoint = '/api/v1/oauth/clients'
|
||||
required_fields = ['name', 'redirect_url', 'api_scopes']
|
||||
permission_base = 'v1:oauth:clients'
|
||||
|
||||
item = 'OAuth2 Client'
|
||||
plural = 'OAuth2 Clients'
|
||||
|
||||
listing_definition = {
|
||||
display: `
|
||||
OAuth2 clients are applications that support authentication over the OAuth2 protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, you need to create an OAuth2 client for that application, and provide the name, redirect URL, and API scopes.
|
||||
<br><br>
|
||||
You must select the API scopes to grant this OAuth2 client. This defines what ${session.get('app.name')} endpoints the application is allowed to access. For most applications, granting the <code>v1:api:users:get</code> and <code>v1:api:groups:get</code> API scopes should be sufficient.
|
||||
<br><br>
|
||||
This method can also be used to access the API for other purposes. Hence, the expansive API scopes. ${session.get('app.name')} uses Flitter-Auth's built-in OAuth2 server under the hood, so you can find details on how to configure the OAuth2 clients <a href="https://flitter.garrettmills.dev/tutorial-flitter-auth-oauth2-server.html" target="_blank">here.</a>
|
||||
`,
|
||||
columns: [
|
||||
{
|
||||
name: 'Client Name',
|
||||
field: 'name',
|
||||
},
|
||||
{
|
||||
name: '# of Scopes',
|
||||
field: 'api_scopes',
|
||||
renderer: (api_scopes) => api_scopes.length,
|
||||
},
|
||||
{
|
||||
name: 'Redirect URL',
|
||||
field: 'redirect_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',
|
||||
},
|
||||
{
|
||||
type: 'resource',
|
||||
position: 'row',
|
||||
action: 'delete',
|
||||
icon: 'fa fa-times',
|
||||
color: 'danger',
|
||||
confirm: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
form_definition = {
|
||||
fields: [
|
||||
{
|
||||
name: 'Client Name',
|
||||
field: 'name',
|
||||
placeholder: 'Awesome External App',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Redirect URL',
|
||||
field: 'redirect_url',
|
||||
placeholder: 'https://awesome.app/oauth2/callback',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'API Scopes',
|
||||
field: 'api_scopes',
|
||||
type: 'select.dynamic.multiple',
|
||||
options: {
|
||||
resource: 'reflect/Scope',
|
||||
display: 'scope',
|
||||
value: 'scope',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'Client ID',
|
||||
field: 'uuid',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
hidden: ['insert'],
|
||||
},
|
||||
{
|
||||
name: 'Client Secret',
|
||||
field: 'secret',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
hidden: ['insert'],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const oauth_client = new ClientResource()
|
||||
export { oauth_client }
|
||||
13
app/assets/app/resource/reflect/Scope.resource.js
Normal file
13
app/assets/app/resource/reflect/Scope.resource.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
|
||||
class ScopeResource extends CRUDBase {
|
||||
endpoint = '/api/v1/reflect/scopes'
|
||||
required_fields = ['scope']
|
||||
permission_base = 'v1:reflect:scopes'
|
||||
|
||||
item = 'API Scope'
|
||||
plural = 'API Scopes'
|
||||
}
|
||||
|
||||
const reflect_scope = new ScopeResource()
|
||||
export { reflect_scope }
|
||||
89
app/assets/app/resource/reflect/Token.resource.js
Normal file
89
app/assets/app/resource/reflect/Token.resource.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
|
||||
class TokenResource extends CRUDBase {
|
||||
endpoint = '/api/v1/reflect/tokens'
|
||||
required_fields = ['client_id']
|
||||
permission_base = 'v1:reflect:tokens'
|
||||
|
||||
item = 'API Token'
|
||||
plural = 'API Tokens'
|
||||
|
||||
listing_definition = {
|
||||
display: `
|
||||
This allows you to create bearer tokens manually to allow for easier testing of the API. Notably, this is meant as a measure for testing and development, not for long term use.
|
||||
<br><br>
|
||||
If you have an application that needs to regularly interact with the API, set it up as an <a href="/dash/c/listing/oauth/Client">OAuth2 Client</a>. Manually-created tokens expire 7 days after their creation.
|
||||
`,
|
||||
columns: [
|
||||
{
|
||||
name: 'Token',
|
||||
field: 'token',
|
||||
},
|
||||
{
|
||||
name: 'Client',
|
||||
field: 'client_display',
|
||||
},
|
||||
{
|
||||
name: 'Expires',
|
||||
field: 'expires',
|
||||
},
|
||||
],
|
||||
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 = {
|
||||
fields: [
|
||||
{
|
||||
name: 'Client',
|
||||
field: 'client_id',
|
||||
required: true,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'oauth/Client',
|
||||
display: 'name',
|
||||
value: 'uuid',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Bearer Token',
|
||||
field: 'token',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
hidden: ['insert'],
|
||||
},
|
||||
{
|
||||
name: 'Expires',
|
||||
field: 'expires',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
hidden: ['insert'],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const reflect_token = new TokenResource()
|
||||
export { reflect_token }
|
||||
@@ -1,13 +1,18 @@
|
||||
import CRUDBase from '../CRUDBase.js'
|
||||
import { session } from '../../service/Session.service.js'
|
||||
|
||||
class ProviderResource extends CRUDBase {
|
||||
endpoint = '/api/v1/saml/providers'
|
||||
required_fields = ['name', 'acs_url', 'entity_id']
|
||||
permission_base = 'v1:saml:providers'
|
||||
|
||||
item = 'SAML Service Provider'
|
||||
plural = 'SAML Service Providers'
|
||||
|
||||
listing_definition = {
|
||||
display: `SAML Service Providers are applications that support external authentication to a SAML Identity Provider. In this case, ${session.get('app.name')} is the identity provider, so these external applications can authenticate against it.
|
||||
<br><br>
|
||||
To do this, you need to know the SAML service provider's entity ID, assertion consumer service URL, and single-logout URL (if supported).`,
|
||||
columns: [
|
||||
{
|
||||
name: 'Provider Name',
|
||||
@@ -42,6 +47,14 @@ class ProviderResource extends CRUDBase {
|
||||
icon: 'fa fa-edit',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
type: 'resource',
|
||||
position: 'row',
|
||||
action: 'delete',
|
||||
icon: 'fa fa-times',
|
||||
color: 'danger',
|
||||
confirm: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ class Session {
|
||||
|
||||
parent[parts.reverse()[0]] = value
|
||||
}
|
||||
|
||||
async check_permissions(...permissions) {
|
||||
const result = await axios.post('/api/v1/reflect/check_permissions', { permissions })
|
||||
if ( permissions.length === 1 ) return result.data.data[permissions[0]]
|
||||
return result.data.data
|
||||
}
|
||||
}
|
||||
|
||||
const session = new Session()
|
||||
|
||||
Reference in New Issue
Block a user