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
- e.g. after insert perform action
- e.g. after update perform action, &c.
- IAM manage user API scopes
- OAuth2 -> support refresh tokens
- Traps -> support session traps; convert MFA challenge to use session trap
- Allow setting user trap from web UI

View File

@ -118,6 +118,7 @@ class PolicyResource extends CRUDBase {
type: 'select',
options: [
{ 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'
},
{
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 {
static get services() {
return [...super.services, 'models']
return [...super.services, 'models', 'canon']
}
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.')
.api()
if ( !['application'].includes(req.body.target_type) )
if ( !['application', 'api_scope'].includes(req.body.target_type) )
return res.status(400)
.message('Invalid target_type. Must be one of: application.')
.api()
@ -124,6 +124,12 @@ class IAMController extends Controller {
return res.status(400)
.message('Invalid target_id.')
.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({
@ -189,7 +195,7 @@ class IAMController extends Controller {
.message('Invalid access_type. Must be one of: allow, deny.')
.api()
if ( !['application'].includes(req.body.target_type) )
if ( !['application', 'api_scope'].includes(req.body.target_type) )
return res.status(400)
.message('Invalid target_type. Must be one of: application.')
.api()
@ -202,6 +208,12 @@ class IAMController extends Controller {
return res.status(400)
.message('Invalid target_id.')
.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

View File

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

View File

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

View File

@ -1,7 +1,13 @@
const { Middleware } = require('libflitter')
class PermissionMiddleware extends Middleware {
static get services() {
return [...super.services, 'models']
}
async test(req, res, next, { check }) {
const Policy = this.models.get('iam:Policy')
// If the request was authorized using an OAuth2 bearer token,
// make sure the associated client has permission to access this endpoint.
if ( req?.oauth?.client ) {
@ -11,8 +17,11 @@ class PermissionMiddleware extends Middleware {
.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
if ( !req.user.can(check) )
if ( policy_denied || (!req.user.can(check) && !policy_access) )
return res.status(401)
.message('Insufficient permissions.')
.api()