diff --git a/lemur/auth/permissions.py b/lemur/auth/permissions.py index 6cc04cac..8b64b558 100644 --- a/lemur/auth/permissions.py +++ b/lemur/auth/permissions.py @@ -19,6 +19,11 @@ CertificateCreator = namedtuple('certificate', ['method', 'value']) CertificateCreatorNeed = partial(CertificateCreator, 'key') +class SensitiveDomainPermission(Permission): + def __init__(self): + super(SensitiveDomainPermission, self).__init__(RoleNeed('admin')) + + class ViewKeyPermission(Permission): def __init__(self, certificate_id, owner): c_need = CertificateCreatorNeed(certificate_id) diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 4bbe4c5e..4436b0b2 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -7,16 +7,24 @@ """ import base64 from builtins import str + from flask import Blueprint, make_response, jsonify from flask.ext.restful import reqparse, Api, fields + from cryptography import x509 from cryptography.hazmat.backends import default_backend 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.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.domains import service as domain_service from lemur.common.utils import marshal_items, paginated_parser from lemur.notifications.views import notification_list @@ -63,6 +71,34 @@ def valid_authority(authority_options): 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): """ 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 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 dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403 diff --git a/lemur/domains/models.py b/lemur/domains/models.py index 0bb62f65..ed7123fc 100644 --- a/lemur/domains/models.py +++ b/lemur/domains/models.py @@ -7,7 +7,7 @@ .. moduleauthor:: Kevin Glisson """ -from sqlalchemy import Column, Integer, String +from sqlalchemy import Column, Integer, String, Boolean from lemur.database import db @@ -16,11 +16,4 @@ class Domain(db.Model): __tablename__ = 'domains' id = Column(Integer, primary_key=True) name = Column(String(256)) - - 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 + sensitive = Column(Boolean, default=False) diff --git a/lemur/domains/service.py b/lemur/domains/service.py index b1e2d559..5b3e02f5 100644 --- a/lemur/domains/service.py +++ b/lemur/domains/service.py @@ -32,6 +32,43 @@ def get_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): """ Helper to parse REST Api requests diff --git a/lemur/domains/views.py b/lemur/domains/views.py index 99e14432..b0922f05 100644 --- a/lemur/domains/views.py +++ b/lemur/domains/views.py @@ -12,12 +12,14 @@ from flask.ext.restful import reqparse, Api, fields from lemur.domains import service from lemur.auth.service import AuthenticatedResource +from lemur.auth.permissions import SensitiveDomainPermission from lemur.common.utils import paginated_parser, marshal_items FIELDS = { 'id': fields.Integer, - 'name': fields.String + 'name': fields.String, + 'sensitive': fields.Boolean } mod = Blueprint('domains', __name__) @@ -57,10 +59,12 @@ class DomainsList(AuthenticatedResource): { "id": 1, "name": "www.example.com", + "sensitive": false }, { "id": 2, "name": "www.example2.com", + "sensitive": false } ] "total": 2 @@ -79,6 +83,54 @@ class DomainsList(AuthenticatedResource): args = parser.parse_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): def __init__(self): @@ -111,6 +163,7 @@ class Domains(AuthenticatedResource): { "id": 1, "name": "www.example.com", + "sensitive": false } :reqheader Authorization: OAuth token to authenticate @@ -119,6 +172,53 @@ class Domains(AuthenticatedResource): """ 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): """ Defines the 'domains' endpoint """ @@ -153,10 +253,12 @@ class CertificateDomains(AuthenticatedResource): { "id": 1, "name": "www.example.com", + "sensitive": false }, { "id": 2, "name": "www.example2.com", + "sensitive": false } ] "total": 2 diff --git a/lemur/migrations/versions/4c50b903d1ae_.py b/lemur/migrations/versions/4c50b903d1ae_.py new file mode 100644 index 00000000..7b0515d4 --- /dev/null +++ b/lemur/migrations/versions/4c50b903d1ae_.py @@ -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 ### diff --git a/lemur/static/app/angular/domains/services.js b/lemur/static/app/angular/domains/services.js index 39c54b99..b91447ec 100644 --- a/lemur/static/app/angular/domains/services.js +++ b/lemur/static/app/angular/domains/services.js @@ -12,4 +12,12 @@ angular.module('lemur') return domains; }); }; + + DomainService.updateSensitive = function (domain) { + return domain.put(); + }; + + DomainService.create = function (domain) { + return DomainApi.post(domain); + }; }); diff --git a/lemur/static/app/angular/domains/view/view.js b/lemur/static/app/angular/domains/view/view.js index 269d0e84..fa7a1541 100644 --- a/lemur/static/app/angular/domains/view/view.js +++ b/lemur/static/app/angular/domains/view/view.js @@ -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.domainsTable = new ngTableParams({ 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) { params.settings().$scope.show_filter = !params.settings().$scope.show_filter; }; diff --git a/lemur/static/app/angular/domains/view/view.tpl.html b/lemur/static/app/angular/domains/view/view.tpl.html index f2658aea..f1bff7e2 100644 --- a/lemur/static/app/angular/domains/view/view.tpl.html +++ b/lemur/static/app/angular/domains/view/view.tpl.html @@ -4,6 +4,9 @@ Zone transfers as scary
+
+ +
@@ -16,6 +19,12 @@ {{ domain.name }} + +
+ +
+ diff --git a/lemur/tests/test_domains.py b/lemur/tests/test_domains.py index 85909f72..5757c91d 100644 --- a/lemur/tests/test_domains.py +++ b/lemur/tests/test_domains.py @@ -10,7 +10,7 @@ def test_domain_post(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): @@ -34,7 +34,7 @@ def test_auth_domain_post_(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): @@ -58,7 +58,7 @@ def test_admin_domain_post(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): @@ -74,7 +74,7 @@ def test_domains_get(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):