Making roles more apparent for certificates and authorities. (#327)

This commit is contained in:
kevgliss 2016-05-20 12:48:12 -07:00
parent e04c1e7dc9
commit bd727b825d
18 changed files with 136 additions and 61 deletions

View File

@ -18,6 +18,9 @@ admin_permission = Permission(RoleNeed('admin'))
CertificateCreator = namedtuple('certificate', ['method', 'value']) CertificateCreator = namedtuple('certificate', ['method', 'value'])
CertificateCreatorNeed = partial(CertificateCreator, 'key') CertificateCreatorNeed = partial(CertificateCreator, 'key')
CertificateOwner = namedtuple('certificate', ['method', 'value'])
CertificateOwnerNeed = partial(CertificateOwner, 'role')
class SensitiveDomainPermission(Permission): class SensitiveDomainPermission(Permission):
def __init__(self): def __init__(self):
@ -36,6 +39,15 @@ class UpdateCertificatePermission(Permission):
super(UpdateCertificatePermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin')) super(UpdateCertificatePermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
class CertificatePermission(Permission):
def __init__(self, certificate_id, roles):
needs = [RoleNeed('admin'), CertificateCreatorNeed(certificate_id)]
for r in roles:
needs.append(CertificateOwnerNeed(str(r)))
super(CertificatePermission, self).__init__(*needs)
RoleUser = namedtuple('role', ['method', 'value']) RoleUser = namedtuple('role', ['method', 'value'])
ViewRoleCredentialsNeed = partial(RoleUser, 'roleView') ViewRoleCredentialsNeed = partial(RoleUser, 'roleView')

View File

@ -165,7 +165,7 @@ def on_identity_loaded(sender, identity):
# identity with the roles that the user provides # identity with the roles that the user provides
if hasattr(user, 'roles'): if hasattr(user, 'roles'):
for role in user.roles: for role in user.roles:
identity.provides.add(ViewRoleCredentialsNeed(role.id)) identity.provides.add(ViewRoleCredentialsNeed(role.name))
identity.provides.add(RoleNeed(role.name)) identity.provides.add(RoleNeed(role.name))
# apply ownership for authorities # apply ownership for authorities

View File

@ -5,7 +5,7 @@
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from flask import Blueprint, g from flask import Blueprint
from flask.ext.restful import reqparse, Api from flask.ext.restful import reqparse, Api
from lemur.common.utils import paginated_parser from lemur.common.utils import paginated_parser
@ -13,7 +13,6 @@ from lemur.common.schema import validate_schema
from lemur.auth.service import AuthenticatedResource from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import AuthorityPermission from lemur.auth.permissions import AuthorityPermission
from lemur.roles import service as role_service
from lemur.certificates import service as certificate_service from lemur.certificates import service as certificate_service
from lemur.authorities import service from lemur.authorities import service
@ -270,24 +269,11 @@ class Authorities(AuthenticatedResource):
if not authority: if not authority:
return dict(message='Not Found'), 404 return dict(message='Not Found'), 404
role = role_service.get_by_name(authority.owner)
# all the authority role members should be allowed # all the authority role members should be allowed
roles = [x.name for x in authority.roles] roles = [x.name for x in authority.roles]
# allow "owner" roles by team DL
roles.append(role)
permission = AuthorityPermission(authority_id, roles) permission = AuthorityPermission(authority_id, roles)
if permission.can(): if permission.can():
# we want to make sure that we cannot add roles that we are not members of
if not g.current_user.is_admin:
role_ids = set([r.id for r in data['roles']])
user_role_ids = set([r.id for r in g.current_user.roles])
if not role_ids.issubset(user_role_ids):
return dict(message="You are not allowed to associate a role which you are not a member of."), 403
return service.update( return service.update(
authority_id, authority_id,
owner=data['owner'], owner=data['owner'],

View File

@ -11,11 +11,12 @@ from marshmallow import fields, validates_schema, post_load
from marshmallow.exceptions import ValidationError from marshmallow.exceptions import ValidationError
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \ from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema
from lemur.authorities.schemas import AuthorityNestedOutputSchema from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.destinations.schemas import DestinationNestedOutputSchema from lemur.destinations.schemas import DestinationNestedOutputSchema
from lemur.notifications.schemas import NotificationNestedOutputSchema from lemur.notifications.schemas import NotificationNestedOutputSchema
from lemur.roles.schemas import RoleNestedOutputSchema
# from lemur.domains.schemas import DomainNestedOutputSchema # from lemur.domains.schemas import DomainNestedOutputSchema
from lemur.users.schemas import UserNestedOutputSchema from lemur.users.schemas import UserNestedOutputSchema
@ -51,6 +52,7 @@ class CertificateInputSchema(CertificateSchema):
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True) destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True) notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
csr = fields.String(validate=validators.csr) csr = fields.String(validate=validators.csr)
@ -73,6 +75,7 @@ class CertificateEditInputSchema(CertificateSchema):
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True) destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True) notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
class CertificateNestedOutputSchema(LemurOutputSchema): class CertificateNestedOutputSchema(LemurOutputSchema):
@ -117,6 +120,7 @@ class CertificateOutputSchema(LemurOutputSchema):
notifications = fields.Nested(NotificationNestedOutputSchema, many=True) notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
replaces = fields.Nested(CertificateNestedOutputSchema, many=True) replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
authority = fields.Nested(AuthorityNestedOutputSchema) authority = fields.Nested(AuthorityNestedOutputSchema)
roles = fields.Nested(RoleNestedOutputSchema, many=True)
class CertificateUploadInputSchema(CertificateSchema): class CertificateUploadInputSchema(CertificateSchema):
@ -130,6 +134,7 @@ class CertificateUploadInputSchema(CertificateSchema):
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True) destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True) notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
@validates_schema @validates_schema
def keys(self, data): def keys(self, data):

View File

@ -91,7 +91,7 @@ def export(cert, export_plugin):
return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions']) return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions'])
def update(cert_id, owner, description, active, destinations, notifications, replaces): def update(cert_id, owner, description, active, destinations, notifications, replaces, roles):
""" """
Updates a certificate Updates a certificate
:param cert_id: :param cert_id:
@ -107,6 +107,8 @@ def update(cert_id, owner, description, active, destinations, notifications, rep
cert.active = active cert.active = active
cert.description = description cert.description = description
cert.destinations = destinations cert.destinations = destinations
cert.notifications = notifications
cert.roles = roles
cert.replaces = replaces cert.replaces = replaces
cert.owner = owner cert.owner = owner

View File

@ -15,7 +15,7 @@ from lemur.common.schema import validate_schema
from lemur.common.utils import paginated_parser from lemur.common.utils import paginated_parser
from lemur.auth.service import AuthenticatedResource from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, CertificatePermission
from lemur.certificates import service from lemur.certificates import service
from lemur.certificates.schemas import certificate_input_schema, certificate_output_schema, \ from lemur.certificates.schemas import certificate_input_schema, certificate_output_schema, \
@ -519,9 +519,8 @@ class Certificates(AuthenticatedResource):
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
cert = service.get(certificate_id) cert = service.get(certificate_id)
role = role_service.get_by_name(cert.owner)
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None)) permission = CertificatePermission(cert.id, [x.name for x in cert.roles])
if permission.can(): if permission.can():
return service.update( return service.update(
@ -531,7 +530,8 @@ class Certificates(AuthenticatedResource):
data['active'], data['active'],
data['destinations'], data['destinations'],
data['notifications'], data['notifications'],
data['replacements'] data['replacements'],
data['roles']
) )
return dict(message='You are not authorized to update this certificate'), 403 return dict(message='You are not authorized to update this certificate'), 403
@ -742,8 +742,8 @@ class CertificateExport(AuthenticatedResource):
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
cert = service.get(certificate_id) cert = service.get(certificate_id)
role = role_service.get_by_name(cert.owner)
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None)) permission = CertificatePermission(cert.id, [x.name for x in cert.roles])
options = data['plugin']['plugin_options'] options = data['plugin']['plugin_options']
plugin = data['plugin']['plugin_object'] plugin = data['plugin']['plugin_object']

View File

@ -30,6 +30,13 @@ class RoleOutputSchema(LemurOutputSchema):
users = fields.Nested(UserNestedOutputSchema, many=True) users = fields.Nested(UserNestedOutputSchema, many=True)
class RoleNestedOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
name = fields.String()
description = fields.String()
role_input_schema = RoleInputSchema() role_input_schema = RoleInputSchema()
role_output_schema = RoleOutputSchema() role_output_schema = RoleOutputSchema()
roles_output_schema = RoleOutputSchema(many=True) roles_output_schema = RoleOutputSchema(many=True)

View File

@ -9,8 +9,6 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from flask import g
from lemur import database from lemur import database
from lemur.roles.models import Role from lemur.roles.models import Role
from lemur.users.models import User from lemur.users.models import User
@ -102,13 +100,6 @@ def render(args):
if authority_id: if authority_id:
query = query.filter(Role.authority_id == authority_id) query = query.filter(Role.authority_id == authority_id)
# we make sure that user can see the role - admins can see all
if not g.current_user.is_admin:
ids = []
for role in g.current_user.roles:
ids.append(role.id)
query = query.filter(Role.id.in_(ids))
if filt: if filt:
terms = filt.split(';') terms = filt.split(';')
query = database.filter(query, Role, terms) query = database.filter(query, Role, terms)

View File

@ -1,6 +1,8 @@
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h3>Edit <span class="text-muted"><small>{{ authority.name }}</small></span></h3> <h3>Edit <span class="text-muted"><small>{{ authority.name }}</small></span></h3>
</div>
<div class="modal-body"> <div class="modal-body">
<form name="createForm" class="form-horizontal" role="form" novalidate> <form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group" <div class="form-group"
@ -22,41 +24,23 @@
Description Description
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea name="description" ng-model="authority.description" placeholder="Something elegant" class="form-control" required></textarea> <textarea name="description" ng-model="authority.description" placeholder="Something elegant"
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p> class="form-control" required></textarea>
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You
must give a short description about this authority will be used for, this description should only
include alphanumeric characters</p>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
Roles Roles
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10" ng-model="authority" role-select></div>
<div class="input-group">
<input type="text" ng-model="authority.selectedRole" placeholder="Role Name"
uib-typeahead="role.name for role in roleService.findRoleByName($viewValue)" typeahead-loading="loadingRoles"
class="form-control input-md" typeahead-on-select="authority.attachRole($item)" typeahead-wait-ms="500"
uib-tooltip="Roles control which authorities a user can issue certificates from"
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="roles.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ authority.roles.length || 0 }}</span>
</button>
</span>
</div>
<table ng-show="authority.roles" class="table">
<tr ng-repeat="role in authority.roles track by $index">
<td><a class="btn btn-sm btn-info" href="#/roles/{{ role.id }}/edit">{{ role.name }}</a></td>
<td><span class="text-muted">{{ role.description }}</span></td>
<td>
<button type="button" ng-click="authority.removeRole($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
</table>
</div>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button ng-click="save(authority)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button> <button ng-click="save(authority)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save
</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button> <button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div> </div>

View File

@ -120,6 +120,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10" ng-model="authority" role-select></div>
</div>
</div> </div>
</form> </form>

View File

@ -27,6 +27,12 @@
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p> <p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
</div> </div>
</div> </div>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10" ng-model="certificate" role-select></div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>

View File

@ -146,6 +146,12 @@
class="help-block">Enter a valid certificate signing request.</p> class="help-block">Enter a valid certificate signing request.</p>
</div> </div>
</div> </div>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10" ng-model="certificate" role-select></div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>

View File

@ -81,6 +81,12 @@
class="help-block">Enter a valid certificate.</p> class="help-block">Enter a valid certificate.</p>
</div> </div>
</div> </div>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10" ng-model="certificate" role-select></div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>

View File

@ -4,6 +4,16 @@ angular.module('lemur')
.service('CertificateApi', function (LemurRestangular, DomainService) { .service('CertificateApi', function (LemurRestangular, DomainService) {
LemurRestangular.extendModel('certificates', function (obj) { LemurRestangular.extendModel('certificates', function (obj) {
return angular.extend(obj, { return angular.extend(obj, {
attachRole: function (role) {
this.selectedRole = null;
if (this.roles === undefined) {
this.roles = [];
}
this.roles.push(role);
},
removeRole: function (index) {
this.roles.splice(index, 1);
},
attachAuthority: function (authority) { attachAuthority: function (authority) {
this.authority = authority; this.authority = authority;
this.authority.maxDate = moment(this.authority.notAfter).subtract(1, 'days').format('YYYY/MM/DD'); this.authority.maxDate = moment(this.authority.notAfter).subtract(1, 'days').format('YYYY/MM/DD');

View File

@ -121,6 +121,15 @@
</li> </li>
</ul> </ul>
</uib-tab> </uib-tab>
<uib-tab>
<uib-tab-heading>Roles</uib-tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="role in certificate.roles">
<strong>{{ role.name }}</strong>
<span class="pull-right">{{ role.description}}</span>
</li>
</ul>
</uib-tab>
<uib-tab> <uib-tab>
<uib-tab-heading>Destinations</uib-tab-heading> <uib-tab-heading>Destinations</uib-tab-heading>
<ul class="list-group"> <ul class="list-group">

View File

@ -1,7 +1,29 @@
'use strict'; 'use strict';
angular.module('lemur') angular.module('lemur')
.directive('roleSelect', function (RoleApi) {
return {
restrict: 'AE',
scope: {
ngModel: '='
},
replace: true,
require: 'ngModel',
templateUrl: '/angular/roles/role/roleSelect.tpl.html',
link: function postLink($scope) {
RoleApi.getList().then(function (roles) {
$scope.roles = roles;
});
$scope.findRoleByName = function (search) {
return RoleApi.getList({'filter[name]': search})
.then(function (roles) {
return roles;
});
};
}
};
})
.controller('RolesEditController', function ($scope, $uibModalInstance, RoleApi, RoleService, UserService, toaster, editId) { .controller('RolesEditController', function ($scope, $uibModalInstance, RoleApi, RoleService, UserService, toaster, editId) {
RoleApi.get(editId).then(function (role) { RoleApi.get(editId).then(function (role) {
$scope.role = role; $scope.role = role;

View File

@ -0,0 +1,23 @@
<div>
<div class="input-group">
<input type="text" ng-model="ngModel.selectedRole" placeholder="Role Name"
uib-typeahead="role.name for role in findRoleByName($viewValue)" typeahead-loading="loadingRoles"
class="form-control input-md" typeahead-on-select="ngModel.attachRole($item)" typeahead-wait-ms="500"
uib-tooltip="Roles control who can access this resource"
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="roles.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ ngModel.roles.length || 0 }}</span>
</button>
</span>
</div>
<table ng-show="ngModel.roles" class="table">
<tr ng-repeat="role in ngModel.roles track by $index">
<td><a class="btn btn-sm btn-info" href="#/roles/{{ role.id }}/edit">{{ role.name }}</a></td>
<td><span class="text-muted">{{ role.description }}</span></td>
<td>
<button type="button" ng-click="ngModel.removeRole($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
</table>
</div>

View File

@ -108,7 +108,7 @@ def test_certificate_input_schema(client, authority):
assert data['country'] == 'US' assert data['country'] == 'US'
assert data['location'] == 'Los Gatos' assert data['location'] == 'Los Gatos'
assert len(data.keys()) == 12 assert len(data.keys()) == 13
def test_certificate_input_with_extensions(client, authority): def test_certificate_input_with_extensions(client, authority):