Add ability to manage and grant IAM permissions as policy
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Garrett Mills 2021-04-15 10:38:43 -05:00
parent 5645e8fae1
commit f2995899ec
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
10 changed files with 437 additions and 9 deletions

View File

@ -28,7 +28,7 @@ const template = `
v-if="field.type === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))" v-if="field.type === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"
v-html="typeof field.display === 'function' ? field.display(data) : field.display" v-html="typeof field.display === 'function' ? field.display(data) : field.display"
></span> ></span>
<span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"> <span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data, field.options))">
<label :for="uuid+field.field">{{ field.name }}</label> <label :for="uuid+field.field">{{ field.name }}</label>
<select <select
:id="uuid+field.field" :id="uuid+field.field"

View File

@ -60,6 +60,12 @@ export default class SideBarComponent extends Component {
type: 'resource', type: 'resource',
resource: 'iam/Policy', resource: 'iam/Policy',
}, },
{
text: 'IAM Permissions',
action: 'list',
type: 'resource',
resource: 'iam/Permission',
},
{ {
text: 'Computers', text: 'Computers',
action: 'list', action: 'list',

View File

@ -0,0 +1,87 @@
import CRUDBase from '../CRUDBase.js'
class PermissionResource extends CRUDBase {
constructor() {
super()
this.endpoint = '/api/v1/iam/permission'
this.required_fields = ['target_type', 'permission']
this.permission_base = 'v1:iam:permission'
this.item = 'IAM Permission'
this.plural = 'IAM Permissions'
this.listing_definition = {
display: `Permissions are custom actions that can be performed on a given IAM target by the subject.`,
columns: [
{
name: 'Target Type',
field: 'target_type',
renderer: type => type.split('_').map(x => `${x.charAt(0).toUpperCase()}${x.slice(1)}`).join(' '),
},
{
name: 'Permission',
field: 'permission',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
fields: [
{
name: 'Target Type',
field: 'target_type',
required: true,
type: 'select',
options: [
{display: 'Application', value: 'application'},
{display: 'Api Scope', value: 'api_scope'},
{display: 'Machine', value: 'machine'},
{display: 'Machine Group', value: 'machine_group'},
],
},
{
name: 'Permission',
field: 'permission',
required: true,
type: 'text',
},
],
/*handlers: {
insert: {
action: 'back',
},
update: {
action: 'back',
},
},*/
}
}
}
const iam_permission = new PermissionResource()
export { iam_permission }

View File

@ -41,6 +41,11 @@ class PolicyResource extends CRUDBase {
name: 'Target', name: 'Target',
field: 'target_display', field: 'target_display',
}, },
{
name: 'Permission',
field: 'permission',
renderer: permission => permission || '-',
},
], ],
actions: [ actions: [
{ {
@ -174,6 +179,70 @@ class PolicyResource extends CRUDBase {
}, },
if: (form_data) => form_data.target_type === 'machine_group' if: (form_data) => form_data.target_type === 'machine_group'
}, },
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'application',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'application' && opts?.length
},
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'api_scope',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'api_scope' && opts?.length
},
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'machine',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'machine' && opts?.length
},
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'machine_group',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'machine_group' && opts?.length
},
], ],
/*handlers: { /*handlers: {
insert: { insert: {

View File

@ -13,7 +13,7 @@ class IAMController extends Controller {
.message(`${req.T('api.missing_field', true)} entity_id, target_id`) .message(`${req.T('api.missing_field', true)} entity_id, target_id`)
.api() .api()
return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id)) return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id, req.body.permission || undefined))
} }
async check_user_access(req, res, next) { async check_user_access(req, res, next) {
@ -39,7 +39,7 @@ class IAMController extends Controller {
.message(req.T('api.insufficient_permissions')) .message(req.T('api.insufficient_permissions'))
.api() .api()
return res.api(await Policy.check_user_access(user, req.body.target_id)) return res.api(await Policy.check_user_access(user, req.body.target_id, req.body.permission || undefined))
} }
async get_policies(req, res, next) { async get_policies(req, res, next) {
@ -56,6 +56,33 @@ class IAMController extends Controller {
return res.api(data) return res.api(data)
} }
async get_permissions(req, res, next) {
const Permission = this.models.get('iam:Permission')
const permissions = await Permission.find({
active: true,
...(req.query.target_type ? {
target_type: req.query.target_type,
} : {})
})
const data = []
for ( const perm of permissions ) {
if ( req.user.can(`iam:permission:${perm.target_type}:view`) ) {
data.push(await perm.to_api())
}
}
if ( req.query.include_unset ) {
data.reverse().push({
permission: '',
})
data.reverse()
}
return res.api(data)
}
async get_policy(req, res, next) { async get_policy(req, res, next) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const policy = await Policy.findById(req.params.id) const policy = await Policy.findById(req.params.id)
@ -73,6 +100,23 @@ class IAMController extends Controller {
return res.api(await policy.to_api()) return res.api(await policy.to_api())
} }
async get_permission(req, res, next) {
const Permission = this.models.get('iam:Permission')
const permission = await Permission.findById(req.params.id)
if ( !permission )
return res.status(404)
.message(req.T('iam.permission_not_found'))
.api()
if ( !req.user.can(`iam:permission:${permission.target_type}:view`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
return res.api(await permission.to_api())
}
async create_policy(req, res, next) { async create_policy(req, res, next) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
@ -154,12 +198,71 @@ class IAMController extends Controller {
target_id: req.body.target_id, target_id: req.body.target_id,
}) })
if ( req.body.permission ) {
// Validate the permission and set it, if it is valid
const Permission = this.models.get('iam:Permission')
const permission = await Permission.findOne({
active: true,
target_type: req.body.target_type,
permission: req.body.permission,
})
if ( permission ) {
policy.for_permission = true
policy.permission = req.body.permission
}
}
await policy.save() await policy.save()
req.user.allow(`iam:policy:${policy.id}`) req.user.allow(`iam:policy:${policy.id}`)
await req.user.save() await req.user.save()
return res.api(await policy.to_api()) return res.api(await policy.to_api())
} }
async create_permission(req, res, next) {
const Permission = this.models.get('iam:Permission')
const required_fields = ['target_type', 'permission']
for ( const field of required_fields ) {
if ( !req.body[field] )
return res.status(400)
.message(`${req.T('api.missing_field')} ${field}`)
.api()
}
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group']
if ( !valid_target_types.includes(req.body.target_type) ) {
return res.status(400)
.message(`${req.T('api.invalid_target_type')}`)
.api()
}
if ( !req.user.can(`iam:permission${req.body.target_type}:create`) ) {
return res.status(401).api()
}
// Make sure one doesn't already exist
const existing = await Permission.findOne({
active: true,
target_type: req.body.target_type,
permission: req.body.permission,
})
if ( existing ) {
return res.status(400)
.message(req.T('api.permission_already_exists'))
.api()
}
const perm = new Permission({
target_type: req.body.target_type,
permission: req.body.permission,
})
await perm.save()
return res.api(await perm.to_api())
}
async update_policy(req, res, next) { async update_policy(req, res, next) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const policy = await Policy.findById(req.params.id) const policy = await Policy.findById(req.params.id)
@ -249,10 +352,69 @@ class IAMController extends Controller {
policy.access_type = req.body.access_type policy.access_type = req.body.access_type
policy.target_type = req.body.target_type policy.target_type = req.body.target_type
policy.target_id = req.body.target_id policy.target_id = req.body.target_id
if ( req.body.permission ) {
// Validate the permission and set it, if it is valid
const Permission = this.models.get('iam:Permission')
const permission = await Permission.findOne({
active: true,
target_type: req.body.target_type,
permission: req.body.permission,
})
if ( permission ) {
policy.for_permission = true
policy.permission = req.body.permission
} else {
policy.for_permission = false
policy.permission = undefined
}
} else {
policy.for_permission = false
policy.permission = undefined
}
await policy.save() await policy.save()
return res.api() return res.api()
} }
async update_permission(req, res, next) {
const Permission = this.models.get('iam:Permission')
const required_fields = ['target_type', 'permission']
for ( const field of required_fields ) {
if ( !req.body[field] )
return res.status(400)
.message(`${req.T('api.missing_field')} ${field}`)
.api()
}
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group']
if ( !valid_target_types.includes(req.body.target_type) ) {
return res.status(400)
.message(`${req.T('api.invalid_target_type')}`)
.api()
}
if ( !req.user.can(`iam:permission${req.body.target_type}:update`) ) {
return res.status(401).api()
}
// Make sure one doesn't already exist
const existing = await Permission.findById(req.params.id)
if ( !existing?.active ) {
return res.status(404)
.message(req.T('api.permission_not_found'))
.api()
}
existing.target_type = req.body.target_type
existing.permission = req.body.permission
await existing.save()
return res.api(await existing.to_api())
}
async delete_policy(req, res, next) { async delete_policy(req, res, next) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const policy = await Policy.findById(req.params.id) const policy = await Policy.findById(req.params.id)
@ -271,6 +433,27 @@ class IAMController extends Controller {
await policy.save() await policy.save()
return res.api() return res.api()
} }
async delete_permission(req, res, next) {
const Permission = this.models.get('iam:Permission')
const permission = await Permission.findById(req.params.id)
if ( !permission?.active ) {
return res.status(404)
.message(req.T('api.permission_not_found'))
.api()
}
if ( !req.user.can(`iam:permission:${permission.target_type}:delete`) ) {
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
}
permission.active = false
await permission.save()
return res.api()
}
} }
module.exports = exports = IAMController module.exports = exports = IAMController

View File

@ -0,0 +1,23 @@
const { Model } = require('flitter-orm')
class PermissionModel extends Model {
static get schema() {
return {
active: { type: Boolean, default: true },
target_type: String,
permission: String
}
}
async to_api() {
return {
_id: this.id,
id: this.id,
active: this.active,
target_type: this.target_type,
permission: this.permission,
}
}
}
module.exports = exports = PermissionModel

View File

@ -15,36 +15,46 @@ class PolicyModel extends Model {
target_type: { type: String, default: 'application' }, // application | api_scope | machine | machine_group target_type: { type: String, default: 'application' }, // application | api_scope | machine | machine_group
target_id: String, target_id: String,
active: { type: Boolean, default: true }, active: { type: Boolean, default: true },
for_permission: { type: Boolean, default: false },
permission: String,
} }
} }
static async check_allow(entity_id, target_id) { static async check_allow(entity_id, target_id, permission = undefined) {
const policies = await this.find({ const policies = await this.find({
entity_id, entity_id,
target_id, target_id,
access_type: 'allow', access_type: 'allow',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
return policies.length > 0 return policies.length > 0
} }
static async check_deny(entity_id, target_id) { static async check_deny(entity_id, target_id, permission = undefined) {
const policies = await this.find({ const policies = await this.find({
entity_id, entity_id,
target_id, target_id,
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
return policies.length === 0 return policies.length === 0
} }
static async check_entity_access(entity_id, target_id) { static async check_entity_access(entity_id, target_id, permission = undefined) {
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, permission)) && !(await this.check_deny(entity_id, target_id, permission))
} }
static async check_user_denied(user, target_id) { static async check_user_denied(user, target_id, permission = undefined) {
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)
@ -53,6 +63,10 @@ class PolicyModel extends Model {
target_id, target_id,
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
const group_denials = await this.find({ const group_denials = await this.find({
@ -60,6 +74,10 @@ class PolicyModel extends Model {
target_id, target_id,
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
return user_denials.length > 0 || group_denials.length > 0 return user_denials.length > 0 || group_denials.length > 0
@ -95,7 +113,7 @@ class PolicyModel extends Model {
return all return all
} }
static async check_user_access(user, target_id) { static async check_user_access(user, target_id, permission = undefined) {
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)
const target_ids = await this.get_all_related(target_id) const target_ids = await this.get_all_related(target_id)
@ -105,6 +123,10 @@ class PolicyModel extends Model {
target_id: { $in: target_ids }, target_id: { $in: target_ids },
access_type: 'allow', access_type: 'allow',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
const user_denials = await this.find({ const user_denials = await this.find({
@ -112,6 +134,10 @@ class PolicyModel extends Model {
target_id: { $in: target_ids }, target_id: { $in: target_ids },
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
const group_approvals = await this.find({ const group_approvals = await this.find({
@ -119,6 +145,10 @@ class PolicyModel extends Model {
target_id: { $in: target_ids }, target_id: { $in: target_ids },
access_type: 'allow', access_type: 'allow',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
const group_denials = await this.find({ const group_denials = await this.find({
@ -126,6 +156,10 @@ class PolicyModel extends Model {
target_id: { $in: target_ids }, target_id: { $in: target_ids },
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
// IF user has explicit denial, deny // IF user has explicit denial, deny
@ -186,6 +220,8 @@ class PolicyModel extends Model {
target_display, target_display,
target_type: this.target_type, target_type: this.target_type,
target_id: this.target_id, target_id: this.target_id,
for_permission: this.for_permission,
permission: this.permission,
} }
} }
} }

View File

@ -14,6 +14,14 @@ const iam_routes = {
['middleware::api:Permission', { check: 'v1:iam:policy:get' }], ['middleware::api:Permission', { check: 'v1:iam:policy:get' }],
'controller::api:v1:IAM.get_policy', 'controller::api:v1:IAM.get_policy',
], ],
'/permission': [
['middleware::api:Permission', { check: 'v1:iam:permission:list' }],
'controller::api:v1:IAM.get_permissions',
],
'/permission/:id': [
['middleware::api:Permission', { check: 'v1:iam:permission:get' }],
'controller::api:v1:IAM.get_permission',
],
}, },
post: { post: {
@ -21,6 +29,10 @@ const iam_routes = {
['middleware::api:Permission', { check: 'v1:iam:policy:create' }], ['middleware::api:Permission', { check: 'v1:iam:policy:create' }],
'controller::api:v1:IAM.create_policy', 'controller::api:v1:IAM.create_policy',
], ],
'/permission': [
['middleware::api:Permission', { check: 'v1:iam:permission:create' }],
'controller::api:v1:IAM.create_permission',
],
'/check_entity_access': [ '/check_entity_access': [
['middleware::api:Permission', { check: 'v1:iam:check_entity_access' }], ['middleware::api:Permission', { check: 'v1:iam:check_entity_access' }],
'controller::api:v1:IAM.check_entity_access', 'controller::api:v1:IAM.check_entity_access',
@ -36,6 +48,10 @@ const iam_routes = {
['middleware::api:Permission', { check: 'v1:iam:policy:update' }], ['middleware::api:Permission', { check: 'v1:iam:policy:update' }],
'controller::api:v1:IAM.update_policy', 'controller::api:v1:IAM.update_policy',
], ],
'/permission/:id': [
['middleware::api:Permission', { check: 'v1:iam:permission:update' }],
'controller::api:v1:IAM.update_permission',
],
}, },
delete: { delete: {
@ -43,6 +59,10 @@ const iam_routes = {
['middleware::api:Permission', { check: 'v1:iam:policy:delete' }], ['middleware::api:Permission', { check: 'v1:iam:policy:delete' }],
'controller::api:v1:IAM.delete_policy', 'controller::api:v1:IAM.delete_policy',
], ],
'/permission/:id': [
['middleware::api:Permission', { check: 'v1:iam:permission:delete' }],
'controller::api:v1:IAM.delete_permission',
],
}, },
} }

View File

@ -16,6 +16,8 @@ module.exports = exports = {
token_not_found: 'Token not found with that ID, or the token has expired.', token_not_found: 'Token not found with that ID, or the token has expired.',
provider_already_exists: 'A service provider with that entity_id already exists.', provider_already_exists: 'A service provider with that entity_id already exists.',
permission_already_exists: 'A permission for that target_type already exists.',
permission_not_found: 'Permission not found with that ID.',
setting_not_found: 'No such setting exists with that key.', setting_not_found: 'No such setting exists with that key.',
@ -28,6 +30,7 @@ module.exports = exports = {
invalid_ldap_client_id: 'Invalid ldap_client_id:', invalid_ldap_client_id: 'Invalid ldap_client_id:',
invalid_oauth_client_id: 'Invalid oauth_client_id:', invalid_oauth_client_id: 'Invalid oauth_client_id:',
invalid_saml_service_provider_id: 'Invalid saml_service_provider_id:', invalid_saml_service_provider_id: 'Invalid saml_service_provider_id:',
invalid_target_type: 'Invalid target_type.',
insufficient_permissions: 'Insufficient permissions.', insufficient_permissions: 'Insufficient permissions.',
missing_field: { missing_field: {

View File

@ -1,4 +1,5 @@
module.exports = exports = { module.exports = exports = {
policy_not_found: 'Policy not found with that ID.', policy_not_found: 'Policy not found with that ID.',
permission_not_found: 'Permission not found with that ID.',
invalid_entity: 'Invalid entity_type. Must be one of:' invalid_entity: 'Invalid entity_type. Must be one of:'
} }