20 Commits
ci-10 ... ci-24

Author SHA1 Message Date
1d5c00768c Update flitter jobs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-12-03 20:00:40 -06:00
7f1c9ec9a8 Update libflitter
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-29 08:52:13 -05:00
fe0a4d5991 Update libflitter and set session max age
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-29 08:46:30 -05:00
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
8f1bbfef56 OpenID - revert case insensitive session UID lookup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:26:52 -05:00
e400e16ccc OpenID - revert case insensitive cast to UID
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:22:09 -05:00
97096f619f Make UID case-insensitive
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-18 23:27:23 -05:00
2d97b77bbf Fix user bind error constructor
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 23:04:01 -05:00
5916222f7b Update libflitte
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 22:11:23 -05:00
bb79d52911 Increase error stack trace limit
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-18 22:07:12 -05:00
2e05ec77c8 Increase error stack trace limit
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-18 22:06:08 -05:00
433af8261f Add debug output
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-18 21:44:53 -05:00
70 changed files with 1582 additions and 1311 deletions

View File

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

View File

@@ -28,12 +28,16 @@ export default class MFADisableComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return [] } static get props() { return [] }
app_name = '' constructor() {
step = 0 super()
loading = false
error_message = '' this.app_name = ''
other_message = '' this.step = 0
t = {} this.loading = false
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.app_name = session.get('app.name') 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 template() { return template }
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
verify_success = false constructor() {
loading = false super()
recovery_code = ''
error_message = '' this.verify_success = false
other_message = '' this.loading = false
t = {} this.recovery_code = ''
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.t = await T( this.t = await T(

View File

@@ -61,19 +61,23 @@ export default class MFASetupPage extends Component {
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
static get template() { return template } static get template() { return template }
loading = false constructor() {
step = 0 super()
qr_data = '' this.loading = false
otpauth_url = '' this.step = 0
secret = ''
verify_code = ''
verify_success = false this.qr_data = ''
this.otpauth_url = ''
this.secret = ''
this.verify_code = ''
error_message = '' this.verify_success = false
other_message = ''
t = {} this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.t = await T( 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 props() { return ['app_name', 'message', 'actions'] }
static get template() { return template } static get template() { return template }
loading = false constructor() {
super()
this.loading = false
}
async action_click(index) { async action_click(index) {
this.loading = true this.loading = true

View File

@@ -78,23 +78,27 @@ export default class PasswordResetComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
step = 0 constructor() {
loading = false super()
has_mfa = false
error_message = '' this.step = 0
other_message = '' this.loading = false
this.has_mfa = false
step_1_valid = false this.error_message = ''
step_1_calc_time = '' this.other_message = ''
step_1_problem = ''
step_2_valid = false this.step_1_valid = false
this.step_1_calc_time = ''
this.step_1_problem = ''
password = '' this.step_2_valid = false
confirm_password = ''
t = {} this.password = ''
ready = false this.confirm_password = ''
this.t = {}
this.ready = false
}
async vue_on_create() { async vue_on_create() {
this.has_mfa = !!session.get('user.has_mfa') 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 } static get template() { return template }
username = '' constructor() {
password = '' super()
button_text = ''
step_two = false
btn_disabled = true
loading = false
error_message = ''
other_message = ''
allow_back = true
auth_user = false
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) { watch_username(new_username, old_username) {
this.btn_disabled = !new_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 template() { return template }
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
loading = false constructor() {
step = 1 super()
other_message = ''
error_message = ''
message = ''
btn_disabled = true
button_text = ''
first_name = '' this.loading = false
last_name = '' this.step = 1
username = '' this.other_message = ''
email = '' this.error_message = ''
t = {} 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() { async vue_on_create() {
// Batch-load translated phrases // Batch-load translated phrases

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,9 +23,14 @@ export default class SideBarComponent extends Component {
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
static get template() { return template } static get template() { return template }
actions = [] constructor() {
super()
possible_actions = [ this.actions = []
this.isCollapsed = false
this.possible_actions = [
{ {
text: 'Profile', text: 'Profile',
action: 'redirect', action: 'redirect',
@@ -87,8 +92,6 @@ export default class SideBarComponent extends Component {
}, },
] ]
constructor() {
super()
event_bus.event('sidebar.toggle').subscribe(() => { event_bus.event('sidebar.toggle').subscribe(() => {
this.toggle() this.toggle()
}) })
@@ -120,8 +123,6 @@ export default class SideBarComponent extends Component {
this.actions = new_actions this.actions = new_actions
} }
isCollapsed = false
toggle() { toggle() {
this.isCollapsed = !this.isCollapsed this.isCollapsed = !this.isCollapsed
} }

View File

@@ -68,8 +68,12 @@ export default class MessageContainerComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return [] } static get props() { return [] }
messages = [] constructor() {
modals = [] super()
this.messages = []
this.modals = []
}
vue_on_create() { vue_on_create() {
this.alert_event = event_bus.event('message.alert') 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 template() { return template }
static get props() { return ['user_id'] } static get props() { return ['user_id'] }
profile_first = '' constructor() {
profile_last = '' super()
profile_email = ''
profile_tagline = ''
last_reset = ''
mfa_enable_date = ''
has_mfa_recovery = false this.profile_first = ''
mfa_recovery_date = '' this.profile_last = ''
mfa_recovery_codes = 0 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 this.form_message = 'No changes.'
ready = false
notify_gateway_url = '' this.has_mfa = false
notify_app_key = '' this.ready = false
notify_enabled = false
notify_created_on = ''
notify_loaded = false
app_passwords = [] this.notify_gateway_url = ''
app_name = '' this.notify_app_key = ''
t = {} this.notify_enabled = false
this.notify_created_on = ''
this.notify_loaded = false
this.app_passwords = []
this.app_name = ''
this.t = {}
}
on_key_up = ($event) => {} on_key_up = ($event) => {}

View File

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

View File

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

View File

@@ -2,14 +2,17 @@ import CRUDBase from './CRUDBase.js'
import { session } from '../service/Session.service.js' import { session } from '../service/Session.service.js'
class AppResource extends CRUDBase { class AppResource extends CRUDBase {
endpoint = '/api/v1/applications' constructor() {
required_fields = ['name', 'identifier'] super()
permission_base = 'v1:applications'
item = 'Application' this.endpoint = '/api/v1/applications'
plural = 'Applications' this.required_fields = ['name', 'identifier']
this.permission_base = 'v1:applications'
listing_definition = { this.item = 'Application'
this.plural = 'Applications'
this.listing_definition = {
display: ` 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. 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.
`, `,
@@ -60,7 +63,7 @@ class AppResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Name', name: 'Name',
@@ -123,6 +126,7 @@ class AppResource extends CRUDBase {
}, },
], ],
} }
}
} }
const app = new AppResource() const app = new AppResource()

View File

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

View File

@@ -2,14 +2,17 @@ import CRUDBase from './CRUDBase.js'
import { session } from '../service/Session.service.js' import { session } from '../service/Session.service.js'
class SettingResource extends CRUDBase { class SettingResource extends CRUDBase {
endpoint = '/api/v1/settings' constructor() {
required_fields = ['key', 'value'] super()
permission_base = 'v1:settings'
item = 'Setting' this.endpoint = '/api/v1/settings'
plural = 'Settings' this.required_fields = ['key', 'value']
this.permission_base = 'v1:settings'
listing_definition = { this.item = 'Setting'
this.plural = 'Settings'
this.listing_definition = {
display: ` 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> <p>These are advanced settings that allow you to tweak the way ${session.get('app.name')} behaves. Tweak them at your own risk.</p>
`, `,
@@ -35,7 +38,7 @@ class SettingResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Setting Key', name: 'Setting Key',
@@ -50,6 +53,7 @@ class SettingResource extends CRUDBase {
}, },
], ],
} }
}
} }
const setting = new SettingResource() const setting = new SettingResource()

View File

@@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class GroupResource extends CRUDBase { class GroupResource extends CRUDBase {
endpoint = '/api/v1/auth/groups' constructor() {
required_fields = ['name'] super()
permission_base = 'v1:auth:groups'
item = 'Group' this.endpoint = '/api/v1/auth/groups'
plural = 'Groups' this.required_fields = ['name']
this.permission_base = 'v1:auth:groups'
listing_definition = { this.item = 'Group'
this.plural = 'Groups'
this.listing_definition = {
display: ` 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. 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.
`, `,
@@ -50,7 +53,7 @@ class GroupResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Name', name: 'Name',
@@ -71,6 +74,7 @@ class GroupResource extends CRUDBase {
}, },
], ],
} }
}
} }
const auth_group = new GroupResource() const auth_group = new GroupResource()

View File

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

View File

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

View File

@@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class UserResource extends CRUDBase { class UserResource extends CRUDBase {
endpoint = '/api/v1/auth/users' constructor() {
required_fields = ['uid', 'first_name', 'last_name', 'email'] super()
permission_base = 'v1:auth:users'
item = 'User' this.endpoint = '/api/v1/auth/users'
plural = 'Users' this.required_fields = ['uid', 'first_name', 'last_name', 'email']
this.permission_base = 'v1:auth:users'
listing_definition = { this.item = 'User'
this.plural = 'Users'
this.listing_definition = {
display: ` 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. 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.
`, `,
@@ -57,7 +60,7 @@ class UserResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'First Name', name: 'First Name',
@@ -111,6 +114,7 @@ class UserResource extends CRUDBase {
}, },
], ],
} }
}
} }
const auth_user = new UserResource() const auth_user = new UserResource()

View File

@@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class PolicyResource extends CRUDBase { class PolicyResource extends CRUDBase {
endpoint = '/api/v1/iam/policy' constructor() {
required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type'] super()
permission_base = 'v1:iam:policy'
item = 'IAM Policy' this.endpoint = '/api/v1/iam/policy'
plural = 'IAM Policies' this.required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type']
this.permission_base = 'v1:iam:policy'
listing_definition = { this.item = 'IAM Policy'
this.plural = 'IAM Policies'
this.listing_definition = {
display: ` 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. 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> <br><br>
@@ -65,7 +68,7 @@ class PolicyResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Subject Type', name: 'Subject Type',
@@ -73,8 +76,8 @@ class PolicyResource extends CRUDBase {
required: true, required: true,
type: 'select', type: 'select',
options: [ options: [
{ display: 'User', value: 'user' }, {display: 'User', value: 'user'},
{ display: 'Group', value: 'group' }, {display: 'Group', value: 'group'},
], ],
}, },
{ {
@@ -107,8 +110,8 @@ class PolicyResource extends CRUDBase {
required: true, required: true,
type: 'select', type: 'select',
options: [ options: [
{ display: '...is granted access to...', value: 'allow' }, {display: '...is granted access to...', value: 'allow'},
{ display: '...is denied access to...', value: 'deny' }, {display: '...is denied access to...', value: 'deny'},
], ],
}, },
{ {
@@ -117,8 +120,8 @@ class PolicyResource extends CRUDBase {
required: true, required: true,
type: 'select', type: 'select',
options: [ options: [
{ display: 'Application', value: 'application' }, {display: 'Application', value: 'application'},
{ display: 'API Scope', value: 'api_scope' }, {display: 'API Scope', value: 'api_scope'},
], ],
}, },
{ {
@@ -155,6 +158,7 @@ class PolicyResource extends CRUDBase {
}, },
},*/ },*/
} }
}
} }
const iam_policy = new PolicyResource() const iam_policy = new PolicyResource()

View File

@@ -2,19 +2,18 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class ClientResource extends CRUDBase { class ClientResource extends CRUDBase {
endpoint = '/api/v1/ldap/clients' constructor() {
required_fields = ['name', 'uid', 'password'] super()
permission_base = 'v1:ldap:clients'
item = 'LDAP Client' this.endpoint = '/api/v1/ldap/clients'
plural = 'LDAP Clients' this.required_fields = ['name', 'uid', 'password']
this.permission_base = 'v1:ldap:clients'
async server_config() { this.item = 'LDAP Client'
const results = await axios.get('/api/v1/ldap/config') this.plural = 'LDAP Clients'
if ( results && results.data && results.data.data ) return results.data.data
}
listing_definition = {
this.listing_definition = {
display: ` 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. 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> <br><br>
@@ -56,7 +55,7 @@ class ClientResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Provider Name', name: 'Provider Name',
@@ -80,6 +79,12 @@ class ClientResource extends CRUDBase {
}, },
], ],
} }
}
async server_config() {
const results = await axios.get('/api/v1/ldap/config')
if (results && results.data && results.data.data) return results.data.data
}
} }
const ldap_client = new ClientResource() const ldap_client = new ClientResource()

View File

@@ -1,14 +1,17 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class GroupResource extends CRUDBase { class GroupResource extends CRUDBase {
endpoint = '/api/v1/ldap/groups' constructor() {
required_fields = ['name', 'role'] super()
permission_base = 'v1:ldap:groups'
item = 'LDAP Group' this.endpoint = '/api/v1/ldap/groups'
plural = 'LDAP Groups' this.required_fields = ['name', 'role']
this.permission_base = 'v1:ldap:groups'
listing_definition = { this.item = 'LDAP Group'
this.plural = 'LDAP Groups'
this.listing_definition = {
columns: [ columns: [
{ {
name: 'Group Name', name: 'Group Name',
@@ -50,7 +53,7 @@ class GroupResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
// back_action: { // back_action: {
// text: 'Back', // text: 'Back',
// action: 'back', // action: 'back',
@@ -93,6 +96,7 @@ class GroupResource extends CRUDBase {
}, },
], ],
} }
}
} }
const ldap_group = new GroupResource() const ldap_group = new GroupResource()

View File

@@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'; import { session } from '../../service/Session.service.js';
class ClientResource extends CRUDBase { class ClientResource extends CRUDBase {
endpoint = '/api/v1/oauth/clients' constructor() {
required_fields = ['name', 'redirect_url', 'api_scopes'] super()
permission_base = 'v1:oauth:clients'
item = 'OAuth2 Client' this.endpoint = '/api/v1/oauth/clients'
plural = 'OAuth2 Clients' this.required_fields = ['name', 'redirect_url', 'api_scopes']
this.permission_base = 'v1:oauth:clients'
listing_definition = { this.item = 'OAuth2 Client'
this.plural = 'OAuth2 Clients'
this.listing_definition = {
display: ` 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. 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> <br><br>
@@ -58,7 +61,7 @@ class ClientResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Client Name', name: 'Client Name',
@@ -101,6 +104,7 @@ class ClientResource extends CRUDBase {
}, },
], ],
} }
}
} }
const oauth_client = new ClientResource() const oauth_client = new ClientResource()

View File

@@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class ClientResource extends CRUDBase { class ClientResource extends CRUDBase {
endpoint = '/openid/clients' constructor() {
required_fields = ['client_name', 'grant_types', 'redirect_uri'] super()
permission_base = 'v1:openid:clients'
item = 'OpenID Connect Client' this.endpoint = '/openid/clients'
plural = 'OpenID Connect Clients' this.required_fields = ['client_name', 'grant_types', 'redirect_uri']
this.permission_base = 'v1:openid:clients'
listing_definition = { this.item = 'OpenID Connect Client'
this.plural = 'OpenID Connect Clients'
this.listing_definition = {
display: ` 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. 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.
`, `,
@@ -49,7 +52,7 @@ class ClientResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Client Name', name: 'Client Name',
@@ -70,8 +73,8 @@ class ClientResource extends CRUDBase {
field: 'grant_types', field: 'grant_types',
type: 'select.multiple', type: 'select.multiple',
options: [ options: [
{ display: 'Refresh Token', value: 'refresh_token' }, {display: 'Refresh Token', value: 'refresh_token'},
{ display: 'Authorization Code', value: 'authorization_code' }, {display: 'Authorization Code', value: 'authorization_code'},
], ],
required: true, required: true,
}, },
@@ -91,6 +94,7 @@ class ClientResource extends CRUDBase {
}, },
], ],
} }
}
} }
const openid_client = new ClientResource() const openid_client = new ClientResource()

View File

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

View File

@@ -1,14 +1,16 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class TokenResource extends CRUDBase { class TokenResource extends CRUDBase {
endpoint = '/api/v1/reflect/tokens' constructor() {
required_fields = ['client_id'] super()
permission_base = 'v1:reflect:tokens' this.endpoint = '/api/v1/reflect/tokens'
this.required_fields = ['client_id']
this.permission_base = 'v1:reflect:tokens'
item = 'API Token' this.item = 'API Token'
plural = 'API Tokens' this.plural = 'API Tokens'
listing_definition = { this.listing_definition = {
display: ` 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. 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> <br><br>
@@ -54,7 +56,7 @@ class TokenResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Client', name: 'Client',
@@ -83,6 +85,7 @@ class TokenResource extends CRUDBase {
}, },
], ],
} }
}
} }
const reflect_token = new TokenResource() const reflect_token = new TokenResource()

View File

@@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class ProviderResource extends CRUDBase { class ProviderResource extends CRUDBase {
endpoint = '/api/v1/saml/providers' constructor() {
required_fields = ['name', 'acs_url', 'entity_id'] super()
permission_base = 'v1:saml:providers'
item = 'SAML Service Provider' this.endpoint = '/api/v1/saml/providers'
plural = 'SAML Service Providers' this.required_fields = ['name', 'acs_url', 'entity_id']
this.permission_base = 'v1:saml:providers'
listing_definition = { 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. 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> <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).`, To do this, you need to know the SAML service provider's entity ID, assertion consumer service URL, and single-logout URL (if supported).`,
@@ -58,7 +61,7 @@ class ProviderResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Provider Name', name: 'Provider Name',
@@ -89,6 +92,7 @@ class ProviderResource extends CRUDBase {
}, },
], ],
} }
}
} }
const saml_provider = new ProviderResource() const saml_provider = new ProviderResource()

View File

@@ -1,14 +1,17 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class AnnouncementResource extends CRUDBase { class AnnouncementResource extends CRUDBase {
endpoint = '/api/v1/system/announcements' constructor() {
required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type'] super()
permission_base = 'v1:system:announcements'
item = 'System Announcement' this.endpoint = '/api/v1/system/announcements'
plural = 'System Announcements' this.required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type']
this.permission_base = 'v1:system:announcements'
listing_definition = { this.item = 'System Announcement'
this.plural = 'System Announcements'
this.listing_definition = {
display: ` 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. 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.
`, `,
@@ -42,7 +45,7 @@ class AnnouncementResource extends CRUDBase {
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Title', name: 'Title',
@@ -79,9 +82,9 @@ class AnnouncementResource extends CRUDBase {
field: 'type', field: 'type',
type: 'select', type: 'select',
options: [ options: [
{ display: 'Login Intercept', value: 'login' }, {display: 'Login Intercept', value: 'login'},
{ display: 'E-Mail', value: 'email' }, {display: 'E-Mail', value: 'email'},
{ display: 'System Banner', value: 'banner' }, {display: 'System Banner', value: 'banner'},
], ],
}, },
], ],
@@ -92,6 +95,7 @@ class AnnouncementResource extends CRUDBase {
}, },
} }
} }
}
} }
const system_announcement = new AnnouncementResource() const system_announcement = new AnnouncementResource()

View File

@@ -1,9 +1,9 @@
class Event { class Event {
firings = []
subscriptions = []
constructor(name) { constructor(name) {
this.name = name this.name = name
this.firings = []
this.subscriptions = []
} }
subscribe(handler) { subscribe(handler) {
@@ -22,7 +22,9 @@ class Event {
} }
class EventBusService { class EventBusService {
_events = {} constructor() {
this._events = {}
}
event(name) { event(name) {
if ( !this._events[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' import { auth_api } from './AuthApi.service.js'
class MessageService { class MessageService {
listener_interval = 25000 constructor() {
this.listener_interval = 25000
}
alert({type, message, timeout = 0, on_dismiss = () => {} }) { alert({type, message, timeout = 0, on_dismiss = () => {} }) {
event_bus.event('message.alert').fire({ type, message, timeout, on_dismiss }) event_bus.event('message.alert').fire({ type, message, timeout, on_dismiss })

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
class UtilityService { class UtilityService {
_debounce_timeouts = {} constructor() {
this._debounce_timeouts = {}
}
uuid() { uuid() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => 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)) expiresAt = new Date(Date.now() + (expiresIn * 1000))
} }
if ( payload.uid ) {
payload.originalUid = payload.uid
payload.uid = payload.uid.toLowerCase()
}
await this.coll().updateOne( await this.coll().updateOne(
{ _id }, { _id },
{ $set: { payload, ...(expiresAt ? { expiresAt } : undefined) } }, { $set: { payload, ...(expiresAt ? { expiresAt } : undefined) } },
@@ -34,6 +39,11 @@ class CoreIDAdapter {
).limit(1).next() ).limit(1).next()
if (!result) return undefined if (!result) return undefined
if ( result?.payload?.originalUid ) {
result.payload.uid = result.payload.originalUid
}
return result.payload return result.payload
} }
@@ -49,11 +59,16 @@ class CoreIDAdapter {
async findByUid(uid) { async findByUid(uid) {
const result = await this.coll().find( const result = await this.coll().find(
{ 'payload.uid': uid }, { 'payload.uid': uid.toLowerCase() },
{ payload: 1 }, { payload: 1 },
).limit(1).next() ).limit(1).next()
if (!result) return undefined if (!result) return undefined
if ( result?.payload?.originalUid ) {
result.payload.uid = result.payload.originalUid
}
return result.payload return result.payload
} }

View File

@@ -43,7 +43,7 @@ class FlitterProfileMapper {
getClaims() { getClaims() {
const claims = {} const claims = {}
claims[this.map.nameIdentifier] = this.user.uid claims[this.map.nameIdentifier] = this.user.uid.toLowerCase()
claims[this.map.email] = this.user.email claims[this.map.email] = this.user.email
claims[this.map.name] = `${this.user.first_name} ${this.user.last_name}` claims[this.map.name] = `${this.user.first_name} ${this.user.last_name}`
claims[this.map.givenname] = this.user.first_name claims[this.map.givenname] = this.user.first_name
@@ -54,7 +54,7 @@ class FlitterProfileMapper {
} }
getNameIdentifier() { getNameIdentifier() {
return { nameIdentifier: this.user.uid } return { nameIdentifier: this.user.uid.toLowerCase() }
} }
} }

View File

@@ -29,6 +29,12 @@ class Home extends Controller {
async tmpl(req, res) { async tmpl(req, res) {
return res.page('tmpl', {...this.Vue.data(), ...this.Vue.session(req)}) 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 module.exports = Home

View File

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

View File

@@ -71,7 +71,7 @@ class AuthController extends Controller {
const user = new User({ const user = new User({
first_name: req.body.first_name, first_name: req.body.first_name,
last_name: req.body.last_name, last_name: req.body.last_name,
uid: req.body.uid, uid: req.body.uid.toLowerCase(),
email: req.body.email, email: req.body.email,
trap: 'password_reset', // Force user to reset password trap: 'password_reset', // Force user to reset password
}) })
@@ -297,7 +297,7 @@ class AuthController extends Controller {
.api() .api()
const user = new User({ const user = new User({
uid: req.body.uid, uid: req.body.uid.toLowerCase(),
email: req.body.email, email: req.body.email,
first_name: req.body.first_name, first_name: req.body.first_name,
last_name: req.body.last_name, last_name: req.body.last_name,
@@ -417,7 +417,7 @@ class AuthController extends Controller {
user.first_name = req.body.first_name user.first_name = req.body.first_name
user.last_name = req.body.last_name user.last_name = req.body.last_name
user.uid = req.body.uid user.uid = req.body.uid.toLowerCase()
user.email = req.body.email user.email = req.body.email
if ( req.body.tagline ) if ( req.body.tagline )
@@ -493,7 +493,7 @@ class AuthController extends Controller {
if ( is_valid ) { if ( is_valid ) {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const user = await User.findOne({uid: req.body.username}) const user = await User.findOne({uid: req.body.username.toLowerCase()})
if ( !user || !user.can_login ) is_valid = false if ( !user || !user.can_login ) is_valid = false
} }
@@ -511,7 +511,7 @@ class AuthController extends Controller {
const data = {} const data = {}
if ( req.body.username ) { if ( req.body.username ) {
const existing_user = await User.findOne({ const existing_user = await User.findOne({
uid: req.body.username, uid: req.body.username.toLowerCase(),
}) })
data.username_taken = !!existing_user data.username_taken = !!existing_user
@@ -544,7 +544,8 @@ class AuthController extends Controller {
.message(req.T('auth.unable_to_complete')) .message(req.T('auth.unable_to_complete'))
.api({ errors }) .api({ errors })
const login_args = await flitter.get_login_args(req.body) const [username, ...other_args] = await flitter.get_login_args(req.body)
const login_args = [username.toLowerCase(), ...other_args]
const user = await flitter.login.apply(flitter, login_args) const user = await flitter.login.apply(flitter, login_args)
if ( !user ) if ( !user )

View File

@@ -96,7 +96,7 @@ class LDAPController extends Controller {
// Make sure the uid is free // Make sure the uid is free
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const existing_user = await User.findOne({ uid: req.body.uid }) const existing_user = await User.findOne({ uid: req.body.uid.toLowerCase() })
if ( existing_user ) if ( existing_user )
return res.status(400) return res.status(400)
.message(req.T('api.user_already_exists')) .message(req.T('api.user_already_exists'))
@@ -113,7 +113,7 @@ class LDAPController extends Controller {
// Create the client // Create the client
const Client = this.models.get('ldap:Client') const Client = this.models.get('ldap:Client')
const client = await Client.create({ const client = await Client.create({
uid: req.body.uid, uid: req.body.uid.toLowerCase(),
password: req.body.password, password: req.body.password,
name: req.body.name, name: req.body.name,
}) })
@@ -210,16 +210,16 @@ class LDAPController extends Controller {
} }
// Update the uid // Update the uid
if ( req.body.uid !== user.uid ) { if ( req.body.uid.toLowerCase() !== user.uid ) {
// Make sure the UID is free // Make sure the UID is free
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const existing_user = await User.findOne({ uid: req.body.uid }) const existing_user = await User.findOne({ uid: req.body.uid.toLowerCase() })
if ( existing_user ) if ( existing_user )
return res.status(400) return res.status(400)
.message(req.T('api.user_already_exists')) .message(req.T('api.user_already_exists'))
.api() .api()
user.uid = req.body.uid user.uid = req.body.uid.toLowerCase()
} }
// Update the password // Update the password

View File

@@ -8,7 +8,7 @@ const Oauth2Controller = require('flitter-auth/controllers/Oauth2')
*/ */
class Oauth2 extends Oauth2Controller { class Oauth2 extends Oauth2Controller {
static get services() { static get services() {
return [...super.services, 'Vue', 'configs', 'models'] return [...super.services, 'Vue', 'configs', 'models', 'output']
} }
async authorize_post(req, res, next) { async authorize_post(req, res, next) {
@@ -18,6 +18,24 @@ class Oauth2 extends Oauth2Controller {
const StarshipClient = this.models.get('oauth:Client') const StarshipClient = this.models.get('oauth:Client')
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID }) 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) req.user.authorize(starship_client)
await req.user.save() await req.user.save()
return super.authorize_post(req, res, next) return super.authorize_post(req, res, next)
@@ -31,6 +49,24 @@ class Oauth2 extends Oauth2Controller {
const StarshipClient = this.models.get('oauth:Client') const StarshipClient = this.models.get('oauth:Client')
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID }) 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) ) { if ( req.user.has_authorized(starship_client) ) {
return this.Vue.invoke_action(res, { return this.Vue.invoke_action(res, {
text: 'Grant Access', text: 'Grant Access',

View File

@@ -67,7 +67,7 @@ class SAMLController extends Controller {
key: await this.saml.private_key(), key: await this.saml.private_key(),
protocolBinding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', protocolBinding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
clearIdPSession: done => { clearIdPSession: done => {
this.output.info(`${req.T('saml.clear_idp_session')} ${req.user.uid}`) this.output.info(`${req.T('saml.clear_idp_session')} ${req.user.uid.toLowerCase()}`)
req.saml.participants.clear().then(async () => { req.saml.participants.clear().then(async () => {
if ( this.saml.config().slo.end_coreid_session ) { if ( this.saml.config().slo.end_coreid_session ) {
await req.user.logout(req) await req.user.logout(req)

View File

@@ -50,7 +50,7 @@ class LDAPController extends Injectable {
const item = await this.get_resource_from_dn(req.dn) const item = await this.get_resource_from_dn(req.dn)
if ( !item ) { if ( !item ) {
this.output.debug(`Bind failure: ${req.dn} not found`) this.output.debug(`Bind failure: ${req.dn} not found`)
return next(new LDAP.NoSuchObject()) return next(new LDAP.NoSuchObjectError())
} }
// If the object is can-able, make sure it can bind // If the object is can-able, make sure it can bind

View File

@@ -52,7 +52,7 @@ class UsersController extends LDAPController {
first_name: req_data.cn ? req_data.cn[0] : '', first_name: req_data.cn ? req_data.cn[0] : '',
last_name: req_data.sn ? req_data.sn[0] : '', last_name: req_data.sn ? req_data.sn[0] : '',
email: req_data.mail ? req_data.mail[0] : '', email: req_data.mail ? req_data.mail[0] : '',
username: req_data.uid ? req_data.uid[0] : '', username: req_data.uid ? req_data.uid[0].toLowerCase() : '',
password: req_data.userpassword ? req_data.userpassword[0] : '', password: req_data.userpassword ? req_data.userpassword[0] : '',
} }
@@ -299,6 +299,7 @@ class UsersController extends LDAPController {
// Make sure the user is of appropriate scope // Make sure the user is of appropriate scope
if ( req.dn.equals(user.dn) || req.dn.parentOf(user.dn) ) { if ( req.dn.equals(user.dn) || req.dn.parentOf(user.dn) ) {
this.output.debug(await user.to_ldap())
this.output.debug(`Matches sub scope. Matches filter? ${req.filter.matches(await user.to_ldap(iam_targets))}`) this.output.debug(`Matches sub scope. Matches filter? ${req.filter.matches(await user.to_ldap(iam_targets))}`)
// Check if filter matches // Check if filter matches
@@ -326,7 +327,7 @@ class UsersController extends LDAPController {
try { try {
if ( typeof dn === 'string' ) dn = LDAP.parseDN(dn) if ( typeof dn === 'string' ) dn = LDAP.parseDN(dn)
return dn.rdns[0].attrs[uid_field].value return dn.rdns[0].attrs[uid_field].value.toLowerCase()
} catch (e) {} } catch (e) {}
} }
@@ -334,7 +335,7 @@ class UsersController extends LDAPController {
const uid = this.get_uid_from_dn(dn) const uid = this.get_uid_from_dn(dn)
if ( uid ) { if ( uid ) {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
return User.findOne({uid, ldap_visible: true}) return User.findOne({uid: uid.toLowerCase(), ldap_visible: true})
} }
} }
} }

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

@@ -173,7 +173,7 @@ class User extends AuthUser {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const ldap_data = { const ldap_data = {
uid: this.uid, uid: this.uid.toLowerCase(),
uuid: this.uuid, uuid: this.uuid,
cn: this.first_name, cn: this.first_name,
sn: this.last_name, sn: this.last_name,
@@ -213,7 +213,7 @@ class User extends AuthUser {
} }
get dn() { get dn() {
return LDAP.parseDN(`uid=${this.uid},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`) return LDAP.parseDN(`uid=${this.uid.toLowerCase()},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`)
} }
// The following are used by OpenID connect // The following are used by OpenID connect
@@ -227,15 +227,15 @@ class User extends AuthUser {
given_name: this.first_name, given_name: this.first_name,
locale: 'en_US', // TODO locale: 'en_US', // TODO
name: `${this.first_name} ${this.last_name}`, name: `${this.first_name} ${this.last_name}`,
preferred_username: this.uid, preferred_username: this.uid.toLowerCase(),
username: this.uid, username: this.uid.toLowerCase(),
} }
} }
static async findByLogin(login) { static async findByLogin(login) {
return this.findOne({ return this.findOne({
active: true, active: true,
uid: login, uid: login.toLowerCase(),
}) })
} }

View File

@@ -118,7 +118,7 @@ class PolicyModel extends Model {
if ( this.entity_type === 'user' ) { if ( this.entity_type === 'user' ) {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const user = await User.findById(this.entity_id) const user = await User.findById(this.entity_id)
entity_display = `User: ${user.last_name}, ${user.first_name} (${user.uid})` entity_display = `User: ${user.last_name}, ${user.first_name} (${user.uid.toLowerCase()})`
} else if ( this.entity_type === 'group' ) { } else if ( this.entity_type === 'group' ) {
const Group = this.models.get('auth:Group') const Group = this.models.get('auth:Group')
const group = await Group.findById(this.entity_id) const group = await Group.findById(this.entity_id)

View File

@@ -19,7 +19,7 @@ class ClientModel extends Model {
const user = new User({ const user = new User({
first_name: name, first_name: name,
last_name: '(LDAP Agent)', last_name: '(LDAP Agent)',
uid, uid: uid.toLowerCase(),
roles: ['ldap_client'], roles: ['ldap_client'],
}) })
@@ -58,7 +58,7 @@ class ClientModel extends Model {
id: this.id, id: this.id,
name: this.name, name: this.name,
user_id: user.id, user_id: user.id,
uid: user.uid, uid: user.uid.toLowerCase(),
last_invocation: this.last_invocation, last_invocation: this.last_invocation,
permissions: [...user.permissions, ...role_permissions], permissions: [...user.permissions, ...role_permissions],
} }

View File

@@ -17,7 +17,7 @@ class SessionParticipantStore extends Injectable {
async issue({ service_provider }) { async issue({ service_provider }) {
const sp = new this.SessionParticipant({ const sp = new this.SessionParticipant({
service_provider_id: service_provider.id, service_provider_id: service_provider.id,
name_id: this.request.user.uid, name_id: this.request.user.uid.toLowerCase(),
// session_index: this.get_index(), // session_index: this.get_index(),
slo_url: service_provider.slo_url, slo_url: service_provider.slo_url,
// TODO sp_cert, // TODO sp_cert,

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ class MFAService extends Service {
secret(user) { secret(user) {
return speakeasy.generateSecret({ return speakeasy.generateSecret({
length: this.configs.get('auth.mfa.secret_length') ?? 20, length: this.configs.get('auth.mfa.secret_length') ?? 20,
name: `${this.configs.get('app.name')} (${user.uid})`, name: `${this.configs.get('app.name')} (${user.uid.toLowerCase()})`,
}) })
} }

View File

@@ -25,7 +25,7 @@ class VueService extends Service {
user: { user: {
first_name: req.user.first_name, first_name: req.user.first_name,
last_name: req.user.last_name, last_name: req.user.last_name,
username: req.user.uid, username: req.user.uid.toLowerCase(),
email: req.user.email, email: req.user.email,
tagline: req.user.tagline, tagline: req.user.tagline,
user_id: req.user.id, user_id: req.user.id,

View File

@@ -28,7 +28,7 @@ class OpenIDConnectUnit extends Unit {
clients: [], clients: [],
interactions: { interactions: {
interactions, interactions,
url: (ctx, interaction) => `/openid/interaction/${ctx.oidc.uid}`, url: (ctx, interaction) => `/openid/interaction/${ctx.oidc.uid.toLowerCase()}`,
}, },
cookies: { cookies: {
long: { signed: true, maxAge: 24 * 60 * 60 * 1000 }, // 1 day, ms long: { signed: true, maxAge: 24 * 60 * 60 * 1000 }, // 1 day, ms

View File

@@ -10,6 +10,7 @@ class SettingsUnit extends Unit {
} }
async go(app) { async go(app) {
Error.stackTraceLimit = 50
app.express.set('trust proxy', true) app.express.set('trust proxy', true)
const Setting = this.models.get('Setting') const Setting = this.models.get('Setting')

View File

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

View File

@@ -31,6 +31,13 @@ const jobs_config = {
// worker processes of the same type. // worker processes of the same type.
// (e.g. you can have two "main" workers) // (e.g. you can have two "main" workers)
}, },
connector: {
enabled: env('JOB_QUEUE_CONNECTOR', false),
mount: env('JOB_QUEUE_CONNECTOR_MOUNT', '/job_queue_api'),
secret: env('JOB_QUEUE_CONNECTOR_SECRET'),
},
} }
module.exports = exports = jobs_config module.exports = exports = jobs_config

View File

@@ -35,7 +35,12 @@ const server_config = {
* The secret used to encrypt the session. * The secret used to encrypt the session.
* This should be set in the environment. * This should be set in the environment.
*/ */
secret: env("SECRET", "changeme") secret: env("SECRET", "changeme"),
/*
* The max age of a session in milliseconds
*/
max_age: env("SESSION_MAX_AGE", 1000 * 60 * 60 * 24 * 2), // default to 2 days
}, },
uploads: { uploads: {

View File

@@ -33,3 +33,6 @@ SMTP_PORT="587"
SMTP_USER="coreid@localhost.localdomain" SMTP_USER="coreid@localhost.localdomain"
SMTP_DEFAULT_SENDER="coreid@localhost.localdomain" SMTP_DEFAULT_SENDER="coreid@localhost.localdomain"
SMTP_PASS="something super secure" SMTP_PASS="something super secure"
JOB_QUEUE_CONNECTOR=true
JOB_QUEUE_CONNECTOR_SECRET=

View File

@@ -25,7 +25,7 @@
"flitter-forms": "^0.8.1", "flitter-forms": "^0.8.1",
"flitter-gotify": "^0.1.0", "flitter-gotify": "^0.1.0",
"flitter-i18n": "^0.1.1", "flitter-i18n": "^0.1.1",
"flitter-jobs": "^0.1.2", "flitter-jobs": "^0.3.0",
"flitter-less": "^0.5.3", "flitter-less": "^0.5.3",
"flitter-orm": "^0.4.0", "flitter-orm": "^0.4.0",
"flitter-redis": "^0.1.1", "flitter-redis": "^0.1.1",
@@ -33,7 +33,7 @@
"ioredis": "^4.17.1", "ioredis": "^4.17.1",
"is-absolute-url": "^3.0.3", "is-absolute-url": "^3.0.3",
"ldapjs": "^1.0.2", "ldapjs": "^1.0.2",
"libflitter": "^0.56.0", "libflitter": "^0.57.1",
"moment": "^2.24.0", "moment": "^2.24.0",
"mongodb": "^3.5.9", "mongodb": "^3.5.9",
"nodemailer": "^6.4.6", "nodemailer": "^6.4.6",

View File

@@ -2148,10 +2148,10 @@ flitter-i18n@^0.1.1:
ncp "^2.0.0" ncp "^2.0.0"
pluralize "^8.0.0" pluralize "^8.0.0"
flitter-jobs@^0.1.2: flitter-jobs@^0.3.0:
version "0.1.2" version "0.3.0"
resolved "https://registry.yarnpkg.com/flitter-jobs/-/flitter-jobs-0.1.2.tgz#5536bb12be728b61f6e0940b6c18760e4f1b59d6" resolved "https://registry.yarnpkg.com/flitter-jobs/-/flitter-jobs-0.3.0.tgz#c10fd45e4ab3c41c32ad4d8540b9b76547c20fbd"
integrity sha512-bxJD3akDnoKPRyIXXy8ytlypGI8uj0Fjn2C8gIY2HFxZeFIBJo47MqQ9qZvVWFS28pS6aUY+0emOvTFkuG/2zA== integrity sha512-uapjauWZCoZwc+3DuL0W1tY4LozSXTr3FqrKJEcL/Ri8diRYjZuT0+v+kiZBJ4YzThcjjXF+aYoFL09QlIG7Zw==
dependencies: dependencies:
bullmq "^1.8.8" bullmq "^1.8.8"
flitter-redis "^0.1.1" flitter-redis "^0.1.1"
@@ -3235,10 +3235,10 @@ leven@^1.0.2:
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3" resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM= integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
libflitter@^0.56.0: libflitter@^0.57.1:
version "0.56.0" version "0.57.1"
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.56.0.tgz#1ff04b7749d55e7a011149b8a6cae561b8e800d8" resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.57.1.tgz#e605333a5e38cadac1203e4be8ea685c5520472b"
integrity sha512-AwCmTZaKPOQqDWiASvxMuWaexBYxuKpY/QMVnrVdW/VXT5eW0rIf+bJy6RdQGiH8GzlB3442+U4yl4VWNfeKsQ== integrity sha512-bEu02HZjcAp53A2xnE0hz/LVhkErvnqaKmlNOqvJJaGMjKClfVuqJlr9bFfWU84qXL9SaHWd06P3nxIVKTvFIw==
dependencies: dependencies:
colors "^1.3.3" colors "^1.3.3"
connect-mongodb-session "^2.2.0" connect-mongodb-session "^2.2.0"