19 Commits
ci-04 ... ci-17

Author SHA1 Message Date
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
23 changed files with 138 additions and 53 deletions

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

@@ -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

@@ -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,34 @@ 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')
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 +45,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,13 +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.user ) { 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 = {} 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')
@@ -43,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
@@ -52,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

@@ -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

@@ -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"