Add basic logic for managing vaults
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Garrett Mills 2021-04-15 15:34:13 -05:00
parent 5391c7c6d6
commit 3730ddc2f2
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
11 changed files with 399 additions and 8 deletions

View File

@ -66,6 +66,12 @@ export default class SideBarComponent extends Component {
type: 'resource', type: 'resource',
resource: 'iam/Permission', resource: 'iam/Permission',
}, },
{
text: 'Vaults',
action: 'list',
type: 'resource',
resource: 'vault/Vault',
},
{ {
text: 'Computers', text: 'Computers',
action: 'list', action: 'list',

View File

@ -129,6 +129,7 @@ class PolicyResource extends CRUDBase {
{display: 'API Scope', value: 'api_scope'}, {display: 'API Scope', value: 'api_scope'},
{display: 'Computer', value: 'machine'}, {display: 'Computer', value: 'machine'},
{display: 'Computer Group', value: 'machine_group'}, {display: 'Computer Group', value: 'machine_group'},
{display: 'Vault', value: 'vault'},
], ],
}, },
{ {
@ -179,6 +180,18 @@ class PolicyResource extends CRUDBase {
}, },
if: (form_data) => form_data.target_type === 'machine_group' if: (form_data) => form_data.target_type === 'machine_group'
}, },
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'vault/Vault',
display: 'name',
value: 'id',
},
if: (form_data) => form_data.target_type === 'vault'
},
{ {
name: 'Permission', name: 'Permission',
field: 'permission', field: 'permission',
@ -243,6 +256,22 @@ class PolicyResource extends CRUDBase {
}, },
if: (form_data, opts) => form_data.target_type === 'machine_group' && opts?.length if: (form_data, opts) => form_data.target_type === 'machine_group' && opts?.length
}, },
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'vault',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'vault' && opts?.length
},
], ],
/*handlers: { /*handlers: {
insert: { insert: {

View File

@ -0,0 +1,62 @@
import CRUDBase from '../CRUDBase.js'
class VaultResource extends CRUDBase {
constructor() {
super()
this.endpoint = '/api/v1/vault/vaults'
this.required_fields = ['name']
this.permission_base = 'v1:vault:vaults'
this.item = 'Vault'
this.plural = 'Vaults'
this.listing_definition = {
display: `Vaults are encrypted key-value stores that can be managed with IAM and accessed via REST APIs.`,
columns: [
{
name: 'Name',
field: 'name',
},
],
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: 'Name',
field: 'name',
required: true,
type: 'text',
},
],
}
}
}
const vault_vault = new VaultResource()
export { vault_vault }

View File

@ -152,12 +152,12 @@ class IAMController extends Controller {
if ( !['allow', 'deny'].includes(req.body.access_type) ) if ( !['allow', 'deny'].includes(req.body.access_type) )
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} access_type. ${req.T('api:must_one')} allow, deny.`) .message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
.api() .api()
if ( !['application', 'api_scope', 'machine', 'machine_group'].includes(req.body.target_type) ) if ( !['application', 'api_scope', 'machine', 'machine_group', 'vault'].includes(req.body.target_type) )
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} target_type. ${req.T('api:must_one')} application, api_scope, machine, machine_group.`) .message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group, vault.`)
.api() .api()
// Make sure the target_id is valid // Make sure the target_id is valid
@ -188,6 +188,13 @@ class IAMController extends Controller {
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} target_id.`) .message(`${req.T('common.invalid')} target_id.`)
.api() .api()
} else if ( req.body.target_type === 'vault' ) {
const Vault = this.models.get('vault:Vault')
const vault = await Vault.findById(req.body.target_id)
if ( !vault?.active || !(await Policy.check_user_access(req.user, vault.id, 'update')) )
return res.status(400)
.message(`${req.T('common.invalid')} target_id.`)
.api()
} }
const policy = new Policy({ const policy = new Policy({
@ -230,7 +237,7 @@ class IAMController extends Controller {
.api() .api()
} }
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group'] const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group', 'vault']
if ( !valid_target_types.includes(req.body.target_type) ) { if ( !valid_target_types.includes(req.body.target_type) ) {
return res.status(400) return res.status(400)
.message(`${req.T('api.invalid_target_type')}`) .message(`${req.T('api.invalid_target_type')}`)
@ -312,9 +319,9 @@ class IAMController extends Controller {
.message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`) .message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
.api() .api()
if ( !['application', 'api_scope', 'machine', 'machine_group'].includes(req.body.target_type) ) if ( !['application', 'api_scope', 'machine', 'machine_group', 'vault'].includes(req.body.target_type) )
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group.`) .message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group, vault.`)
.api() .api()
// Make sure the target_id is valid // Make sure the target_id is valid
@ -345,6 +352,13 @@ class IAMController extends Controller {
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} target_id.`) .message(`${req.T('common.invalid')} target_id.`)
.api() .api()
} else if ( req.body.target_type === 'vault' ) {
const Vault = this.models.get('vault:Vault')
const vault = await Vault.findById(req.body.target_id)
if ( !vault?.active || !(await Policy.check_user_access(req.user, vault.id, 'update')) )
return res.status(400)
.message(`${req.T('common.invalid')} target_id.`)
.api()
} }
policy.entity_type = req.body.entity_type policy.entity_type = req.body.entity_type
@ -389,7 +403,7 @@ class IAMController extends Controller {
.api() .api()
} }
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group'] const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group', 'vault']
if ( !valid_target_types.includes(req.body.target_type) ) { if ( !valid_target_types.includes(req.body.target_type) ) {
return res.status(400) return res.status(400)
.message(`${req.T('api.invalid_target_type')}`) .message(`${req.T('api.invalid_target_type')}`)

View File

@ -0,0 +1,130 @@
const { Controller } = require('libflitter')
class VaultController extends Controller {
static get services() {
return [...super.services, 'models']
}
async get_vaults(req, res, next) {
const Policy = this.models.get('iam:Policy')
const Vault = this.models.get('vault:Vault')
await Vault.for_user(req.user)
const vaults = await Vault.find({ active: true })
console.log('found vaults', vaults)
const accessible = []
for ( const vault of vaults ) {
if ( await Policy.check_user_access(req.user, vault.id, 'view') ) {
accessible.push(await vault.to_api())
}
}
return res.api(accessible)
}
async get_vault(req, res, next) {
const Policy = this.models.get('iam:Policy')
const Vault = this.models.get('vault:Vault')
const vault = await Vault.findById(req.params.id)
if ( !vault?.active ) {
return res.status(404)
.message(req.T('api.vault_not_found'))
.api()
}
if ( !(await Policy.check_user_access(req.user, vault.id, 'view')) ) {
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
}
return res.api(await vault.to_api())
}
async create_vault(req, res, next) {
const Policy = this.models.get('iam:Policy')
const Vault = this.models.get('vault:Vault')
if ( !req.body.name ) {
return res.status(400)
.message(`${req.T('api.missing_field')} name`)
.api()
}
const vault = new Vault({
name: req.body.name
})
await vault.save()
await vault.grant_default(req.user)
return res.api(await vault.to_api())
}
async update_vault(req, res, next) {
const Policy = this.models.get('iam:Policy')
const Vault = this.models.get('vault:Vault')
if ( !req.body.name ) {
return res.status(400)
.message(`${req.T('api.missing_field')} name`)
.api()
}
const vault = await Vault.findById(req.params.id)
if ( !vault?.active ) {
return res.status(404)
.message(req.T('api.vault_not_found'))
.api()
}
if ( !(await Policy.check_user_access(req.user, vault.id, 'update')) ) {
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
}
vault.name = req.body.name
await vault.save()
return res.api(await vault.to_api())
}
async delete_vault(req, res, next) {
const Policy = this.models.get('iam:Policy')
const Vault = this.models.get('vault:Vault')
const vault = await Vault.findById(req.params.id)
if ( !vault?.active ) {
return res.status(404)
.message(req.T('api.vault_not_found'))
.api()
}
if ( !(await Policy.check_user_access(req.user, vault.id, 'delete')) ) {
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
}
vault.active = false
await vault.save()
const policies = await Policy.find({
active: true,
target_type: 'vault',
target_id: vault.id,
})
for ( const policy of policies ) {
policy.active = false
await policy.save()
}
return res.api()
}
}
module.exports = exports = VaultController

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 | api_scope | machine | machine_group target_type: { type: String, default: 'application' }, // application | api_scope | machine | machine_group | vault
target_id: String, target_id: String,
active: { type: Boolean, default: true }, active: { type: Boolean, default: true },
for_permission: { type: Boolean, default: false }, for_permission: { type: Boolean, default: false },
@ -209,6 +209,10 @@ class PolicyModel extends Model {
const MachineGroup = this.models.get('ldap:MachineGroup') const MachineGroup = this.models.get('ldap:MachineGroup')
const group = await MachineGroup.findById(this.target_id) const group = await MachineGroup.findById(this.target_id)
target_display = `Computer Group: ${group.name} (${group.machine_ids.length} computers)` target_display = `Computer Group: ${group.name} (${group.machine_ids.length} computers)`
} else if ( this.target_type === 'vault' ) {
const Vault = this.models.get('vault:Vault')
const vault = await Vault.findById(this.target_id)
target_display = `Vault: ${vault.name}`
} }
return { return {

View File

@ -0,0 +1,66 @@
const { Model } = require('flitter-orm')
class VaultModel extends Model {
static get services() {
return [...super.services, 'models']
}
static get schema() {
return {
active: { type: Boolean, default: true },
name: String,
user_id: String,
}
}
static async for_user(user) {
const existing = await this.findOne({
user_id: user.id,
})
if ( existing ) return existing
const vault = new this({
name: `${user.first_name} ${user.last_name}'s Vault`,
user_id: user.id,
})
await vault.save()
await vault.grant_default(user)
return vault
}
async grant_default(user) {
const Policy = this.models.get('iam:Policy')
const grants = ['view', 'read', 'update', 'delete', undefined]
for ( const grant of grants ) {
const policy = new Policy({
entity_type: 'user',
entity_id: user.id,
access_type: 'allow',
target_type: 'vault',
target_id: this.id,
...(grant ? {
for_permission: true,
permission: grant
} : {})
})
await policy.save()
}
}
async to_api() {
return {
id: this.id,
_id: this.id,
name: this.name,
active: this.active,
user_id: this.user_id,
}
}
}
module.exports = exports = VaultModel

View File

@ -0,0 +1,41 @@
const iam_routes = {
prefix: '/api/v1/vault',
middleware: [
'auth:APIRoute'
],
get: {
'/vaults': [
['middleware::api:Permission', { check: 'v1:vault:vaults:list' }],
'controller::api:v1:Vault.get_vaults',
],
'/vaults/:id': [
['middleware::api:Permission', { check: 'v1:vault:vaults:get' }],
'controller::api:v1:Vault.get_vault',
],
},
post: {
'/vaults': [
['middleware::api:Permission', { check: 'v1:vault:vaults:create' }],
'controller::api:v1:Vault.create_vault',
],
},
patch: {
'/vaults/:id': [
['middleware::api:Permission', { check: 'v1:vault:vaults:update' }],
'controller::api:v1:Vault.update_vault',
],
},
delete: {
'/vaults/:id': [
['middleware::api:Permission', { check: 'v1:vault:vaults:delete' }],
'controller::api:v1:Vault.delete_vault',
],
},
}
module.exports = exports = iam_routes

View File

@ -37,6 +37,15 @@ class SettingsUnit extends Unit {
this.output.debug(`Guarantee setting key "${key}" with default value "${default_value}".`) this.output.debug(`Guarantee setting key "${key}" with default value "${default_value}".`)
await Setting.guarantee(key, default_value) await Setting.guarantee(key, default_value)
} }
const Permission = this.models.get('iam:Permission')
const default_permissions = this.configs.get('auth.iam.default_permissions')
for ( const perm of default_permissions ) {
const existing = await Permission.findOne(perm)
if ( !existing ) {
await (new Permission(perm)).save()
}
}
} }
} }

View File

@ -3,6 +3,35 @@ const auth_config = {
default_provider: env('AUTH_DEFAULT_PROVIDER', 'flitter'), default_provider: env('AUTH_DEFAULT_PROVIDER', 'flitter'),
default_login_route: '/dash', default_login_route: '/dash',
iam: {
default_permissions: [
{
target_type: 'machine',
permission: 'sudo',
},
{
target_type: 'machine_group',
permission: 'sudo',
},
{
target_type: 'vault',
permission: 'view',
},
{
target_type: 'vault',
permission: 'read',
},
{
target_type: 'vault',
permission: 'update',
},
{
target_type: 'vault',
permission: 'delete',
},
],
},
mfa: { mfa: {
secret_length: env('MFA_SECRET_LENGTH', 20) secret_length: env('MFA_SECRET_LENGTH', 20)
}, },

View File

@ -6,6 +6,7 @@ module.exports = exports = {
machine_not_found: 'Machine not found with that ID.', machine_not_found: 'Machine not found with that ID.',
group_already_exists: 'A group with that name already exists.', group_already_exists: 'A group with that name already exists.',
machine_already_exists: 'A machine with that name already exists.', machine_already_exists: 'A machine with that name already exists.',
vault_not_found: 'A vault with that ID not found.',
user_not_found: 'User not found with that ID.', user_not_found: 'User not found with that ID.',
user_already_exists: 'A user with that identifier already exists.', user_already_exists: 'A user with that identifier already exists.',