9 Commits
ci-14 ... ci-21

Author SHA1 Message Date
f06ff83dce Move all front-end public field definitions into constructors for iOS support
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-28 19:53:07 -05:00
251aa6cf97 Remove source map annotations from minified libraries
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-28 19:29:00 -05:00
60003d64d5 Add front-end error logging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-28 19:13:13 -05:00
535dde13ff Guarantee additional logging data object in permission middleware
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 10:19:01 -05:00
63d102296f Fix bad logging method call names
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:55:26 -05:00
77d203b2b0 Add missing service injection...
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:53:27 -05:00
fcbf25e3ce Check IAM policy for OAuth2 logins
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:51:36 -05:00
084ec7bbc1 inflate OpenID UID to case-sensitive on lookup
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-19 09:39:28 -05:00
6b3339a883 Force OpenID UID to be lowercase
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-19 09:35:49 -05:00
51 changed files with 1523 additions and 1270 deletions

View File

@@ -38,14 +38,18 @@ export default class MFAChallengePage extends Component {
static get props() { return ['app_name'] }
static get template() { return template }
loading = false
constructor() {
super()
verify_code = ''
verify_success = false
this.loading = false
error_message = ''
other_message = ''
t = {}
this.verify_code = ''
this.verify_success = false
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() {
this.t = await T(

View File

@@ -28,12 +28,16 @@ export default class MFADisableComponent extends Component {
static get template() { return template }
static get props() { return [] }
app_name = ''
step = 0
loading = false
error_message = ''
other_message = ''
t = {}
constructor() {
super()
this.app_name = ''
this.step = 0
this.loading = false
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() {
this.app_name = session.get('app.name')

View File

@@ -38,12 +38,16 @@ export default class MFARecoveryComponent extends Component {
static get template() { return template }
static get props() { return ['app_name'] }
verify_success = false
loading = false
recovery_code = ''
error_message = ''
other_message = ''
t = {}
constructor() {
super()
this.verify_success = false
this.loading = false
this.recovery_code = ''
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() {
this.t = await T(

View File

@@ -61,19 +61,23 @@ export default class MFASetupPage extends Component {
static get props() { return ['app_name'] }
static get template() { return template }
loading = false
step = 0
constructor() {
super()
qr_data = ''
otpauth_url = ''
secret = ''
verify_code = ''
this.loading = false
this.step = 0
verify_success = false
this.qr_data = ''
this.otpauth_url = ''
this.secret = ''
this.verify_code = ''
error_message = ''
other_message = ''
t = {}
this.verify_success = false
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() {
this.t = await T(

View File

@@ -25,7 +25,11 @@ export default class AuthPage extends Component {
static get props() { return ['app_name', 'message', 'actions'] }
static get template() { return template }
loading = false
constructor() {
super()
this.loading = false
}
async action_click(index) {
this.loading = true

View File

@@ -78,23 +78,27 @@ export default class PasswordResetComponent extends Component {
static get template() { return template }
static get props() { return ['app_name'] }
step = 0
loading = false
has_mfa = false
constructor() {
super()
error_message = ''
other_message = ''
this.step = 0
this.loading = false
this.has_mfa = false
step_1_valid = false
step_1_calc_time = ''
step_1_problem = ''
this.error_message = ''
this.other_message = ''
step_2_valid = false
this.step_1_valid = false
this.step_1_calc_time = ''
this.step_1_problem = ''
password = ''
confirm_password = ''
t = {}
ready = false
this.step_2_valid = false
this.password = ''
this.confirm_password = ''
this.t = {}
this.ready = false
}
async vue_on_create() {
this.has_mfa = !!session.get('user.has_mfa')

View File

@@ -63,18 +63,21 @@ export default class AuthLoginForm extends Component {
] }
static get template() { return template }
username = ''
password = ''
button_text = ''
step_two = false
btn_disabled = true
loading = false
error_message = ''
other_message = ''
allow_back = true
auth_user = false
constructor() {
super()
t = {}
this.username = ''
this.password = ''
this.button_text = ''
this.step_two = false
this.btn_disabled = true
this.loading = false
this.error_message = ''
this.other_message = ''
this.allow_back = true
this.auth_user = false
this.t = {}
}
watch_username(new_username, old_username) {
this.btn_disabled = !new_username

View File

@@ -98,19 +98,23 @@ export default class RegistrationFormComponent extends Component {
static get template() { return template }
static get props() { return ['app_name'] }
loading = false
step = 1
other_message = ''
error_message = ''
message = ''
btn_disabled = true
button_text = ''
constructor() {
super()
first_name = ''
last_name = ''
username = ''
email = ''
t = {}
this.loading = false
this.step = 1
this.other_message = ''
this.error_message = ''
this.message = ''
this.btn_disabled = true
this.button_text = ''
this.first_name = ''
this.last_name = ''
this.username = ''
this.email = ''
this.t = {}
}
async vue_on_create() {
// Batch-load translated phrases

View File

@@ -146,20 +146,24 @@ export default class FormComponent extends Component {
return ['resource', 'form_id', 'initial_mode']
}
definition = {}
data = {}
uuid = ''
title = ''
error_message = ''
other_message = ''
constructor() {
super()
access_msg = ''
can_access = false
this.definition = {}
this.data = {}
this.uuid = ''
this.title = ''
this.error_message = ''
this.other_message = ''
is_ready = false
mode = ''
id = ''
t = {}
this.access_msg = ''
this.can_access = false
this.is_ready = false
this.mode = ''
this.id = ''
this.t = {}
}
reset() {
this.definition = {}

View File

@@ -65,13 +65,17 @@ export default class ListingComponent extends Component {
static get template() { return template }
static get props() { return ['resource'] }
definition = {}
data = []
resource_class = {}
constructor() {
super()
access_msg = ''
can_access = false
t = {}
this.definition = {}
this.data = []
this.resource_class = {}
this.access_msg = ''
this.can_access = false
this.t = {}
}
async vue_on_create() {
this.t = await T(

View File

@@ -232,35 +232,39 @@ export default class AppSetupComponent extends Component {
static get template() { return template }
static get props() { return [] }
step = 0
btn_disabled = true
btn_back = false
btn_hidden = false
btn_listing = false
constructor() {
super()
name = ''
identifier = ''
type = '' // ldap | saml | oauth
oauth_redirect_uri = ''
this.step = 0
this.btn_disabled = true
this.btn_back = false
this.btn_hidden = false
this.btn_listing = false
saml_entity_id = ''
saml_acs_url = ''
saml_slo_url = ''
this.name = ''
this.identifier = ''
this.type = '' // ldap | saml | oauth
this.oauth_redirect_uri = ''
ldap_username = ''
ldap_password = ''
ldap_password_confirm = ''
ldap_config = {}
this.saml_entity_id = ''
this.saml_acs_url = ''
this.saml_slo_url = ''
error_message = ''
this.ldap_username = ''
this.ldap_password = ''
this.ldap_password_confirm = ''
this.ldap_config = {}
app = {}
oauth_client = {}
saml_provider = {}
ldap_client = {}
this.error_message = ''
app_name = ''
host = ''
this.app = {}
this.oauth_client = {}
this.saml_provider = {}
this.ldap_client = {}
this.app_name = ''
this.host = ''
}
make_url(path) {
return session.url(path)

View File

@@ -1,7 +1,6 @@
import { Component } from '../../lib/vues6/vues6.js'
import { event_bus } from '../service/EventBus.service.js'
import { session } from '../service/Session.service.js'
import { message_service } from '../service/Message.service.js'
const template = `
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
@@ -53,10 +52,10 @@ export default class NavBarComponent extends Component {
static get template() { return template }
static get props() { return [] }
can = {}
constructor() {
super()
this.can = {}
this.toggle_event = event_bus.event('sidebar.toggle')
this.first_name = session.get('user.first_name')
this.last_name = session.get('user.last_name')

View File

@@ -23,72 +23,75 @@ export default class SideBarComponent extends Component {
static get props() { return ['app_name'] }
static get template() { return template }
actions = []
possible_actions = [
{
text: 'Profile',
action: 'redirect',
next: '/dash/profile',
},
{
text: 'Users',
action: 'list',
type: 'resource',
resource: 'auth/User',
},
{
text: 'Groups',
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: 'list',
type: 'resource',
resource: 'ldap/Client',
},
{
text: 'OAuth2 Clients',
action: 'list',
type: 'resource',
resource: 'oauth/Client',
},
{
text: 'OpenID Connect Clients',
action: 'list',
type: 'resource',
resource: 'openid/Client',
},
{
text: 'SAML Service Providers',
action: 'list',
type: 'resource',
resource: 'saml/Provider',
},
{
text: 'Settings',
action: 'list',
type: 'resource',
resource: 'Setting',
},
]
constructor() {
super()
this.actions = []
this.isCollapsed = false
this.possible_actions = [
{
text: 'Profile',
action: 'redirect',
next: '/dash/profile',
},
{
text: 'Users',
action: 'list',
type: 'resource',
resource: 'auth/User',
},
{
text: 'Groups',
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: 'list',
type: 'resource',
resource: 'ldap/Client',
},
{
text: 'OAuth2 Clients',
action: 'list',
type: 'resource',
resource: 'oauth/Client',
},
{
text: 'OpenID Connect Clients',
action: 'list',
type: 'resource',
resource: 'openid/Client',
},
{
text: 'SAML Service Providers',
action: 'list',
type: 'resource',
resource: 'saml/Provider',
},
{
text: 'Settings',
action: 'list',
type: 'resource',
resource: 'Setting',
},
]
event_bus.event('sidebar.toggle').subscribe(() => {
this.toggle()
})
@@ -120,8 +123,6 @@ export default class SideBarComponent extends Component {
this.actions = new_actions
}
isCollapsed = false
toggle() {
this.isCollapsed = !this.isCollapsed
}

View File

@@ -68,8 +68,12 @@ export default class MessageContainerComponent extends Component {
static get template() { return template }
static get props() { return [] }
messages = []
modals = []
constructor() {
super()
this.messages = []
this.modals = []
}
vue_on_create() {
this.alert_event = event_bus.event('message.alert')

View File

@@ -195,31 +195,35 @@ export default class EditProfileComponent extends Component {
static get template() { return template }
static get props() { return ['user_id'] }
profile_first = ''
profile_last = ''
profile_email = ''
profile_tagline = ''
last_reset = ''
mfa_enable_date = ''
constructor() {
super()
has_mfa_recovery = false
mfa_recovery_date = ''
mfa_recovery_codes = 0
this.profile_first = ''
this.profile_last = ''
this.profile_email = ''
this.profile_tagline = ''
this.last_reset = ''
this.mfa_enable_date = ''
form_message = 'No changes.'
this.has_mfa_recovery = false
this.mfa_recovery_date = ''
this.mfa_recovery_codes = 0
has_mfa = false
ready = false
this.form_message = 'No changes.'
notify_gateway_url = ''
notify_app_key = ''
notify_enabled = false
notify_created_on = ''
notify_loaded = false
this.has_mfa = false
this.ready = false
app_passwords = []
app_name = ''
t = {}
this.notify_gateway_url = ''
this.notify_app_key = ''
this.notify_enabled = false
this.notify_created_on = ''
this.notify_loaded = false
this.app_passwords = []
this.app_name = ''
this.t = {}
}
on_key_up = ($event) => {}

View File

@@ -72,12 +72,16 @@ export default class AppPasswordFormComponent extends Component {
static get template() { return template }
static get props() { return [] }
name = ''
valid = false
uuid = ''
enable_form = true
display_password = ''
t = {}
constructor() {
super()
this.name = ''
this.valid = false
this.uuid = ''
this.enable_form = true
this.display_password = ''
this.t = {}
}
async vue_on_create() {
this.t = await T(

View File

@@ -29,8 +29,12 @@ export default class ProfilePhotoUploaderComponent extends Component {
static get template() { return template }
static get params() { return [] }
ready = false
t = {}
constructor() {
super()
this.ready = false
this.t = {}
}
async vue_on_create() {
this.t = await T(

View File

@@ -2,126 +2,130 @@ 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'
constructor() {
super()
item = 'Application'
plural = 'Applications'
this.endpoint = '/api/v1/applications'
this.required_fields = ['name', 'identifier']
this.permission_base = 'v1:applications'
listing_definition = {
display: `
this.item = 'Application'
this.plural = 'Applications'
this.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: 'Manual Setup',
color: 'outline-success',
},
{
position: 'main',
action: 'redirect',
text: 'Setup Wizard',
color: 'success',
next: '/dash/app/setup',
},
{
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,
},
],
}
columns: [
{
name: 'Name',
field: 'name',
},
{
name: 'Identifier',
field: 'identifier',
},
{
name: 'Description',
field: 'description',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Manual Setup',
color: 'outline-success',
},
{
position: 'main',
action: 'redirect',
text: 'Setup Wizard',
color: 'success',
next: '/dash/app/setup',
},
{
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',
this.form_definition = {
fields: [
{
name: 'Name',
field: 'name',
placeholder: 'Awesome App',
required: true,
type: 'text',
},
},
{
name: 'Associated OAuth2 Clients',
field: 'oauth_client_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'oauth/Client',
display: 'name',
value: 'id',
{
name: 'Identifier',
field: 'identifier',
placeholder: 'awesome_app',
required: true,
type: 'text',
},
},
{
name: 'Associated OpenID Connect Clients',
field: 'openid_client_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'openid/Client',
display: 'client_name',
value: 'id',
{
name: 'Description',
field: 'description',
type: 'textarea',
},
},
{
name: 'Associated SAML Service Providers',
field: 'saml_service_provider_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'saml/Provider',
display: 'name',
value: 'id',
{
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 OpenID Connect Clients',
field: 'openid_client_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'openid/Client',
display: 'client_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',
},
},
],
}
}
}

View File

@@ -2,15 +2,17 @@ import APIParseError from './APIParseError.js'
import { session } from '../service/Session.service.js'
export default class CRUDBase {
endpoint = '/api/v1'
required_fields = []
permission_base = ''
constructor() {
this.endpoint = '/api/v1'
this.required_fields = []
this.permission_base = ''
listing_definition = {}
form_definition = {}
this.listing_definition = {}
this.form_definition = {}
item = ''
plural = ''
this.item = ''
this.plural = ''
}
async can(action) {
return session.check_permissions(`${this.permission_base}:${action}`)

View File

@@ -2,53 +2,57 @@ import CRUDBase from './CRUDBase.js'
import { session } from '../service/Session.service.js'
class SettingResource extends CRUDBase {
endpoint = '/api/v1/settings'
required_fields = ['key', 'value']
permission_base = 'v1:settings'
constructor() {
super()
item = 'Setting'
plural = 'Settings'
this.endpoint = '/api/v1/settings'
this.required_fields = ['key', 'value']
this.permission_base = 'v1:settings'
listing_definition = {
display: `
this.item = 'Setting'
this.plural = 'Settings'
this.listing_definition = {
display: `
<p>These are advanced settings that allow you to tweak the way ${session.get('app.name')} behaves. Tweak them at your own risk.</p>
`,
columns: [
{
name: 'Setting Key',
field: 'key',
},
{
name: 'Value',
field: 'value',
renderer: (v) => JSON.stringify(v),
},
],
actions: [
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
],
}
columns: [
{
name: 'Setting Key',
field: 'key',
},
{
name: 'Value',
field: 'value',
renderer: (v) => JSON.stringify(v),
},
],
actions: [
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
],
}
form_definition = {
fields: [
{
name: 'Setting Key',
field: 'key',
type: 'text',
readonly: true,
},
{
name: 'Value (JSON)',
field: 'value',
type: 'json',
},
],
this.form_definition = {
fields: [
{
name: 'Setting Key',
field: 'key',
type: 'text',
readonly: true,
},
{
name: 'Value (JSON)',
field: 'value',
type: 'json',
},
],
}
}
}

View File

@@ -2,74 +2,78 @@ 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'
constructor() {
super()
item = 'Group'
plural = 'Groups'
this.endpoint = '/api/v1/auth/groups'
this.required_fields = ['name']
this.permission_base = 'v1:auth: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,
},
],
}
this.item = 'Group'
this.plural = 'Groups'
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',
this.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,
},
],
}
this.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',
},
},
],
}
}
}

View File

@@ -1,12 +1,17 @@
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'
constructor() {
super()
this.endpoint = '/api/v1/auth/roles'
this.required_fields = ['role', 'permissions']
this.permission_base = 'v1:auth:roles'
this.item = 'Role'
this.plural = 'Roles'
}
}
const auth_role = new RoleResource()

View File

@@ -1,12 +1,16 @@
import CRUDBase from '../CRUDBase.js'
class TrapResource extends CRUDBase {
endpoint = '/api/v1/auth/traps'
required_fields = ['name', 'trap', 'redirect_to']
permission_base = 'v1:auth:traps'
constructor() {
super()
item = 'Trap'
plural = 'Traps'
this.endpoint = '/api/v1/auth/traps'
this.required_fields = ['name', 'trap', 'redirect_to']
this.permission_base = 'v1:auth:traps'
this.item = 'Trap'
this.plural = 'Traps'
}
}
const auth_trap = new TrapResource()

View File

@@ -2,114 +2,118 @@ 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']
permission_base = 'v1:auth:users'
constructor() {
super()
item = 'User'
plural = 'Users'
this.endpoint = '/api/v1/auth/users'
this.required_fields = ['uid', 'first_name', 'last_name', 'email']
this.permission_base = 'v1:auth:users'
listing_definition = {
display: `
this.item = 'User'
this.plural = 'Users'
this.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'],
},
{
name: 'Trap',
field: 'trap',
type: 'select.dynamic',
options: {
resource: 'auth/Trap',
display: 'name',
value: 'trap',
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,
},
],
}
this.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'],
},
{
name: 'Trap',
field: 'trap',
type: 'select.dynamic',
options: {
resource: 'auth/Trap',
display: 'name',
value: 'trap',
},
},
],
}
}
}

View File

@@ -2,15 +2,18 @@ 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'
constructor() {
super()
item = 'IAM Policy'
plural = 'IAM Policies'
this.endpoint = '/api/v1/iam/policy'
this.required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type']
this.permission_base = 'v1:iam:policy'
listing_definition = {
display: `
this.item = 'IAM Policy'
this.plural = 'IAM Policies'
this.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.
@@ -24,136 +27,137 @@ class PolicyResource extends CRUDBase {
</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,
},
],
}
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',
this.form_definition = {
fields: [
{
name: 'Subject Type',
field: 'entity_type',
required: true,
type: 'select',
options: [
{display: 'User', value: 'user'},
{display: 'Group', value: 'group'},
],
},
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',
{
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',
},
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' },
{ display: 'API Scope', value: 'api_scope' },
],
},
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'App',
display: 'name',
value: 'id',
{
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',
},
if: (form_data) => form_data.target_type === 'application'
},
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'reflect/Scope',
display: 'scope',
value: 'scope',
{
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'},
],
},
if: (form_data) => form_data.target_type === 'api_scope'
},
],
/*handlers: {
insert: {
action: 'back',
},
update: {
action: 'back',
},
},*/
{
name: 'Target Type',
field: 'target_type',
required: true,
type: 'select',
options: [
{display: 'Application', value: 'application'},
{display: 'API Scope', value: 'api_scope'},
],
},
{
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'
},
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'reflect/Scope',
display: 'scope',
value: 'scope',
},
if: (form_data) => form_data.target_type === 'api_scope'
},
],
/*handlers: {
insert: {
action: 'back',
},
update: {
action: 'back',
},
},*/
}
}
}

View File

@@ -2,83 +2,88 @@ 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'
constructor() {
super()
item = 'LDAP Client'
plural = 'LDAP Clients'
this.endpoint = '/api/v1/ldap/clients'
this.required_fields = ['name', 'uid', 'password']
this.permission_base = 'v1:ldap:clients'
async server_config() {
const results = await axios.get('/api/v1/ldap/config')
if ( results && results.data && results.data.data ) return results.data.data
}
this.item = 'LDAP Client'
this.plural = 'LDAP Clients'
listing_definition = {
display: `
this.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',
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',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
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',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.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',
},
],
}
}
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',
},
],
async server_config() {
const results = await axios.get('/api/v1/ldap/config')
if (results && results.data && results.data.data) return results.data.data
}
}

View File

@@ -1,97 +1,101 @@
import CRUDBase from '../CRUDBase.js'
class GroupResource extends CRUDBase {
endpoint = '/api/v1/ldap/groups'
required_fields = ['name', 'role']
permission_base = 'v1:ldap:groups'
constructor() {
super()
item = 'LDAP Group'
plural = 'LDAP Groups'
this.endpoint = '/api/v1/ldap/groups'
this.required_fields = ['name', 'role']
this.permission_base = 'v1: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,
},
],
}
this.item = 'LDAP Group'
this.plural = 'LDAP Groups'
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',
this.listing_definition = {
columns: [
{
name: 'Group Name',
field: 'name',
},
// 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',
{
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,
},
],
}
this.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',
},
},
],
}
}
}

View File

@@ -2,104 +2,108 @@ 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'
constructor() {
super()
item = 'OAuth2 Client'
plural = 'OAuth2 Clients'
this.endpoint = '/api/v1/oauth/clients'
this.required_fields = ['name', 'redirect_url', 'api_scopes']
this.permission_base = 'v1:oauth:clients'
listing_definition = {
display: `
this.item = 'OAuth2 Client'
this.plural = 'OAuth2 Clients'
this.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',
columns: [
{
name: 'Client Name',
field: 'name',
},
required: true,
},
{
name: 'Client ID',
field: 'uuid',
type: 'text',
readonly: true,
hidden: ['insert'],
},
{
name: 'Client Secret',
field: 'secret',
type: 'text',
readonly: true,
hidden: ['insert'],
},
],
{
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,
},
],
}
this.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'],
},
],
}
}
}

View File

@@ -2,94 +2,98 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'
class ClientResource extends CRUDBase {
endpoint = '/openid/clients'
required_fields = ['client_name', 'grant_types', 'redirect_uri']
permission_base = 'v1:openid:clients'
constructor() {
super()
item = 'OpenID Connect Client'
plural = 'OpenID Connect Clients'
this.endpoint = '/openid/clients'
this.required_fields = ['client_name', 'grant_types', 'redirect_uri']
this.permission_base = 'v1:openid:clients'
listing_definition = {
display: `
this.item = 'OpenID Connect Client'
this.plural = 'OpenID Connect Clients'
this.listing_definition = {
display: `
OpenID Connect clients are applications that support authentication over the OpenID Connect 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, the application need only comply with the OpenID standards.
`,
columns: [
{
name: 'Client Name',
field: 'client_name',
},
{
name: 'Redirect URI',
field: 'redirect_uri',
},
],
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,
},
],
}
columns: [
{
name: 'Client Name',
field: 'client_name',
},
{
name: 'Redirect URI',
field: 'redirect_uri',
},
],
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: 'client_name',
placeholder: 'Awesome External App',
required: true,
type: 'text',
},
{
name: 'Redirect URI',
field: 'redirect_uri',
placeholder: 'https://awesome.app/oauth2/callback',
required: true,
type: 'text',
},
{
name: 'Grant Types',
field: 'grant_types',
type: 'select.multiple',
options: [
{ display: 'Refresh Token', value: 'refresh_token' },
{ display: 'Authorization Code', value: 'authorization_code' },
],
required: true,
},
{
name: 'Client ID',
field: 'client_id',
type: 'text',
readonly: true,
hidden: ['insert'],
},
{
name: 'Client Secret',
field: 'client_secret',
type: 'text',
readonly: true,
hidden: ['insert'],
},
],
this.form_definition = {
fields: [
{
name: 'Client Name',
field: 'client_name',
placeholder: 'Awesome External App',
required: true,
type: 'text',
},
{
name: 'Redirect URI',
field: 'redirect_uri',
placeholder: 'https://awesome.app/oauth2/callback',
required: true,
type: 'text',
},
{
name: 'Grant Types',
field: 'grant_types',
type: 'select.multiple',
options: [
{display: 'Refresh Token', value: 'refresh_token'},
{display: 'Authorization Code', value: 'authorization_code'},
],
required: true,
},
{
name: 'Client ID',
field: 'client_id',
type: 'text',
readonly: true,
hidden: ['insert'],
},
{
name: 'Client Secret',
field: 'client_secret',
type: 'text',
readonly: true,
hidden: ['insert'],
},
],
}
}
}

View File

@@ -1,12 +1,16 @@
import CRUDBase from '../CRUDBase.js'
class ScopeResource extends CRUDBase {
endpoint = '/api/v1/reflect/scopes'
required_fields = ['scope']
permission_base = 'v1:reflect:scopes'
constructor() {
super()
item = 'API Scope'
plural = 'API Scopes'
this.endpoint = '/api/v1/reflect/scopes'
this.required_fields = ['scope']
this.permission_base = 'v1:reflect:scopes'
this.item = 'API Scope'
this.plural = 'API Scopes'
}
}
const reflect_scope = new ScopeResource()

View File

@@ -1,87 +1,90 @@
import CRUDBase from '../CRUDBase.js'
class TokenResource extends CRUDBase {
endpoint = '/api/v1/reflect/tokens'
required_fields = ['client_id']
permission_base = 'v1:reflect:tokens'
constructor() {
super()
this.endpoint = '/api/v1/reflect/tokens'
this.required_fields = ['client_id']
this.permission_base = 'v1:reflect:tokens'
item = 'API Token'
plural = 'API Tokens'
this.item = 'API Token'
this.plural = 'API Tokens'
listing_definition = {
display: `
this.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',
columns: [
{
name: 'Token',
field: 'token',
},
},
{
name: 'Bearer Token',
field: 'token',
type: 'text',
readonly: true,
hidden: ['insert'],
},
{
name: 'Expires',
field: 'expires',
type: 'text',
readonly: true,
hidden: ['insert'],
},
],
{
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,
},
],
}
this.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'],
},
],
}
}
}

View File

@@ -2,92 +2,96 @@ 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'
constructor() {
super()
item = 'SAML Service Provider'
plural = 'SAML Service Providers'
this.endpoint = '/api/v1/saml/providers'
this.required_fields = ['name', 'acs_url', 'entity_id']
this.permission_base = 'v1:saml: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.
this.item = 'SAML Service Provider'
this.plural = 'SAML Service Providers'
this.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',
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',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
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',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
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',
},
],
this.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',
},
],
}
}
}

View File

@@ -1,95 +1,99 @@
import CRUDBase from '../CRUDBase.js'
class AnnouncementResource extends CRUDBase {
endpoint = '/api/v1/system/announcements'
required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type']
permission_base = 'v1:system:announcements'
constructor() {
super()
item = 'System Announcement'
plural = 'System Announcements'
this.endpoint = '/api/v1/system/announcements'
this.required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type']
this.permission_base = 'v1:system:announcements'
listing_definition = {
display: `
this.item = 'System Announcement'
this.plural = 'System Announcements'
this.listing_definition = {
display: `
System announcements are administrative messages that you want all or some of your users to see. These messages can be delivered via e-mail, as a message after login, or as a system banner announcement.
`,
columns: [
{
name: 'Title',
field: 'title',
},
{
name: 'Message',
field: 'message',
renderer: (message) => String(message).slice(0, 150),
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
columns: [
{
name: 'Title',
field: 'title',
},
{
name: 'Message',
field: 'message',
renderer: (message) => String(message).slice(0, 150),
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
form_definition = {
fields: [
{
name: 'Title',
field: 'title',
type: 'text',
},
{
name: 'Message',
field: 'message',
type: 'textarea',
},
{
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',
this.form_definition = {
fields: [
{
name: 'Title',
field: 'title',
type: 'text',
},
},
{
name: 'Groups',
field: 'group_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'auth/Group',
display: (group) => `${group.name}`,
value: 'id',
{
name: 'Message',
field: 'message',
type: 'textarea',
},
},
{
name: 'Type',
field: 'type',
type: 'select',
options: [
{ display: 'Login Intercept', value: 'login' },
{ display: 'E-Mail', value: 'email' },
{ display: 'System Banner', value: 'banner' },
],
},
],
handlers: {
insert: {
action: 'redirect',
next: '/dash/c/listing/system/Announcement',
},
{
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',
},
},
{
name: 'Groups',
field: 'group_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'auth/Group',
display: (group) => `${group.name}`,
value: 'id',
},
},
{
name: 'Type',
field: 'type',
type: 'select',
options: [
{display: 'Login Intercept', value: 'login'},
{display: 'E-Mail', value: 'email'},
{display: 'System Banner', value: 'banner'},
],
},
],
handlers: {
insert: {
action: 'redirect',
next: '/dash/c/listing/system/Announcement',
},
}
}
}
}

View File

@@ -1,9 +1,9 @@
class Event {
firings = []
subscriptions = []
constructor(name) {
this.name = name
this.firings = []
this.subscriptions = []
}
subscribe(handler) {
@@ -22,7 +22,9 @@ class Event {
}
class EventBusService {
_events = {}
constructor() {
this._events = {}
}
event(name) {
if ( !this._events[name] ) {

View File

@@ -2,7 +2,9 @@ import { event_bus } from './EventBus.service.js'
import { auth_api } from './AuthApi.service.js'
class MessageService {
listener_interval = 25000
constructor() {
this.listener_interval = 25000
}
alert({type, message, timeout = 0, on_dismiss = () => {} }) {
event_bus.event('message.alert').fire({ type, message, timeout, on_dismiss })

View File

@@ -1,5 +1,7 @@
class Session {
data = {}
constructor() {
this.data = {}
}
init(data) {
this.data = data

View File

@@ -1,5 +1,7 @@
class TranslateService {
_cache = {}
constructor() {
this._cache = {}
}
check_cache(...keys) {
const obj = {}

View File

@@ -1,5 +1,7 @@
class UtilityService {
_debounce_timeouts = {}
constructor() {
this._debounce_timeouts = {}
}
uuid() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>

35
app/assets/error-log.js Normal file
View File

@@ -0,0 +1,35 @@
window.COREID_ERROR_LOG_URL = window.COREID_ERROR_LOG_URL || '/api/v1/log-error'
async function logError(error) {
try {
await fetch(window.COREID_ERROR_LOG_URL, {
method: 'POST',
cache: 'no-cache',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
full_url: window.location.href,
trace: [
error.name + ': ' + error.message,
error.stack,
].join('\n')
}),
})
} catch (e) {}
}
;(function() {
var old_onerror = window.onerror
window.onerror = function(msg, src, line, col, error) {
logError(error).then(function() {
if ( typeof old_onerror === 'function' ) {
try {
old_onerror(msg, src, line, col, error)
} catch(e) {}
}
})
}
})()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -18,6 +18,11 @@ class CoreIDAdapter {
expiresAt = new Date(Date.now() + (expiresIn * 1000))
}
if ( payload.uid ) {
payload.originalUid = payload.uid
payload.uid = payload.uid.toLowerCase()
}
await this.coll().updateOne(
{ _id },
{ $set: { payload, ...(expiresAt ? { expiresAt } : undefined) } },
@@ -34,6 +39,11 @@ class CoreIDAdapter {
).limit(1).next()
if (!result) return undefined
if ( result?.payload?.originalUid ) {
result.payload.uid = result.payload.originalUid
}
return result.payload
}
@@ -49,11 +59,16 @@ class CoreIDAdapter {
async findByUid(uid) {
const result = await this.coll().find(
{ 'payload.uid': uid },
{ 'payload.uid': uid.toLowerCase() },
{ payload: 1 },
).limit(1).next()
if (!result) return undefined
if ( result?.payload?.originalUid ) {
result.payload.uid = result.payload.originalUid
}
return result.payload
}

View File

@@ -29,6 +29,12 @@ class Home extends Controller {
async tmpl(req, res) {
return res.page('tmpl', {...this.Vue.data(), ...this.Vue.session(req)})
}
async log_front_end_error(req, res, next) {
const FrontEndError = this.models.get('FrontEndError')
await FrontEndError.log(req)
return res.api()
}
}
module.exports = Home

View File

@@ -119,14 +119,12 @@ class OpenIDController extends Controller {
uid, prompt, params, session,
} = await this.openid_connect.provider.interactionDetails(req, res)
console.log({uid, prompt, params, session})
const name = prompt.name
if ( typeof this[name] !== 'function' ) {
return this.fail(res, 'Sorry, something has gone wrong.')
}
return this[name](req, res, { uid: uid, prompt, params, session })
return this[name](req, res, { uid: uid.toLowerCase(), prompt, params, session })
}
async consent(req, res, { uid, prompt, params, session }) {
@@ -142,13 +140,13 @@ class OpenIDController extends Controller {
const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ openid_client_ids: params.client_id })
if ( !application ) {
this.output.warning('IAM Denial!')
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
next_destination: '/dash',
})
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warning('IAM Denial!')
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
@@ -172,7 +170,7 @@ class OpenIDController extends Controller {
{
text: req.T('common.grant'),
action: 'redirect',
next: `/openid/interaction/${uid}/grant`,
next: `/openid/interaction/${uid.toLowerCase()}/grant`,
},
],
})
@@ -180,7 +178,7 @@ class OpenIDController extends Controller {
}
async login(req, res, { uid, prompt, params, session }) {
return res.redirect(`/openid/interaction/${uid}/start-session`)
return res.redirect(`/openid/interaction/${uid.toLowerCase()}/start-session`)
}
/**
@@ -202,13 +200,13 @@ class OpenIDController extends Controller {
const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ openid_client_ids: params.client_id })
if ( !application ) {
this.output.warning('IAM Denial!')
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
next_destination: '/dash',
})
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warning('IAM Denial!')
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
@@ -238,13 +236,13 @@ class OpenIDController extends Controller {
const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ openid_client_ids: params.client_id })
if ( !application ) {
this.output.warning('IAM Denial!')
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
next_destination: '/dash',
})
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warning('IAM Denial!')
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',

View File

@@ -8,7 +8,7 @@ const Oauth2Controller = require('flitter-auth/controllers/Oauth2')
*/
class Oauth2 extends Oauth2Controller {
static get services() {
return [...super.services, 'Vue', 'configs', 'models']
return [...super.services, 'Vue', 'configs', 'models', 'output']
}
async authorize_post(req, res, next) {
@@ -18,6 +18,24 @@ class Oauth2 extends Oauth2Controller {
const StarshipClient = this.models.get('oauth:Client')
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID })
// Make sure the user has IAM access before proceeding
const Application = this.models.get('Application')
const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
if ( !application ) {
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
})
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
})
}
req.user.authorize(starship_client)
await req.user.save()
return super.authorize_post(req, res, next)
@@ -31,6 +49,24 @@ class Oauth2 extends Oauth2Controller {
const StarshipClient = this.models.get('oauth:Client')
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID })
// Make sure the user has IAM access before proceeding
const Application = this.models.get('Application')
const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
if ( !application ) {
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
})
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
})
}
if ( req.user.has_authorized(starship_client) ) {
return this.Vue.invoke_action(res, {
text: 'Grant Access',

View File

@@ -0,0 +1,29 @@
const { Model } = require('flitter-orm')
class FrontEndErrorModel extends Model {
static get schema() {
return {
user_agent: String,
logged_at: { type: Date, default: () => new Date },
user_id: String,
session_id: String,
full_url: String,
trace: String,
}
}
static async log(request) {
const err = new this({
user_agent: request.get('user-agent'),
user_id: request?.user?.id,
session_id: request.sessionID,
full_url: request.body.full_url,
trace: request.body.trace,
})
await err.save()
return err
}
}
module.exports = exports = FrontEndErrorModel

View File

@@ -8,6 +8,7 @@ class PermissionMiddleware extends Middleware {
async test(req, res, next, { check }) {
const Policy = this.models.get('iam:Policy')
if ( !req.additional_api_log_data ) req.additional_api_log_data = {}
req.additional_api_log_data.permission_check = check
// If the request was authorized using an OAuth2 bearer token,

View File

@@ -59,6 +59,10 @@ const index = {
'middleware::auth:GuestOnly',
'controller::api:v1:Password.request_reset',
],
'/api/v1/log-error': [
'controller::Home.log_front_end_error'
],
},
}

View File

@@ -15,6 +15,7 @@ html(lang='en')
.app-container
block app
block script
script(src='/assets/error-log.js')
script(src='/assets/lib/axios/axios.min.js')
script(src='/assets/lib/jquery/jquery-3.4.1.slim.min.js')
script(src='/assets/lib/popper/popper-1.16.0.min.js')