25 Commits

Author SHA1 Message Date
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
59d831c61f Update libflitter and enable database error logging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 21:31:46 -05:00
fac3431375 Add api authorization logging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 21:07:42 -05:00
7c8a05aa4f Update libflitter
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 21:00:59 -05:00
1cd306157a Update libflitter and add config to log API responses
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 20:49:19 -05:00
5eb0487c77 Allow oauth2 clients to exercise permissions independent to the user
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 20:22:10 -05:00
3f2680671b Permission middleware log oauth client UUID
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 18:55:21 -05:00
fd06e17d7d Use req.user to check auth instead of req.is_auth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 18:35:55 -05:00
efdea10b14 Guarantee req.oauth in APIRoute middleware
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 17:19:26 -05:00
ce7349565e Fix LDAP search error for individual users
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 17:06:34 -05:00
33 changed files with 232 additions and 64 deletions

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] : '',
} }
@@ -218,10 +218,12 @@ class UsersController extends LDAPController {
// TODO flitter-orm chunk query // TODO flitter-orm chunk query
// TODO generalize scoped search logic // TODO generalize scoped search logic
async search_people(req, res, next) { async search_people(req, res, next) {
if ( !req.user.can('ldap:search:users') ) { if ( !req.user.can('ldap:search:users:me') ) {
return next(new LDAP.InsufficientAccessRightsError()) return next(new LDAP.InsufficientAccessRightsError())
} }
const can_search_all = req.user.can('ldap:search:users')
const iam_targets = this.parse_iam_targets(req.filter) const iam_targets = this.parse_iam_targets(req.filter)
if ( req.scope === 'base' ) { if ( req.scope === 'base' ) {
// If scope is base, check if the base DN matches the filter. // If scope is base, check if the base DN matches the filter.
@@ -231,7 +233,12 @@ class UsersController extends LDAPController {
const user = await this.get_resource_from_dn(req.dn) const user = await this.get_resource_from_dn(req.dn)
// Make sure the user is ldap visible && match the filter // Make sure the user is ldap visible && match the filter
if ( user && user.ldap_visible && req.filter.matches(await user.to_ldap(iam_targets)) ) { if (
user
&& user.ldap_visible
&& req.filter.matches(await user.to_ldap(iam_targets))
&& (req.user.id === user.id || can_search_all)
) {
// If so, send the object // If so, send the object
res.send({ res.send({
@@ -255,6 +262,7 @@ class UsersController extends LDAPController {
// Fetch the LDAP-visible users // Fetch the LDAP-visible users
const users = await this.User.ldap_directory() const users = await this.User.ldap_directory()
for ( const user of users ) { for ( const user of users ) {
if ( user.id !== req.user.id && !can_search_all ) continue
// Make sure the user os of the appropriate scope // Make sure the user os of the appropriate scope
if ( req.dn.equals(user.dn) || user.dn.parent().equals(req.dn) ) { if ( req.dn.equals(user.dn) || user.dn.parent().equals(req.dn) ) {
@@ -283,6 +291,7 @@ class UsersController extends LDAPController {
this.output.debug(`Filter:`) this.output.debug(`Filter:`)
this.output.debug(this.filter_to_obj(req.filter.json)) this.output.debug(this.filter_to_obj(req.filter.json))
for ( const user of users ) { for ( const user of users ) {
if ( user.id !== req.user.id && !can_search_all ) continue
this.output.debug(`Checking ${user.uid}...`) this.output.debug(`Checking ${user.uid}...`)
this.output.debug(`DN: ${user.dn}`) this.output.debug(`DN: ${user.dn}`)
this.output.debug(`Req DN equals: ${req.dn.equals(user.dn)}`) this.output.debug(`Req DN equals: ${req.dn.equals(user.dn)}`)
@@ -290,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
@@ -317,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) {}
} }
@@ -325,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,22 +8,35 @@ 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
// If the request was authorized using an OAuth2 bearer token, // If the request was authorized using an OAuth2 bearer token,
// make sure the associated client has permission to access this endpoint. // make sure the associated client has permission to access this endpoint.
if ( req?.oauth?.client ) { if ( req?.oauth?.client ) {
if ( !req.oauth.client.can(check) ) { if ( !req.oauth.client.can(check) ) {
const reason = 'oauth-permission-fail' const reason = 'oauth-permission-fail'
await this.activity.api_access_denial({ const fail_activity = await this.activity.api_access_denial({
req, req,
reason, reason,
check, check,
oauth_client_id: req.oauth.client.id, oauth_client_id: req.oauth.client.uuid,
}) })
req.additional_api_log_data.permission_check_succeeded = false
req.additional_api_log_data.permission_check_activity_id = fail_activity.id
return res.status(401) return res.status(401)
.message('Insufficient permissions (OAuth2 Client).') .message('Insufficient permissions (OAuth2 Client).')
.api() .api()
} }
req.additional_api_log_data.permission_check_succeeded = true
// If the oauth2 client has this permission, then allow the request to continue,
// even if the user does not.
// OAuth2Clients need to be able to query users via the API.
return next()
} }
const policy_denied = await Policy.check_user_denied(req.user, check) const policy_denied = await Policy.check_user_denied(req.user, check)
@@ -33,13 +46,18 @@ class PermissionMiddleware extends Middleware {
if ( policy_denied || (!req.user.can(check) && !policy_access) ) { if ( policy_denied || (!req.user.can(check) && !policy_access) ) {
// Record the failed API access // Record the failed API access
const reason = policy_denied ? 'iam-denial' : (!req.user.can(check) ? 'user-permission-fail' : 'iam-not-granted') const reason = policy_denied ? 'iam-denial' : (!req.user.can(check) ? 'user-permission-fail' : 'iam-not-granted')
await this.activity.api_access_denial({ req, reason, check }) const fail_activity = await this.activity.api_access_denial({ req, reason, check })
req.additional_api_log_data.permission_check_succeeded = false
req.additional_api_log_data.permission_check_reason = reason
req.additional_api_log_data.permission_check_activity_id = fail_activity.id
return res.status(401) return res.status(401)
.message('Insufficient permissions.') .message('Insufficient permissions.')
.api() .api()
} }
req.additional_api_log_data.permission_check_succeeded = true
return next() return next()
} }
} }

View File

@@ -6,12 +6,21 @@ class APIRouteMiddleware extends Middleware {
} }
async test(req, res, next, { allow_token = true, allow_user = true }) { async test(req, res, next, { allow_token = true, allow_user = true }) {
if ( !req.additional_api_log_data ) req.additional_api_log_data = {}
// First, check if there is a user in the session. // First, check if there is a user in the session.
if ( allow_user && req.is_auth ) { if ( allow_user && req.user ) {
req.additional_api_log_data.authorized_by = 'user'
return next() return next()
} else if ( allow_token ) { } else if ( allow_token ) {
if ( !req.oauth ) req.oauth = {}
req.additional_api_log_data.attempted_token_auth = true
return req.app.oauth2.authorise()(req, res, async e => { return req.app.oauth2.authorise()(req, res, async e => {
if ( e ) return next(e) if ( e ) return next(e)
req.additional_api_log_data.authorized_by = 'token'
// Look up the OAuth2 client an inject it into the route // Look up the OAuth2 client an inject it into the route
if ( req.user && req.user.id ) { if ( req.user && req.user.id ) {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
@@ -42,6 +51,9 @@ class APIRouteMiddleware extends Middleware {
.message('This OAuth2 client is no longer authorized.') .message('This OAuth2 client is no longer authorized.')
.api() .api()
req.additional_api_log_data.token_client_id = client.uuid
req.additional_api_log_data.token = bearer
req.oauth.token = token req.oauth.token = token
req.oauth.client = client req.oauth.client = client
} else } else
@@ -51,10 +63,10 @@ class APIRouteMiddleware extends Middleware {
next() next()
}) })
} } else {
return res.status(401).api() return res.status(401).api()
} }
} }
}
module.exports = exports = APIRouteMiddleware module.exports = exports = APIRouteMiddleware

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

@@ -36,6 +36,7 @@ class ActivityService extends Service {
} }
await activity.save() await activity.save()
return activity
} }
async mfa_enable({ req }) { async mfa_enable({ req }) {

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

@@ -194,6 +194,7 @@ const auth_config = {
// LDAP Binding // LDAP Binding
'ldap:bind', 'ldap:bind',
'ldap:search:users:me',
], ],
root: ['v1', 'ldap', 'saml', 'profile', 'oauth', 'app', 'auth', 'iam'], root: ['v1', 'ldap', 'saml', 'profile', 'oauth', 'app', 'auth', 'iam'],

View File

@@ -23,6 +23,10 @@ const server_config = {
level: env("LOGGING_LEVEL", 2), level: env("LOGGING_LEVEL", 2),
include_timestamp: env("LOGGING_TIMESTAMP", false), include_timestamp: env("LOGGING_TIMESTAMP", false),
api_logging: env('LOG_API_RESPONSES', false),
error_logging: env('LOG_REQUEST_ERRORS', true),
}, },
session: { session: {

View File

@@ -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.53.1", "libflitter": "^0.56.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

@@ -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.53.1: libflitter@^0.56.1:
version "0.53.1" version "0.56.1"
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.53.1.tgz#30b1838763a228fba8b9c820d2cad501c3aa0117" resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.56.1.tgz#250166027b9cab727c9deb6b1fa1865428b1eafb"
integrity sha512-EK3okZyt0pmnpsZNx2lYOIcwgtmSOEPh4a5xE3pXM9RVc3dtXXscgJ5h9OvLTIN9WfRc7T5VTdpOjeAK6Xmysg== integrity sha512-QikFtFRa9okKOjOio5ehpQ6hyacCoMbtOlqcXt4I7uU3lntBeP5qSbz1q3x1wUY/AdBc2k70+Eg8BcpGmBEs4Q==
dependencies: dependencies:
colors "^1.3.3" colors "^1.3.3"
connect-mongodb-session "^2.2.0" connect-mongodb-session "^2.2.0"