Merge pull request #192 from kevgliss/sensitive-domains
Adds ability for domains to be marked as sensitive and only be allowe…
This commit is contained in:
commit
a4bf847b56
|
@ -19,6 +19,11 @@ CertificateCreator = namedtuple('certificate', ['method', 'value'])
|
||||||
CertificateCreatorNeed = partial(CertificateCreator, 'key')
|
CertificateCreatorNeed = partial(CertificateCreator, 'key')
|
||||||
|
|
||||||
|
|
||||||
|
class SensitiveDomainPermission(Permission):
|
||||||
|
def __init__(self):
|
||||||
|
super(SensitiveDomainPermission, self).__init__(RoleNeed('admin'))
|
||||||
|
|
||||||
|
|
||||||
class ViewKeyPermission(Permission):
|
class ViewKeyPermission(Permission):
|
||||||
def __init__(self, certificate_id, owner):
|
def __init__(self, certificate_id, owner):
|
||||||
c_need = CertificateCreatorNeed(certificate_id)
|
c_need = CertificateCreatorNeed(certificate_id)
|
||||||
|
|
|
@ -7,16 +7,24 @@
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
from builtins import str
|
from builtins import str
|
||||||
|
|
||||||
from flask import Blueprint, make_response, jsonify
|
from flask import Blueprint, make_response, jsonify
|
||||||
from flask.ext.restful import reqparse, Api, fields
|
from flask.ext.restful import reqparse, Api, fields
|
||||||
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
|
||||||
|
from lemur.auth.service import AuthenticatedResource
|
||||||
|
from lemur.auth.permissions import ViewKeyPermission
|
||||||
|
from lemur.auth.permissions import AuthorityPermission
|
||||||
|
from lemur.auth.permissions import UpdateCertificatePermission
|
||||||
|
from lemur.auth.permissions import SensitiveDomainPermission
|
||||||
|
|
||||||
from lemur.certificates import service
|
from lemur.certificates import service
|
||||||
from lemur.authorities.models import Authority
|
from lemur.authorities.models import Authority
|
||||||
from lemur.auth.service import AuthenticatedResource
|
|
||||||
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission
|
|
||||||
from lemur.roles import service as role_service
|
from lemur.roles import service as role_service
|
||||||
|
from lemur.domains import service as domain_service
|
||||||
from lemur.common.utils import marshal_items, paginated_parser
|
from lemur.common.utils import marshal_items, paginated_parser
|
||||||
from lemur.notifications.views import notification_list
|
from lemur.notifications.views import notification_list
|
||||||
|
|
||||||
|
@ -63,6 +71,34 @@ def valid_authority(authority_options):
|
||||||
return authority
|
return authority
|
||||||
|
|
||||||
|
|
||||||
|
def get_domains_from_options(options):
|
||||||
|
"""
|
||||||
|
Retrive all domains from certificate options
|
||||||
|
:param options:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
domains = [options['commonName']]
|
||||||
|
for k, v in options['extensions']['subAltNames']['names']:
|
||||||
|
if k == 'DNSName':
|
||||||
|
domains.append(v)
|
||||||
|
return domains
|
||||||
|
|
||||||
|
|
||||||
|
def check_sensitive_domains(domains):
|
||||||
|
"""
|
||||||
|
Determines if any certificates in the given certificate
|
||||||
|
are marked as sensitive
|
||||||
|
:param domains:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
for domain in domains:
|
||||||
|
domain_objs = domain_service.get_by_name(domain)
|
||||||
|
for d in domain_objs:
|
||||||
|
if d.sensitive:
|
||||||
|
raise ValueError("The domain {0} has been marked as sensitive. Contact an administrator to "
|
||||||
|
"issue this certificate".format(d.name))
|
||||||
|
|
||||||
|
|
||||||
def pem_str(value, name):
|
def pem_str(value, name):
|
||||||
"""
|
"""
|
||||||
Used to validate that the given string is a PEM formatted string
|
Used to validate that the given string is a PEM formatted string
|
||||||
|
@ -338,9 +374,12 @@ class CertificatesList(AuthenticatedResource):
|
||||||
|
|
||||||
# allow "owner" roles by team DL
|
# allow "owner" roles by team DL
|
||||||
roles.append(role)
|
roles.append(role)
|
||||||
permission = AuthorityPermission(authority.id, roles)
|
authority_permission = AuthorityPermission(authority.id, roles)
|
||||||
|
|
||||||
if permission.can():
|
if authority_permission.can():
|
||||||
|
# if we are not admins lets make sure we aren't issuing anything sensitive
|
||||||
|
if not SensitiveDomainPermission().can():
|
||||||
|
check_sensitive_domains(get_domains_from_options(args))
|
||||||
return service.create(**args)
|
return service.create(**args)
|
||||||
|
|
||||||
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403
|
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from sqlalchemy import Column, Integer, String
|
from sqlalchemy import Column, Integer, String, Boolean
|
||||||
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
|
|
||||||
|
@ -16,11 +16,4 @@ class Domain(db.Model):
|
||||||
__tablename__ = 'domains'
|
__tablename__ = 'domains'
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String(256))
|
name = Column(String(256))
|
||||||
|
sensitive = Column(Boolean, default=False)
|
||||||
def as_dict(self):
|
|
||||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
blob = self.as_dict()
|
|
||||||
blob['certificates'] = [x.id for x in self.certificate]
|
|
||||||
return blob
|
|
||||||
|
|
|
@ -32,6 +32,43 @@ def get_all():
|
||||||
return database.find_all(query, Domain, {}).all()
|
return database.find_all(query, Domain, {}).all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_name(name):
|
||||||
|
"""
|
||||||
|
Fetches domain by it's name
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return database.get_all(Domain, name, field="name").all()
|
||||||
|
|
||||||
|
|
||||||
|
def create(name, sensitive):
|
||||||
|
"""
|
||||||
|
Create a new domain
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
:param sensitive:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
domain = Domain(name=name, sensitive=sensitive)
|
||||||
|
return database.create(domain)
|
||||||
|
|
||||||
|
|
||||||
|
def update(domain_id, name, sensitive):
|
||||||
|
"""
|
||||||
|
Update an existing domain
|
||||||
|
|
||||||
|
:param domain_id:
|
||||||
|
:param name:
|
||||||
|
:param sensitive:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
domain = get(domain_id)
|
||||||
|
domain.name = name
|
||||||
|
domain.sensitive = sensitive
|
||||||
|
database.update(domain)
|
||||||
|
|
||||||
|
|
||||||
def render(args):
|
def render(args):
|
||||||
"""
|
"""
|
||||||
Helper to parse REST Api requests
|
Helper to parse REST Api requests
|
||||||
|
|
|
@ -12,12 +12,14 @@ from flask.ext.restful import reqparse, Api, fields
|
||||||
|
|
||||||
from lemur.domains import service
|
from lemur.domains import service
|
||||||
from lemur.auth.service import AuthenticatedResource
|
from lemur.auth.service import AuthenticatedResource
|
||||||
|
from lemur.auth.permissions import SensitiveDomainPermission
|
||||||
|
|
||||||
from lemur.common.utils import paginated_parser, marshal_items
|
from lemur.common.utils import paginated_parser, marshal_items
|
||||||
|
|
||||||
FIELDS = {
|
FIELDS = {
|
||||||
'id': fields.Integer,
|
'id': fields.Integer,
|
||||||
'name': fields.String
|
'name': fields.String,
|
||||||
|
'sensitive': fields.Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
mod = Blueprint('domains', __name__)
|
mod = Blueprint('domains', __name__)
|
||||||
|
@ -57,10 +59,12 @@ class DomainsList(AuthenticatedResource):
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "www.example.com",
|
"name": "www.example.com",
|
||||||
|
"sensitive": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "www.example2.com",
|
"name": "www.example2.com",
|
||||||
|
"sensitive": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
"total": 2
|
"total": 2
|
||||||
|
@ -79,6 +83,54 @@ class DomainsList(AuthenticatedResource):
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return service.render(args)
|
return service.render(args)
|
||||||
|
|
||||||
|
@marshal_items(FIELDS)
|
||||||
|
def post(self):
|
||||||
|
"""
|
||||||
|
.. http:post:: /domains
|
||||||
|
|
||||||
|
The current domain list
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /domains HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "www.example.com",
|
||||||
|
"sensitive": false
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "www.example.com",
|
||||||
|
"sensitive": false
|
||||||
|
}
|
||||||
|
|
||||||
|
:query sortBy: field to sort on
|
||||||
|
:query sortDir: acs or desc
|
||||||
|
:query page: int. default is 1
|
||||||
|
:query filter: key value pair. format is k=v;
|
||||||
|
:query limit: limit number. default is 10
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
"""
|
||||||
|
self.reqparse.add_argument('name', type=str, location='json')
|
||||||
|
self.reqparse.add_argument('sensitive', type=bool, default=False, location='json')
|
||||||
|
args = self.reqparse.parse_args()
|
||||||
|
return service.create(args['name'], args['sensitive'])
|
||||||
|
|
||||||
|
|
||||||
class Domains(AuthenticatedResource):
|
class Domains(AuthenticatedResource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -111,6 +163,7 @@ class Domains(AuthenticatedResource):
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "www.example.com",
|
"name": "www.example.com",
|
||||||
|
"sensitive": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:reqheader Authorization: OAuth token to authenticate
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
@ -119,6 +172,53 @@ class Domains(AuthenticatedResource):
|
||||||
"""
|
"""
|
||||||
return service.get(domain_id)
|
return service.get(domain_id)
|
||||||
|
|
||||||
|
@marshal_items(FIELDS)
|
||||||
|
def put(self, domain_id):
|
||||||
|
"""
|
||||||
|
.. http:get:: /domains/1
|
||||||
|
|
||||||
|
update one domain
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /domains HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "www.example.com",
|
||||||
|
"sensitive": false
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "www.example.com",
|
||||||
|
"sensitive": false
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
"""
|
||||||
|
self.reqparse.add_argument('name', type=str, location='json')
|
||||||
|
self.reqparse.add_argument('sensitive', type=bool, default=False, location='json')
|
||||||
|
args = self.reqparse.parse_args()
|
||||||
|
|
||||||
|
if SensitiveDomainPermission().can():
|
||||||
|
return service.update(domain_id, args['name'], args['sensitive'])
|
||||||
|
|
||||||
|
return dict(message='You are not authorized to modify this domain'), 403
|
||||||
|
|
||||||
|
|
||||||
class CertificateDomains(AuthenticatedResource):
|
class CertificateDomains(AuthenticatedResource):
|
||||||
""" Defines the 'domains' endpoint """
|
""" Defines the 'domains' endpoint """
|
||||||
|
@ -153,10 +253,12 @@ class CertificateDomains(AuthenticatedResource):
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "www.example.com",
|
"name": "www.example.com",
|
||||||
|
"sensitive": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "www.example2.com",
|
"name": "www.example2.com",
|
||||||
|
"sensitive": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
"total": 2
|
"total": 2
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Adding ability to mark domains as 'sensitive'
|
||||||
|
|
||||||
|
Revision ID: 4c50b903d1ae
|
||||||
|
Revises: 33de094da890
|
||||||
|
Create Date: 2015-12-30 10:19:30.057791
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4c50b903d1ae'
|
||||||
|
down_revision = '33de094da890'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('domains', sa.Column('sensitive', sa.Boolean(), nullable=True))
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('domains', 'sensitive')
|
||||||
|
### end Alembic commands ###
|
|
@ -12,4 +12,12 @@ angular.module('lemur')
|
||||||
return domains;
|
return domains;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DomainService.updateSensitive = function (domain) {
|
||||||
|
return domain.put();
|
||||||
|
};
|
||||||
|
|
||||||
|
DomainService.create = function (domain) {
|
||||||
|
return DomainApi.post(domain);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@ angular.module('lemur')
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
.controller('DomainsViewController', function ($scope, DomainApi, ngTableParams) {
|
.controller('DomainsViewController', function ($scope, $modal, DomainApi, DomainService, ngTableParams, toaster) {
|
||||||
$scope.filter = {};
|
$scope.filter = {};
|
||||||
$scope.domainsTable = new ngTableParams({
|
$scope.domainsTable = new ngTableParams({
|
||||||
page: 1, // show first page
|
page: 1, // show first page
|
||||||
|
@ -29,6 +29,40 @@ angular.module('lemur')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.updateSensitive = function (domain) {
|
||||||
|
DomainService.updateSensitive(domain).then(
|
||||||
|
function () {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'success',
|
||||||
|
title: domain.name,
|
||||||
|
body: 'Updated!'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function (response) {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'error',
|
||||||
|
title: domain.name,
|
||||||
|
body: 'Unable to update! ' + response.data.message,
|
||||||
|
timeout: 100000
|
||||||
|
});
|
||||||
|
domain.sensitive = domain.sensitive ? false : true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.create = function () {
|
||||||
|
var modalInstance = $modal.open({
|
||||||
|
animation: true,
|
||||||
|
controller: 'DomainsCreateController',
|
||||||
|
templateUrl: '/angular/domains/domain/domain.tpl.html',
|
||||||
|
size: 'lg'
|
||||||
|
});
|
||||||
|
|
||||||
|
modalInstance.result.then(function () {
|
||||||
|
$scope.domainsTable.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
$scope.toggleFilter = function (params) {
|
$scope.toggleFilter = function (params) {
|
||||||
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
|
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
<span class="text-muted"><small>Zone transfers as scary</small></span></h2>
|
<span class="text-muted"><small>Zone transfers as scary</small></span></h2>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
<div class="btn-group pull-right">
|
||||||
|
<button ng-click="create()" class="btn btn-primary">Create</button>
|
||||||
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button ng-click="toggleFilter(domainsTable)" class="btn btn-default">Filter</button>
|
<button ng-click="toggleFilter(domainsTable)" class="btn btn-default">Filter</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,6 +19,12 @@
|
||||||
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||||
{{ domain.name }}
|
{{ domain.name }}
|
||||||
</td>
|
</td>
|
||||||
|
<td data-title="'Sensitive'">
|
||||||
|
<form>
|
||||||
|
<switch ng-change="updateSensitive(domain)" id="status" name="status"
|
||||||
|
ng-model="domain.sensitive" class="green small"></switch>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -10,7 +10,7 @@ def test_domain_post(client):
|
||||||
|
|
||||||
|
|
||||||
def test_domain_put(client):
|
def test_domain_put(client):
|
||||||
assert client.put(api.url_for(Domains, domain_id=1), data={}).status_code == 405
|
assert client.put(api.url_for(Domains, domain_id=1), data={}).status_code == 401
|
||||||
|
|
||||||
|
|
||||||
def test_domain_delete(client):
|
def test_domain_delete(client):
|
||||||
|
@ -34,7 +34,7 @@ def test_auth_domain_post_(client):
|
||||||
|
|
||||||
|
|
||||||
def test_auth_domain_put(client):
|
def test_auth_domain_put(client):
|
||||||
assert client.put(api.url_for(Domains, domain_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 405
|
assert client.put(api.url_for(Domains, domain_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 403
|
||||||
|
|
||||||
|
|
||||||
def test_auth_domain_delete(client):
|
def test_auth_domain_delete(client):
|
||||||
|
@ -58,7 +58,7 @@ def test_admin_domain_post(client):
|
||||||
|
|
||||||
|
|
||||||
def test_admin_domain_put(client):
|
def test_admin_domain_put(client):
|
||||||
assert client.put(api.url_for(Domains, domain_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 405
|
assert client.put(api.url_for(Domains, domain_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 400
|
||||||
|
|
||||||
|
|
||||||
def test_admin_domain_delete(client):
|
def test_admin_domain_delete(client):
|
||||||
|
@ -74,7 +74,7 @@ def test_domains_get(client):
|
||||||
|
|
||||||
|
|
||||||
def test_domains_post(client):
|
def test_domains_post(client):
|
||||||
assert client.post(api.url_for(DomainsList), data={}).status_code == 405
|
assert client.post(api.url_for(DomainsList), data={}).status_code == 401
|
||||||
|
|
||||||
|
|
||||||
def test_domains_put(client):
|
def test_domains_put(client):
|
||||||
|
|
Loading…
Reference in New Issue