Add api_scope target for IAM policy

This commit is contained in:
garrettmills 2020-05-20 21:17:07 -05:00
parent faab948a6b
commit b526b8f24d
No known key found for this signature in database
GPG Key ID: 6ACD58D6ADACFC6E
6 changed files with 72 additions and 11 deletions

View File

@ -10,7 +10,6 @@
- Cobalt form - after action handlers - Cobalt form - after action handlers
- e.g. after insert perform action - e.g. after insert perform action
- e.g. after update perform action, &c. - e.g. after update perform action, &c.
- IAM manage user API scopes
- OAuth2 -> support refresh tokens - OAuth2 -> support refresh tokens
- Traps -> support session traps; convert MFA challenge to use session trap - Traps -> support session traps; convert MFA challenge to use session trap
- Allow setting user trap from web UI - Allow setting user trap from web UI

View File

@ -118,6 +118,7 @@ class PolicyResource extends CRUDBase {
type: 'select', type: 'select',
options: [ options: [
{ display: 'Application', value: 'application' }, { display: 'Application', value: 'application' },
{ display: 'API Scope', value: 'api_scope' },
], ],
}, },
{ {
@ -132,6 +133,18 @@ class PolicyResource extends CRUDBase {
}, },
if: (form_data) => form_data.target_type === 'application' if: (form_data) => form_data.target_type === 'application'
}, },
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'reflect/Scope',
display: 'scope',
value: 'scope',
},
if: (form_data) => form_data.target_type === 'api_scope'
},
], ],
} }
} }

View File

@ -2,7 +2,7 @@ const { Controller } = require('libflitter')
class IAMController extends Controller { class IAMController extends Controller {
static get services() { static get services() {
return [...super.services, 'models'] return [...super.services, 'models', 'canon']
} }
async check_entity_access(req, res, next) { async check_entity_access(req, res, next) {
@ -111,7 +111,7 @@ class IAMController extends Controller {
.message('Invalid access_type. Must be one of: allow, deny.') .message('Invalid access_type. Must be one of: allow, deny.')
.api() .api()
if ( !['application'].includes(req.body.target_type) ) if ( !['application', 'api_scope'].includes(req.body.target_type) )
return res.status(400) return res.status(400)
.message('Invalid target_type. Must be one of: application.') .message('Invalid target_type. Must be one of: application.')
.api() .api()
@ -124,6 +124,12 @@ class IAMController extends Controller {
return res.status(400) return res.status(400)
.message('Invalid target_id.') .message('Invalid target_id.')
.api() .api()
} else if ( req.body.target_type === 'api_scope' ) {
const api_scopes = this.canon.get('controller::api:v1:Reflect.api_scopes')()
if ( !api_scopes.includes(req.body.target_id) )
return res.status(400)
.message('Invalid target_id.')
.api()
} }
const policy = new Policy({ const policy = new Policy({
@ -189,7 +195,7 @@ class IAMController extends Controller {
.message('Invalid access_type. Must be one of: allow, deny.') .message('Invalid access_type. Must be one of: allow, deny.')
.api() .api()
if ( !['application'].includes(req.body.target_type) ) if ( !['application', 'api_scope'].includes(req.body.target_type) )
return res.status(400) return res.status(400)
.message('Invalid target_type. Must be one of: application.') .message('Invalid target_type. Must be one of: application.')
.api() .api()
@ -202,6 +208,12 @@ class IAMController extends Controller {
return res.status(400) return res.status(400)
.message('Invalid target_id.') .message('Invalid target_id.')
.api() .api()
} else if ( req.body.target_type === 'api_scope' ) {
const api_scopes = this.canon.get('controller::api:v1:Reflect.api_scopes')()
if ( !api_scopes.includes(req.body.target_id) )
return res.status(400)
.message('Invalid target_id.')
.api()
} }
policy.entity_type = req.body.entity_type policy.entity_type = req.body.entity_type

View File

@ -130,7 +130,7 @@ class ReflectController extends Controller {
return res.api() return res.api()
} }
async get_scopes(req, res, next) { api_scopes() {
const routers = this.routers.canonical_items const routers = this.routers.canonical_items
const scopes = [] const scopes = []
@ -158,6 +158,11 @@ class ReflectController extends Controller {
} }
scopes.sort() scopes.sort()
return scopes
}
async get_scopes(req, res, next) {
const scopes = this.api_scopes()
return res.api(scopes.map(x => { return res.api(scopes.map(x => {
return { scope: x } return { scope: x }
})) }))

View File

@ -12,7 +12,7 @@ class PolicyModel extends Model {
entity_type: String, // user | group entity_type: String, // user | group
entity_id: String, entity_id: String,
access_type: String, // allow | deny access_type: String, // allow | deny
target_type: { type: String, default: 'application' }, // application target_type: { type: String, default: 'application' }, // application | api_scope
target_id: String, target_id: String,
active: { type: Boolean, default: true }, active: { type: Boolean, default: true },
} }
@ -44,6 +44,27 @@ class PolicyModel extends Model {
return (await this.check_allow(entity_id, target_id)) && !(await this.check_deny(entity_id, target_id)) return (await this.check_allow(entity_id, target_id)) && !(await this.check_deny(entity_id, target_id))
} }
static async check_user_denied(user, target_id) {
const groups = await user.groups()
const group_ids = groups.map(x => x.id)
const user_denials = await this.find({
entity_id: user.id,
target_id,
access_type: 'deny',
active: true,
})
const group_denials = await this.find({
entity_id: { $in: group_ids },
target_id,
access_type: 'deny',
active: true,
})
return user_denials.length > 0 || group_denials.length > 0
}
static async check_user_access(user, target_id) { static async check_user_access(user, target_id) {
const groups = await user.groups() const groups = await user.groups()
const group_ids = groups.map(x => x.id) const group_ids = groups.map(x => x.id)
@ -51,28 +72,28 @@ class PolicyModel extends Model {
const user_approvals = await this.find({ const user_approvals = await this.find({
entity_id: user.id, entity_id: user.id,
target_id, target_id,
approval_type: 'allow', access_type: 'allow',
active: true, active: true,
}) })
const user_denials = await this.find({ const user_denials = await this.find({
entity_id: user.id, entity_id: user.id,
target_id, target_id,
approval_type: 'deny', access_type: 'deny',
active: true, active: true,
}) })
const group_approvals = await this.find({ const group_approvals = await this.find({
entity_id: { $in: group_ids }, entity_id: { $in: group_ids },
target_id, target_id,
approval_type: 'allow', access_type: 'allow',
active: true, active: true,
}) })
const group_denials = await this.find({ const group_denials = await this.find({
entity_id: { $in: group_ids }, entity_id: { $in: group_ids },
target_id, target_id,
approval_type: 'deny', access_type: 'deny',
active: true, active: true,
}) })
@ -109,6 +130,8 @@ class PolicyModel extends Model {
const Application = this.models.get('Application') const Application = this.models.get('Application')
const app = await Application.findById(this.target_id) const app = await Application.findById(this.target_id)
target_display = `Application: ${app.name}` target_display = `Application: ${app.name}`
} else if ( this.target_type === 'api_scope' ) {
target_display = `API Scope: ${this.target_id}`
} }
return { return {

View File

@ -1,7 +1,13 @@
const { Middleware } = require('libflitter') const { Middleware } = require('libflitter')
class PermissionMiddleware extends Middleware { class PermissionMiddleware extends Middleware {
static get services() {
return [...super.services, 'models']
}
async test(req, res, next, { check }) { async test(req, res, next, { check }) {
const Policy = this.models.get('iam:Policy')
// 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 ) {
@ -11,8 +17,11 @@ class PermissionMiddleware extends Middleware {
.api() .api()
} }
const policy_denied = await Policy.check_user_denied(req.user, check)
const policy_access = await Policy.check_user_access(req.user, check)
// Make sure the user has permission // Make sure the user has permission
if ( !req.user.can(check) ) if ( policy_denied || (!req.user.can(check) && !policy_access) )
return res.status(401) return res.status(401)
.message('Insufficient permissions.') .message('Insufficient permissions.')
.api() .api()