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:
kevgliss 2015-12-30 15:36:31 -08:00
commit a4bf847b56
10 changed files with 272 additions and 19 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ###

View File

@ -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);
};
}); });

View File

@ -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;
}; };

View File

@ -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>

View File

@ -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):