From 7812e1e8b66429485255a2fe45dfd7e6e20cb4de Mon Sep 17 00:00:00 2001 From: Pino de Candia <32303022+pinodeca@users.noreply.github.com> Date: Fri, 8 Dec 2017 15:04:44 -0600 Subject: [PATCH] 4 space indentation --- scripts/add-project-ca | 11 + scripts/cloud-config-to-vendor-data | 12 + scripts/configure_ssh.py | 12 + scripts/get-ca-keys | 12 + scripts/get-user-cert | 12 + scripts/vendor-data-to-cloud-config | 12 + setup.py | 12 + tatu/api/app.py | 15 + tatu/api/models.py | 347 +++++++------- tatu/castellano.py | 24 +- tatu/db/models.py | 181 ++++---- tatu/db/persistence.py | 15 +- tatu/ftests/test_api.py | 167 +++---- tatu/notifications.py | 20 +- tatu/tests/test_app.py | 677 +++++++++++++++------------- tatu/utils.py | 88 ++-- 16 files changed, 941 insertions(+), 676 deletions(-) diff --git a/scripts/add-project-ca b/scripts/add-project-ca index 2e91434..0c34e77 100755 --- a/scripts/add-project-ca +++ b/scripts/add-project-ca @@ -1,4 +1,15 @@ #!/usr/bin/env python +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. import json import requests diff --git a/scripts/cloud-config-to-vendor-data b/scripts/cloud-config-to-vendor-data index a693a3d..65f6065 100755 --- a/scripts/cloud-config-to-vendor-data +++ b/scripts/cloud-config-to-vendor-data @@ -1,4 +1,16 @@ #!/usr/bin/env python +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import sys import json import yaml diff --git a/scripts/configure_ssh.py b/scripts/configure_ssh.py index 53d8fdc..7e310da 100644 --- a/scripts/configure_ssh.py +++ b/scripts/configure_ssh.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import json import requests import os diff --git a/scripts/get-ca-keys b/scripts/get-ca-keys index 51fe717..4d07339 100755 --- a/scripts/get-ca-keys +++ b/scripts/get-ca-keys @@ -1,4 +1,16 @@ #!/usr/bin/env python +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import argparse import json import requests diff --git a/scripts/get-user-cert b/scripts/get-user-cert index 6fb843d..de36c60 100755 --- a/scripts/get-user-cert +++ b/scripts/get-user-cert @@ -1,4 +1,16 @@ #!/usr/bin/env python +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import argparse import json import os diff --git a/scripts/vendor-data-to-cloud-config b/scripts/vendor-data-to-cloud-config index e96152c..06155e2 100755 --- a/scripts/vendor-data-to-cloud-config +++ b/scripts/vendor-data-to-cloud-config @@ -1,4 +1,16 @@ #!/usr/bin/env python +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import sys import json import yaml diff --git a/setup.py b/setup.py index 33111c9..983a7f1 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break diff --git a/tatu/api/app.py b/tatu/api/app.py index 7be99cc..c107a29 100644 --- a/tatu/api/app.py +++ b/tatu/api/app.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import falcon import models import os.path @@ -11,6 +23,7 @@ CONF = cfg.CONF if os.path.isfile(fname): CONF(default_config_files=[fname]) + def create_app(sa): api = falcon.API(middleware=[models.Logger(), sa]) api.add_route('/authorities', models.Authorities()) @@ -23,8 +36,10 @@ def create_app(sa): api.add_route('/novavendordata', models.NovaVendorData()) return api + def get_app(): return create_app(SQLAlchemySessionManager()) + def main(global_config, **settings): return create_app(SQLAlchemySessionManager()) diff --git a/tatu/api/models.py b/tatu/api/models.py index 5bc05c5..2fc8254 100644 --- a/tatu/api/models.py +++ b/tatu/api/models.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import falcon import json import logging @@ -5,203 +17,208 @@ import uuid from tatu.db import models as db from Crypto.PublicKey import RSA + def validate_uuid(map, key): - try: - # Verify it's a valid UUID, then convert to canonical string representation - # to avoiid DB errors. - map[key] = str(uuid.UUID(map[key], version=4)) - except ValueError: - msg = '{} is not a valid UUID'.format(map[key]) - raise falcon.HTTPBadRequest('Bad request', msg) + try: + # Verify it's a valid UUID, then convert to canonical string representation + # to avoiid DB errors. + map[key] = str(uuid.UUID(map[key], version=4)) + except ValueError: + msg = '{} is not a valid UUID'.format(map[key]) + raise falcon.HTTPBadRequest('Bad request', msg) + def validate_uuids(req, params): - id_keys = ['token_id', 'auth_id', 'host_id', 'user_id', 'project-id', 'instance-id'] - if req.method in ('POST', 'PUT'): + id_keys = ['token_id', 'auth_id', 'host_id', 'user_id', 'project-id', 'instance-id'] + if req.method in ('POST', 'PUT'): + for key in id_keys: + if key in req.body: + validate_uuid(req.body, key) for key in id_keys: - if key in req.body: - validate_uuid(req.body, key) - for key in id_keys: - if key in params: - validate_uuid(params, key) + if key in params: + validate_uuid(params, key) + def validate(req, resp, resource, params): - if req.content_length: - # Store the body since we cannot read the stream again later - req.body = json.load(req.stream) - elif req.method in ('POST', 'PUT'): - raise falcon.HTTPBadRequest('The POST/PUT request is missing a body.') - validate_uuids(req, params) + if req.content_length: + # Store the body since we cannot read the stream again later + req.body = json.load(req.stream) + elif req.method in ('POST', 'PUT'): + raise falcon.HTTPBadRequest('The POST/PUT request is missing a body.') + validate_uuids(req, params) + class Logger(object): - def __init__(self): - self.logger = logging.getLogger(__name__) + def __init__(self): + self.logger = logging.getLogger(__name__) - def process_resource(self, req, resp, resource, params): - self.logger.debug('Received request {0} {1} with headers {2}'.format(req.method, req.relative_uri, req.headers)) + def process_resource(self, req, resp, resource, params): + self.logger.debug('Received request {0} {1} with headers {2}'.format(req.method, req.relative_uri, req.headers)) + + def process_response(self, req, resp, resource, params): + self.logger.debug( + 'Request {0} {1} with body {2} produced response ' + 'with status {3} location {4} and body {5}'.format( + req.method, req.relative_uri, + req.body if hasattr(req, 'body') else 'None', + resp.status, resp.location, resp.body)) - def process_response(self, req, resp, resource, params): - self.logger.debug( - 'Request {0} {1} with body {2} produced response ' - 'with status {3} location {4} and body {5}'.format( - req.method, req.relative_uri, - req.body if hasattr(req, 'body') else 'None', - resp.status, resp.location, resp.body)) class Authorities(object): + @falcon.before(validate) + def on_post(self, req, resp): + try: + db.createAuthority( + self.session, + req.body['auth_id'], + ) + except KeyError as e: + raise falcon.HTTPBadRequest(str(e)) + resp.status = falcon.HTTP_201 + resp.location = '/authorities/' + req.body['auth_id'] - @falcon.before(validate) - def on_post(self, req, resp): - try: - db.createAuthority( - self.session, - req.body['auth_id'], - ) - except KeyError as e: - raise falcon.HTTPBadRequest(str(e)) - resp.status = falcon.HTTP_201 - resp.location = '/authorities/' + req.body['auth_id'] class Authority(object): + @falcon.before(validate) + def on_get(self, req, resp, auth_id): + auth = db.getAuthority(self.session, auth_id) + if auth is None: + resp.status = falcon.HTTP_NOT_FOUND + return + user_key = RSA.importKey(db.getAuthUserKey(auth)) + user_pub_key = user_key.publickey().exportKey('OpenSSH') + host_key = RSA.importKey(db.getAuthHostKey(auth)) + host_pub_key = host_key.publickey().exportKey('OpenSSH') + body = { + 'auth_id': auth_id, + 'user_key.pub': user_pub_key, + 'host_key.pub': host_pub_key + } + resp.body = json.dumps(body) + resp.status = falcon.HTTP_OK - @falcon.before(validate) - def on_get(self, req, resp, auth_id): - auth = db.getAuthority(self.session, auth_id) - if auth is None: - resp.status = falcon.HTTP_NOT_FOUND - return - user_key = RSA.importKey(db.getAuthUserKey(auth)) - user_pub_key = user_key.publickey().exportKey('OpenSSH') - host_key = RSA.importKey(db.getAuthHostKey(auth)) - host_pub_key = host_key.publickey().exportKey('OpenSSH') - body = { - 'auth_id': auth_id, - 'user_key.pub': user_pub_key, - 'host_key.pub': host_pub_key - } - resp.body = json.dumps(body) - resp.status = falcon.HTTP_OK class UserCerts(object): + @falcon.before(validate) + def on_post(self, req, resp): + # TODO: validation + try: + user = db.createUserCert( + self.session, + req.body['user_id'], + req.body['auth_id'], + req.body['key.pub'] + ) + except KeyError as e: + raise falcon.HTTPBadRequest(str(e)) + resp.status = falcon.HTTP_201 + resp.location = '/usercerts/' + user.user_id + '/' + user.fingerprint - @falcon.before(validate) - def on_post(self, req, resp): - # TODO: validation - try: - user = db.createUserCert( - self.session, - req.body['user_id'], - req.body['auth_id'], - req.body['key.pub'] - ) - except KeyError as e: - raise falcon.HTTPBadRequest(str(e)) - resp.status = falcon.HTTP_201 - resp.location = '/usercerts/' + user.user_id + '/' + user.fingerprint class UserCert(object): + @falcon.before(validate) + def on_get(self, req, resp, user_id, fingerprint): + user = db.getUserCert(self.session, user_id, fingerprint) + if user is None: + resp.status = falcon.HTTP_NOT_FOUND + return + body = { + 'user_id': user.user_id, + 'fingerprint': user.fingerprint, + 'auth_id': user.auth_id, + 'key-cert.pub': user.cert + } + resp.body = json.dumps(body) + resp.status = falcon.HTTP_OK - @falcon.before(validate) - def on_get(self, req, resp, user_id, fingerprint): - user = db.getUserCert(self.session, user_id, fingerprint) - if user is None: - resp.status = falcon.HTTP_NOT_FOUND - return - body = { - 'user_id': user.user_id, - 'fingerprint': user.fingerprint, - 'auth_id': user.auth_id, - 'key-cert.pub': user.cert - } - resp.body = json.dumps(body) - resp.status = falcon.HTTP_OK def hostToJson(host): - return json.dumps({ - 'host_id': host.host_id, - 'fingerprint': host.fingerprint, - 'auth_id': host.auth_id, - 'key-cert.pub': host.cert, - }) + return json.dumps({ + 'host_id': host.host_id, + 'fingerprint': host.fingerprint, + 'auth_id': host.auth_id, + 'key-cert.pub': host.cert, + }) + class HostCerts(object): + @falcon.before(validate) + def on_post(self, req, resp): + # Note that we could have found the host_id using the token_id. + # But requiring the host_id makes it a little harder to steal the token. + try: + host = db.createHostCert( + self.session, + req.body['token_id'], + req.body['host_id'], + req.body['key.pub'] + ) + except KeyError as e: + raise falcon.HTTPBadRequest(str(e)) + resp.body = hostToJson(host) + resp.status = falcon.HTTP_201 + resp.location = '/hostcerts/' + host.host_id + '/' + host.fingerprint - @falcon.before(validate) - def on_post(self, req, resp): - # Note that we could have found the host_id using the token_id. - # But requiring the host_id makes it a little harder to steal the token. - try: - host = db.createHostCert( - self.session, - req.body['token_id'], - req.body['host_id'], - req.body['key.pub'] - ) - except KeyError as e: - raise falcon.HTTPBadRequest(str(e)) - resp.body = hostToJson(host) - resp.status = falcon.HTTP_201 - resp.location = '/hostcerts/' + host.host_id + '/' + host.fingerprint class HostCert(object): + @falcon.before(validate) + def on_get(self, req, resp, host_id, fingerprint): + host = db.getHostCert(self.session, host_id, fingerprint) + if host is None: + resp.status = falcon.HTTP_NOT_FOUND + return + resp.body = hostToJson(host) + resp.status = falcon.HTTP_OK - @falcon.before(validate) - def on_get(self, req, resp, host_id, fingerprint): - host = db.getHostCert(self.session, host_id, fingerprint) - if host is None: - resp.status = falcon.HTTP_NOT_FOUND - return - resp.body = hostToJson(host) - resp.status = falcon.HTTP_OK class Tokens(object): + @falcon.before(validate) + def on_post(self, req, resp): + try: + token = db.createToken( + self.session, + req.body['host_id'], + req.body['auth_id'], + req.body['hostname'] + ) + except KeyError as e: + raise falcon.HTTPBadRequest(str(e)) + resp.status = falcon.HTTP_201 + resp.location = '/hosttokens/' + token.token_id - @falcon.before(validate) - def on_post(self, req, resp): - try: - token = db.createToken( - self.session, - req.body['host_id'], - req.body['auth_id'], - req.body['hostname'] - ) - except KeyError as e: - raise falcon.HTTPBadRequest(str(e)) - resp.status = falcon.HTTP_201 - resp.location = '/hosttokens/' + token.token_id class NovaVendorData(object): - - @falcon.before(validate) - def on_post(self, req, resp): - # An example of the data nova sends to vendordata services: - # { - # "hostname": "foo", - # "image-id": "75a74383-f276-4774-8074-8c4e3ff2ca64", - # "instance-id": "2ae914e9-f5ab-44ce-b2a2-dcf8373d899d", - # "metadata": {}, - # "project-id": "039d104b7a5c4631b4ba6524d0b9e981", - # "user-data": null - # } - try: - token = db.createToken( - self.session, - req.body['instance-id'], - req.body['project-id'], - req.body['hostname'] - ) - except KeyError as e: - raise falcon.HTTPBadRequest(str(e)) - auth = db.getAuthority(self.session, req.body['project-id']) - if auth is None: - resp.status = falcon.HTTP_NOT_FOUND - return - key = RSA.importKey(auth.user_key) - pub_key = key.publickey().exportKey('OpenSSH') - vendordata = { - 'token': token.token_id, - 'auth_pub_key_user': pub_key, - 'principals': 'admin' - } - resp.body = json.dumps(vendordata) - resp.location = '/hosttokens/' + token.token_id - resp.status = falcon.HTTP_201 + @falcon.before(validate) + def on_post(self, req, resp): + # An example of the data nova sends to vendordata services: + # { + # "hostname": "foo", + # "image-id": "75a74383-f276-4774-8074-8c4e3ff2ca64", + # "instance-id": "2ae914e9-f5ab-44ce-b2a2-dcf8373d899d", + # "metadata": {}, + # "project-id": "039d104b7a5c4631b4ba6524d0b9e981", + # "user-data": null + # } + try: + token = db.createToken( + self.session, + req.body['instance-id'], + req.body['project-id'], + req.body['hostname'] + ) + except KeyError as e: + raise falcon.HTTPBadRequest(str(e)) + auth = db.getAuthority(self.session, req.body['project-id']) + if auth is None: + resp.status = falcon.HTTP_NOT_FOUND + return + key = RSA.importKey(auth.user_key) + pub_key = key.publickey().exportKey('OpenSSH') + vendordata = { + 'token': token.token_id, + 'auth_pub_key_user': pub_key, + 'principals': 'admin' + } + resp.body = json.dumps(vendordata) + resp.location = '/hosttokens/' + token.token_id + resp.status = falcon.HTTP_201 diff --git a/tatu/castellano.py b/tatu/castellano.py index 4e16c2b..a84918d 100644 --- a/tatu/castellano.py +++ b/tatu/castellano.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + from castellan.common.objects.passphrase import Passphrase from castellan.common.utils import credential_factory from castellan.key_manager import API @@ -16,6 +28,7 @@ CONF.register_opts(opts, group='tatu') _context = None _api = None + def validate_config(): if CONF.tatu.use_barbican_key_manager: set_castellan_defaults(CONF) @@ -23,17 +36,20 @@ def validate_config(): set_castellan_defaults(CONF, api_class='tatu.castellano.TatuKeyManager') + def context(): global _context if _context is None and CONF.tatu.use_barbican_key_manager: _context = credential_factory(conf=CONF) return _context + def api(): global _api if _api is None: _api = API() - return _api + return _api + def delete_secret(id, ctx=None): """delete a secret from the external key manager @@ -43,6 +59,7 @@ def delete_secret(id, ctx=None): """ api().delete(ctx or context(), id) + def get_secret(id, ctx=None): """get a secret associated with an id :param id: The identifier of the secret to retrieve @@ -52,6 +69,7 @@ def get_secret(id, ctx=None): key = api().get(ctx or context(), id) return key.get_encoded() + def store_secret(secret, ctx=None): """store a secret and return its identifier :param secret: The secret to store, this should be a string @@ -61,10 +79,13 @@ def store_secret(secret, ctx=None): key = Passphrase(secret) return api().store(ctx or context(), key) + """ This module contains the KeyManager class that will be used by the castellan library, it is not meant for direct usage within tatu. """ + + class TatuKeyManager(KeyManager): """Tatu specific key manager This manager is a thin wrapper around the secret being stored. It is @@ -73,6 +94,7 @@ class TatuKeyManager(KeyManager): This behavior allows Tatu to continue storing secrets in its database while using the Castellan key manager abstraction. """ + def __init__(self, configuration=None): pass diff --git a/tatu/db/models.py b/tatu/db/models.py index 16336fa..7ee8ef0 100644 --- a/tatu/db/models.py +++ b/tatu/db/models.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + from datetime import datetime import falcon import os @@ -6,100 +18,110 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.exc import IntegrityError import sshpubkeys from tatu.castellano import get_secret, store_secret -from tatu.utils import generateCert,random_uuid +from tatu.utils import generateCert, random_uuid import uuid from Crypto.PublicKey import RSA Base = declarative_base() -class Authority(Base): - __tablename__ = 'authorities' - auth_id = sa.Column(sa.String(36), primary_key=True) - user_key = sa.Column(sa.Text) - host_key = sa.Column(sa.Text) +class Authority(Base): + __tablename__ = 'authorities' + + auth_id = sa.Column(sa.String(36), primary_key=True) + user_key = sa.Column(sa.Text) + host_key = sa.Column(sa.Text) + def getAuthority(session, auth_id): - return session.query(Authority).get(auth_id) + return session.query(Authority).get(auth_id) + def getAuthUserKey(auth): - return get_secret(auth.user_key) + return get_secret(auth.user_key) + def getAuthHostKey(auth): - return get_secret(auth.host_key) + return get_secret(auth.host_key) + def createAuthority(session, auth_id): - user_key = RSA.generate(2048).exportKey('PEM') - user_secret_id = store_secret(user_key) - host_key = RSA.generate(2048).exportKey('PEM') - host_secret_id = store_secret(host_key) - auth = Authority(auth_id=auth_id, - user_key=user_secret_id, - host_key=host_secret_id) - session.add(auth) - try: - session.commit() - except IntegrityError: - raise falcon.HTTPConflict("This certificate authority already exists.") - return auth + user_key = RSA.generate(2048).exportKey('PEM') + user_secret_id = store_secret(user_key) + host_key = RSA.generate(2048).exportKey('PEM') + host_secret_id = store_secret(host_key) + auth = Authority(auth_id=auth_id, + user_key=user_secret_id, + host_key=host_secret_id) + session.add(auth) + try: + session.commit() + except IntegrityError: + raise falcon.HTTPConflict("This certificate authority already exists.") + return auth + class UserCert(Base): - __tablename__ = 'user_certs' + __tablename__ = 'user_certs' + + user_id = sa.Column(sa.String(36), primary_key=True) + fingerprint = sa.Column(sa.String(36), primary_key=True) + auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id')) + cert = sa.Column(sa.Text) - user_id = sa.Column(sa.String(36), primary_key=True) - fingerprint = sa.Column(sa.String(36), primary_key=True) - auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id')) - cert = sa.Column(sa.Text) def getUserCert(session, user_id, fingerprint): - return session.query(UserCert).get([user_id, fingerprint]) + return session.query(UserCert).get([user_id, fingerprint]) + def createUserCert(session, user_id, auth_id, pub): # Retrieve the authority's private key and generate the certificate auth = getAuthority(session, auth_id) if auth is None: - raise falcon.HTTPNotFound(description='No Authority found with that ID') + raise falcon.HTTPNotFound(description='No Authority found with that ID') fingerprint = sshpubkeys.SSHKey(pub).hash_md5() certRecord = session.query(UserCert).get([user_id, fingerprint]) if certRecord is not None: - return certRecord + return certRecord cert = generateCert(get_secret(auth.user_key), pub, principals='admin,root') if cert is None: - raise falcon.HTTPInternalServerError("Failed to generate the certificate") + raise falcon.HTTPInternalServerError("Failed to generate the certificate") user = UserCert( - user_id=user_id, - fingerprint=fingerprint, - auth_id=auth_id, - cert=cert + user_id=user_id, + fingerprint=fingerprint, + auth_id=auth_id, + cert=cert ) session.add(user) session.commit() return user -class Token(Base): - __tablename__ = 'tokens' - token_id = sa.Column(sa.String(36), primary_key=True, - default=random_uuid) - auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id')) - host_id = sa.Column(sa.String(36), index=True, unique=True) - hostname = sa.Column(sa.String(36)) - used = sa.Column(sa.Boolean, default=False) - date_used = sa.Column(sa.DateTime, default=datetime.min) - fingerprint_used = sa.Column(sa.String(36)) +class Token(Base): + __tablename__ = 'tokens' + + token_id = sa.Column(sa.String(36), primary_key=True, + default=random_uuid) + auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id')) + host_id = sa.Column(sa.String(36), index=True, unique=True) + hostname = sa.Column(sa.String(36)) + used = sa.Column(sa.Boolean, default=False) + date_used = sa.Column(sa.DateTime, default=datetime.min) + fingerprint_used = sa.Column(sa.String(36)) + def createToken(session, host_id, auth_id, hostname): # Validate the certificate authority auth = getAuthority(session, auth_id) if auth is None: - raise falcon.HTTPNotFound(description='No Authority found with that ID') - #Check whether a token was already created for this host_id + raise falcon.HTTPNotFound(description='No Authority found with that ID') + # Check whether a token was already created for this host_id try: - token = session.query(Token).filter(Token.host_id == host_id).one() - if token is not None: - return token + token = session.query(Token).filter(Token.host_id == host_id).one() + if token is not None: + return token except: - pass + pass token = Token(host_id=host_id, auth_id=auth_id, @@ -108,54 +130,57 @@ def createToken(session, host_id, auth_id, hostname): session.commit() return token -class HostCert(Base): - __tablename__ = 'host_certs' - host_id = sa.Column(sa.String(36), primary_key=True) - fingerprint = sa.Column(sa.String(36), primary_key=True) - auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id')) - token_id = sa.Column(sa.String(36), sa.ForeignKey('tokens.token_id')) - pubkey = sa.Column(sa.Text) - cert = sa.Column(sa.Text) - hostname = sa.Column(sa.String(36)) +class HostCert(Base): + __tablename__ = 'host_certs' + + host_id = sa.Column(sa.String(36), primary_key=True) + fingerprint = sa.Column(sa.String(36), primary_key=True) + auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id')) + token_id = sa.Column(sa.String(36), sa.ForeignKey('tokens.token_id')) + pubkey = sa.Column(sa.Text) + cert = sa.Column(sa.Text) + hostname = sa.Column(sa.String(36)) + def getHostCert(session, host_id, fingerprint): - return session.query(HostCert).get([host_id, fingerprint]) + return session.query(HostCert).get([host_id, fingerprint]) + def createHostCert(session, token_id, host_id, pub): - token = session.query(Token).get(token_id) + token = session.query(Token).get(token_id) if token is None: - raise falcon.HTTPNotFound(description='No Token found with that ID') + raise falcon.HTTPNotFound(description='No Token found with that ID') if token.host_id != host_id: - raise falcon.HTTPConflict(description='The token is not valid for this instance ID') + raise falcon.HTTPConflict(description='The token is not valid for this instance ID') fingerprint = sshpubkeys.SSHKey(pub).hash_md5() if token.used: - if token.fingerprint_used != fingerprint: - raise falcon.HTTPConflict(description='The token was previously used with a different public key') - # The token was already used for same host and pub key. Return record. - host = session.query(HostCert).get([host_id, fingerprint]) - if host is None: - raise falcon.HTTPInternalServerError( - description='The token was used, but no corresponding Host record was found.') - if host.token_id == token_id: - return host - raise falcon.HTTPConflict(description='The presented token was previously used') + if token.fingerprint_used != fingerprint: + raise falcon.HTTPConflict(description='The token was previously used with a different public key') + # The token was already used for same host and pub key. Return record. + host = session.query(HostCert).get([host_id, fingerprint]) + if host is None: + raise falcon.HTTPInternalServerError( + description='The token was used, but no corresponding Host record was found.') + if host.token_id == token_id: + return host + raise falcon.HTTPConflict(description='The presented token was previously used') auth = getAuthority(session, token.auth_id) if auth is None: - raise falcon.HTTPNotFound(description='No Authority found with that ID') + raise falcon.HTTPNotFound(description='No Authority found with that ID') certRecord = session.query(HostCert).get([host_id, fingerprint]) if certRecord is not None: - raise falcon.HTTPConflict('This public key is already signed.') + raise falcon.HTTPConflict('This public key is already signed.') cert = generateCert(get_secret(auth.host_key), pub, hostname=token.hostname) if cert == '': - raise falcon.HTTPInternalServerError("Failed to generate the certificate") + raise falcon.HTTPInternalServerError("Failed to generate the certificate") host = HostCert(host_id=host_id, fingerprint=fingerprint, auth_id=token.auth_id, token_id=token_id, - pubkey = pub, + pubkey=pub, cert=cert, hostname=token.hostname) session.add(host) diff --git a/tatu/db/persistence.py b/tatu/db/persistence.py index 0f449da..b9be131 100644 --- a/tatu/db/persistence.py +++ b/tatu/db/persistence.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import os from sqlalchemy import create_engine @@ -7,7 +19,8 @@ from tatu.db.models import Base def get_url(): return os.getenv("DATABASE_URL", "sqlite:///development.db") - #return os.getenv("DATABASE_URL", "sqlite:///:memory:") + # return os.getenv("DATABASE_URL", "sqlite:///:memory:") + class SQLAlchemySessionManager: """ diff --git a/tatu/ftests/test_api.py b/tatu/ftests/test_api.py index a8cf97c..fca6870 100644 --- a/tatu/ftests/test_api.py +++ b/tatu/ftests/test_api.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import json import requests import os @@ -8,90 +20,93 @@ import uuid server = 'http://172.24.4.1:18322' + def vendordata_request(instance_id, project_id, hostname): - return { - 'instance-id': instance_id, - 'project-id': project_id, - 'hostname': hostname - } + return { + 'instance-id': instance_id, + 'project-id': project_id, + 'hostname': hostname + } + def host_request(token, host, pub_key): - return { - 'token_id': token, - 'host_id': host, - 'key.pub': pub_key - } + return { + 'token_id': token, + 'host_id': host, + 'key.pub': pub_key + } + def test_host_certificate_generation(): - project_id = random_uuid() - response = requests.post( - server + '/authorities', - data=json.dumps({'auth_id': project_id}) - ) - assert response.status_code == 201 - assert 'location' in response.headers - assert response.headers['location'] == '/authorities/' + project_id - - response = requests.get(server + response.headers['location']) - assert response.status_code == 200 - auth = json.loads(response.content) - assert 'auth_id' in auth - assert auth['auth_id'] == project_id - assert 'user_key.pub' in auth - assert 'host_key.pub' in auth - ca_user = auth['user_key.pub'] - - key = RSA.generate(2048) - pub_key = key.publickey().exportKey('OpenSSH') - fingerprint = sshpubkeys.SSHKey(pub_key).hash_md5() - for i in range(1): - instance_id = random_uuid() - hostname = 'host{}'.format(i) - # Simulate Nova's separate requests for each version of metadata API - vendordata = None - token = None - for j in range(3): - response = requests.post( - server + '/novavendordata', - data=json.dumps(vendordata_request(instance_id, project_id, hostname)) - ) - assert response.status_code == 201 - assert 'location' in response.headers - location_path = response.headers['location'].split('/') - assert location_path[1] == 'hosttokens' - vendordata = json.loads(response.content) - assert 'token' in vendordata - tok = vendordata['token'] - if token is None: - token = tok - else: - assert token == tok - assert token == location_path[-1] - assert 'auth_pub_key_user' in vendordata - assert vendordata['auth_pub_key_user'] == ca_user - assert 'principals' in vendordata - assert vendordata['principals'] == 'admin' - + project_id = random_uuid() response = requests.post( - server + '/noauth/hostcerts', - data=json.dumps(host_request(token, instance_id, pub_key)) + server + '/authorities', + data=json.dumps({'auth_id': project_id}) ) assert response.status_code == 201 assert 'location' in response.headers - location = response.headers['location'] - location_path = location.split('/') - assert location_path[1] == 'hostcerts' - assert location_path[2] == instance_id - assert location_path[3] == fingerprint + assert response.headers['location'] == '/authorities/' + project_id - response = requests.get(server + location) + response = requests.get(server + response.headers['location']) assert response.status_code == 200 - hostcert = json.loads(response.content) - assert 'host_id' in hostcert - assert hostcert['host_id'] == instance_id - assert 'fingerprint' in hostcert - assert hostcert['fingerprint'] - assert 'auth_id' in hostcert - auth_id = str(uuid.UUID(hostcert['auth_id'], version=4)) - assert auth_id == project_id - assert 'key-cert.pub' in hostcert + auth = json.loads(response.content) + assert 'auth_id' in auth + assert auth['auth_id'] == project_id + assert 'user_key.pub' in auth + assert 'host_key.pub' in auth + ca_user = auth['user_key.pub'] + + key = RSA.generate(2048) + pub_key = key.publickey().exportKey('OpenSSH') + fingerprint = sshpubkeys.SSHKey(pub_key).hash_md5() + for i in range(1): + instance_id = random_uuid() + hostname = 'host{}'.format(i) + # Simulate Nova's separate requests for each version of metadata API + vendordata = None + token = None + for j in range(3): + response = requests.post( + server + '/novavendordata', + data=json.dumps(vendordata_request(instance_id, project_id, hostname)) + ) + assert response.status_code == 201 + assert 'location' in response.headers + location_path = response.headers['location'].split('/') + assert location_path[1] == 'hosttokens' + vendordata = json.loads(response.content) + assert 'token' in vendordata + tok = vendordata['token'] + if token is None: + token = tok + else: + assert token == tok + assert token == location_path[-1] + assert 'auth_pub_key_user' in vendordata + assert vendordata['auth_pub_key_user'] == ca_user + assert 'principals' in vendordata + assert vendordata['principals'] == 'admin' + + response = requests.post( + server + '/noauth/hostcerts', + data=json.dumps(host_request(token, instance_id, pub_key)) + ) + assert response.status_code == 201 + assert 'location' in response.headers + location = response.headers['location'] + location_path = location.split('/') + assert location_path[1] == 'hostcerts' + assert location_path[2] == instance_id + assert location_path[3] == fingerprint + + response = requests.get(server + location) + assert response.status_code == 200 + hostcert = json.loads(response.content) + assert 'host_id' in hostcert + assert hostcert['host_id'] == instance_id + assert 'fingerprint' in hostcert + assert hostcert['fingerprint'] + assert 'auth_id' in hostcert + auth_id = str(uuid.UUID(hostcert['auth_id'], version=4)) + assert auth_id == project_id + assert 'key-cert.pub' in hostcert diff --git a/tatu/notifications.py b/tatu/notifications.py index 8ead36e..2a757f9 100644 --- a/tatu/notifications.py +++ b/tatu/notifications.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + from oslo_config import cfg from oslo_log import log as logging import oslo_messaging @@ -14,15 +26,15 @@ LOG = logging.getLogger(__name__) CONF = cfg.CONF DOMAIN = 'tatu' -class NotificationEndpoint(object): +class NotificationEndpoint(object): filter_rule = oslo_messaging.NotificationFilter( publisher_id='^identity.*', event_type='^identity.project.created') def __init__(self): self.engine = create_engine(get_url()) - #Base.metadata.create_all(self.engine) + # Base.metadata.create_all(self.engine) self.Session = scoped_session(sessionmaker(self.engine)) def info(self, ctxt, publisher_id, event_type, payload, metadata): @@ -46,12 +58,13 @@ class NotificationEndpoint(object): else: LOG.error("Status update or unknown") + def main(): logging.register_options(CONF) extra_log_level_defaults = ['tatu=DEBUG', '__main__=DEBUG'] logging.set_defaults( default_log_levels=logging.get_default_log_levels() + - extra_log_level_defaults) + extra_log_level_defaults) logging.setup(CONF, DOMAIN) transport = oslo_messaging.get_notification_transport(CONF) @@ -74,5 +87,6 @@ def main(): server.stop() server.wait() + if __name__ == "__main__": sys.exit(main()) diff --git a/tatu/tests/test_app.py b/tatu/tests/test_app.py index 8a022e8..1763175 100644 --- a/tatu/tests/test_app.py +++ b/tatu/tests/test_app.py @@ -1,4 +1,14 @@ -# coding=utf-8 +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. import json import falcon from falcon import testing @@ -12,14 +22,17 @@ from Crypto.PublicKey import RSA import sshpubkeys import time + @pytest.fixture def db(): - return SQLAlchemySessionManager() + return SQLAlchemySessionManager() + @pytest.fixture def client(db): - api = create_app(db) - return testing.TestClient(api) + api = create_app(db) + return testing.TestClient(api) + token_id = '' @@ -36,404 +49,438 @@ user_fingerprint = sshpubkeys.SSHKey(user_pub_key).hash_md5() auth_id = random_uuid() auth_user_pub_key = None + @pytest.mark.dependency() def test_post_authority(client, auth_id=auth_id): - body = { - 'auth_id': auth_id, - } - response = client.simulate_post( - '/authorities', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_CREATED - assert response.headers['location'] == '/authorities/' + auth_id + body = { + 'auth_id': auth_id, + } + response = client.simulate_post( + '/authorities', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_CREATED + assert response.headers['location'] == '/authorities/' + auth_id + @pytest.mark.dependency(depends=['test_post_authority']) def test_post_authority_duplicate(client): - body = { - 'auth_id': auth_id, - } - response = client.simulate_post( - '/authorities', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_CONFLICT + body = { + 'auth_id': auth_id, + } + response = client.simulate_post( + '/authorities', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_CONFLICT + def test_post_no_body(client): - for path in ['/authorities', '/usercerts', '/hosttokens', - '/hostcerts', '/novavendordata']: - response = client.simulate_post(path) - assert response.status == falcon.HTTP_BAD_REQUEST + for path in ['/authorities', '/usercerts', '/hosttokens', + '/hostcerts', '/novavendordata']: + response = client.simulate_post(path) + assert response.status == falcon.HTTP_BAD_REQUEST + def test_post_empty_body(client): - bodystr = json.dumps({}) - for path in ['/authorities', '/usercerts', '/hosttokens', - '/hostcerts', '/novavendordata']: - response = client.simulate_post(path, body=bodystr) - assert response.status == falcon.HTTP_BAD_REQUEST + bodystr = json.dumps({}) + for path in ['/authorities', '/usercerts', '/hosttokens', + '/hostcerts', '/novavendordata']: + response = client.simulate_post(path, body=bodystr) + assert response.status == falcon.HTTP_BAD_REQUEST + def test_post_authority_bad_uuid(client): - body = { - 'auth_id': 'foobar', - } - response = client.simulate_post( - '/authorities', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_BAD_REQUEST + body = { + 'auth_id': 'foobar', + } + response = client.simulate_post( + '/authorities', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_BAD_REQUEST + @pytest.mark.dependency(depends=['test_post_authority']) def test_get_authority(client): - response = client.simulate_get('/authorities/' + auth_id) - assert response.status == falcon.HTTP_OK - body = json.loads(response.content) - assert 'auth_id' in body - assert 'user_key.pub' in body - global auth_user_pub_key - auth_user_pub_key = body['user_key.pub'] - assert 'host_key.pub' in body - assert 'user_key' not in body - assert 'host_key' not in body + response = client.simulate_get('/authorities/' + auth_id) + assert response.status == falcon.HTTP_OK + body = json.loads(response.content) + assert 'auth_id' in body + assert 'user_key.pub' in body + global auth_user_pub_key + auth_user_pub_key = body['user_key.pub'] + assert 'host_key.pub' in body + assert 'user_key' not in body + assert 'host_key' not in body + def test_get_authority_doesnt_exist(client): - response = client.simulate_get('/authorities/' + random_uuid()) - assert response.status == falcon.HTTP_NOT_FOUND + response = client.simulate_get('/authorities/' + random_uuid()) + assert response.status == falcon.HTTP_NOT_FOUND + def test_get_authority_with_bad_uuid(client): - response = client.simulate_get('/authorities/foobar') - assert response.status == falcon.HTTP_BAD_REQUEST + response = client.simulate_get('/authorities/foobar') + assert response.status == falcon.HTTP_BAD_REQUEST + def user_request(auth=auth_id, user_id=user_id, pub_key=user_pub_key): - return { - 'user_id': user_id, - 'auth_id': auth, - 'key.pub': pub_key - } + return { + 'user_id': user_id, + 'auth_id': auth, + 'key.pub': pub_key + } + def test_post_user_bad_uuid(client): - for key in ['user_id', 'auth_id']: - body = user_request() - body[key] = 'foobar' - response = client.simulate_post( - '/usercerts', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_BAD_REQUEST + for key in ['user_id', 'auth_id']: + body = user_request() + body[key] = 'foobar' + response = client.simulate_post( + '/usercerts', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_BAD_REQUEST + @pytest.mark.dependency(depends=['test_post_authority']) def test_post_user(client): - body = user_request() - response = client.simulate_post( - '/usercerts', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location = response.headers['location'].split('/') - assert location[1] == 'usercerts' - assert location[2] == body['user_id'] - assert location[3] == sshpubkeys.SSHKey(body['key.pub']).hash_md5() + body = user_request() + response = client.simulate_post( + '/usercerts', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location = response.headers['location'].split('/') + assert location[1] == 'usercerts' + assert location[2] == body['user_id'] + assert location[3] == sshpubkeys.SSHKey(body['key.pub']).hash_md5() + @pytest.mark.dependency(depends=['test_post_user']) def test_get_user(client): - response = client.simulate_get('/usercerts/' + user_id + '/' + user_fingerprint) - assert response.status == falcon.HTTP_OK - body = json.loads(response.content) - assert 'user_id' in body - assert 'fingerprint' in body - assert 'auth_id' in body - assert 'key-cert.pub' in body - assert body['auth_id'] == auth_id + response = client.simulate_get('/usercerts/' + user_id + '/' + user_fingerprint) + assert response.status == falcon.HTTP_OK + body = json.loads(response.content) + assert 'user_id' in body + assert 'fingerprint' in body + assert 'auth_id' in body + assert 'key-cert.pub' in body + assert body['auth_id'] == auth_id + def test_get_user_doesnt_exist(client): - response = client.simulate_get('/usercerts/' + random_uuid() + '/' + user_fingerprint) - assert response.status == falcon.HTTP_NOT_FOUND + response = client.simulate_get('/usercerts/' + random_uuid() + '/' + user_fingerprint) + assert response.status == falcon.HTTP_NOT_FOUND + def test_get_user_with_bad_uuid(client): - response = client.simulate_get('/usercerts/foobar/' + user_fingerprint) - assert response.status == falcon.HTTP_BAD_REQUEST + response = client.simulate_get('/usercerts/foobar/' + user_fingerprint) + assert response.status == falcon.HTTP_BAD_REQUEST + @pytest.mark.dependency(depends=['test_post_user']) def test_post_second_cert_same_user(client): - key = RSA.generate(2048) - pub_key = key.publickey().exportKey('OpenSSH') - body = user_request(pub_key=pub_key) - response = client.simulate_post( - '/usercerts', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location = response.headers['location'].split('/') - assert location[1] == 'usercerts' - assert location[2] == user_id - assert location[3] == sshpubkeys.SSHKey(pub_key).hash_md5() + key = RSA.generate(2048) + pub_key = key.publickey().exportKey('OpenSSH') + body = user_request(pub_key=pub_key) + response = client.simulate_post( + '/usercerts', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location = response.headers['location'].split('/') + assert location[1] == 'usercerts' + assert location[2] == user_id + assert location[3] == sshpubkeys.SSHKey(pub_key).hash_md5() + def test_post_user_unknown_auth(client): - body = user_request(auth=random_uuid()) - response = client.simulate_post( - '/usercerts', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_NOT_FOUND + body = user_request(auth=random_uuid()) + response = client.simulate_post( + '/usercerts', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_NOT_FOUND + @pytest.mark.dependency(depends=['test_post_user']) def test_post_same_user_and_key_returns_same_result(client): - test_post_user(client) + test_post_user(client) + def token_request(auth=auth_id, host=host_id): - return { - 'host_id': host, - 'auth_id': auth, - 'hostname': 'testname.local' - } + return { + 'host_id': host, + 'auth_id': auth, + 'hostname': 'testname.local' + } + def host_request(token, host=host_id, pub_key=host_pub_key): - return { - 'token_id': token, - 'host_id': host, - 'key.pub': pub_key - } + return { + 'token_id': token, + 'host_id': host, + 'key.pub': pub_key + } + def vendordata_request(auth, host): - return { - 'instance-id': host, - 'project-id': auth, - 'hostname': 'mytest.testing' - } + return { + 'instance-id': host, + 'project-id': auth, + 'hostname': 'mytest.testing' + } + def test_post_vendordata_bad_uuid(client): - for key in ['instance-id', 'project-id']: - body = vendordata_request(auth_id, host_id) - body[key] = 'foobar' - response = client.simulate_post( - '/novavendordata', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_BAD_REQUEST + for key in ['instance-id', 'project-id']: + body = vendordata_request(auth_id, host_id) + body[key] = 'foobar' + response = client.simulate_post( + '/novavendordata', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_BAD_REQUEST + @pytest.mark.dependency(depends=['test_post_authority']) def test_post_novavendordata(client): - req = vendordata_request(auth_id, random_uuid()) - response = client.simulate_post( - '/novavendordata', - body=json.dumps(req) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location_path = response.headers['location'].split('/') - assert location_path[1] == 'hosttokens' - vendordata = json.loads(response.content) - assert 'token' in vendordata - assert vendordata['token'] == location_path[-1] - assert 'auth_pub_key_user' in vendordata - assert vendordata['auth_pub_key_user'] == auth_user_pub_key - assert 'principals' in vendordata - assert vendordata['principals'] == 'admin' - -def test_post_token_bad_uuid(client): - for key in ['auth_id', 'host_id']: - body = token_request() - body[key] = 'foobar' + req = vendordata_request(auth_id, random_uuid()) response = client.simulate_post( - '/hosttokens', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_BAD_REQUEST - -def test_post_host_bad_uuid(client): - for key in ['token_id', 'host_id']: - body = host_request(random_uuid()) - body[key] = 'foobar' - response = client.simulate_post( - '/hosttokens', - body=json.dumps(body) - ) - assert response.status == falcon.HTTP_BAD_REQUEST - -@pytest.mark.dependency(depends=['test_post_authority']) -def test_post_token_and_host(client): - token = token_request() - response = client.simulate_post( - '/hosttokens', - body=json.dumps(token) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location_path = response.headers['location'].split('/') - assert location_path[1] == 'hosttokens' - # Store the token ID for other tests - global token_id - token_id = location_path[-1] - # Verify that it's a valid UUID - uuid.UUID(token_id, version=4) - host = host_request(token_id) - response = client.simulate_post( - '/hostcerts', - body=json.dumps(host) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location = response.headers['location'].split('/') - assert location[1] == 'hostcerts' - assert location[2] == host_id - assert location[3] == host_fingerprint - # Re-trying the same exact calls returns identical results - response = client.simulate_post( - '/hostcerts', - body=json.dumps(host) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location = response.headers['location'].split('/') - assert location[1] == 'hostcerts' - assert location[2] == host_id - assert location[3] == host_fingerprint - - -def test_stress_post_token_and_host(client): - my_auth_id = random_uuid() - test_post_authority(client, my_auth_id) - # Generate a single RSA key pair and reuse it - it takes a few seconds. - key = RSA.generate(2048) - pub_key = key.publickey().exportKey('OpenSSH') - fingerprint = sshpubkeys.SSHKey(pub_key).hash_md5() - # Should do about 15 iterations/second, so only do 4 seconds worth. - start = time.time() - for i in range(60): - hid = random_uuid() - token = token_request(auth=my_auth_id, host=hid) - response = client.simulate_post( - '/hosttokens', - body=json.dumps(token) + '/novavendordata', + body=json.dumps(req) ) assert response.status == falcon.HTTP_CREATED assert 'location' in response.headers location_path = response.headers['location'].split('/') assert location_path[1] == 'hosttokens' + vendordata = json.loads(response.content) + assert 'token' in vendordata + assert vendordata['token'] == location_path[-1] + assert 'auth_pub_key_user' in vendordata + assert vendordata['auth_pub_key_user'] == auth_user_pub_key + assert 'principals' in vendordata + assert vendordata['principals'] == 'admin' + + +def test_post_token_bad_uuid(client): + for key in ['auth_id', 'host_id']: + body = token_request() + body[key] = 'foobar' + response = client.simulate_post( + '/hosttokens', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_BAD_REQUEST + + +def test_post_host_bad_uuid(client): + for key in ['token_id', 'host_id']: + body = host_request(random_uuid()) + body[key] = 'foobar' + response = client.simulate_post( + '/hosttokens', + body=json.dumps(body) + ) + assert response.status == falcon.HTTP_BAD_REQUEST + + +@pytest.mark.dependency(depends=['test_post_authority']) +def test_post_token_and_host(client): + token = token_request() + response = client.simulate_post( + '/hosttokens', + body=json.dumps(token) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location_path = response.headers['location'].split('/') + assert location_path[1] == 'hosttokens' + # Store the token ID for other tests + global token_id token_id = location_path[-1] # Verify that it's a valid UUID uuid.UUID(token_id, version=4) - host = host_request(token_id, host=hid, pub_key=pub_key) + host = host_request(token_id) response = client.simulate_post( - '/hostcerts', - body=json.dumps(host) + '/hostcerts', + body=json.dumps(host) ) assert response.status == falcon.HTTP_CREATED assert 'location' in response.headers location = response.headers['location'].split('/') assert location[1] == 'hostcerts' - assert location[2] == hid - assert location[3] == fingerprint - assert time.time() - start < 5 + assert location[2] == host_id + assert location[3] == host_fingerprint + # Re-trying the same exact calls returns identical results + response = client.simulate_post( + '/hostcerts', + body=json.dumps(host) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location = response.headers['location'].split('/') + assert location[1] == 'hostcerts' + assert location[2] == host_id + assert location[3] == host_fingerprint + + +def test_stress_post_token_and_host(client): + my_auth_id = random_uuid() + test_post_authority(client, my_auth_id) + # Generate a single RSA key pair and reuse it - it takes a few seconds. + key = RSA.generate(2048) + pub_key = key.publickey().exportKey('OpenSSH') + fingerprint = sshpubkeys.SSHKey(pub_key).hash_md5() + # Should do about 15 iterations/second, so only do 4 seconds worth. + start = time.time() + for i in range(60): + hid = random_uuid() + token = token_request(auth=my_auth_id, host=hid) + response = client.simulate_post( + '/hosttokens', + body=json.dumps(token) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location_path = response.headers['location'].split('/') + assert location_path[1] == 'hosttokens' + token_id = location_path[-1] + # Verify that it's a valid UUID + uuid.UUID(token_id, version=4) + host = host_request(token_id, host=hid, pub_key=pub_key) + response = client.simulate_post( + '/hostcerts', + body=json.dumps(host) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location = response.headers['location'].split('/') + assert location[1] == 'hostcerts' + assert location[2] == hid + assert location[3] == fingerprint + assert time.time() - start < 5 + @pytest.mark.dependency(depends=['test_post_token_and_host']) def test_post_token_same_host_id(client): - # Posting with the same host ID should return the same token - token = token_request() - response = client.simulate_post( - '/hosttokens', - body=json.dumps(token) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location_path = response.headers['location'].split('/') - assert location_path[1] == 'hosttokens' - # The token id should be the same as that from the previous test. - assert token_id == location_path[-1] + # Posting with the same host ID should return the same token + token = token_request() + response = client.simulate_post( + '/hosttokens', + body=json.dumps(token) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location_path = response.headers['location'].split('/') + assert location_path[1] == 'hosttokens' + # The token id should be the same as that from the previous test. + assert token_id == location_path[-1] + @pytest.mark.dependency(depends=['test_post_token_and_host']) def test_get_host(client): - response = client.simulate_get('/hostcerts/' + host_id + '/' + host_fingerprint) - assert response.status == falcon.HTTP_OK - body = json.loads(response.content) - assert 'host_id' in body - assert 'fingerprint' in body - assert 'auth_id' in body - assert 'key-cert.pub' in body - assert body['host_id'] == host_id - assert body['fingerprint'] == host_fingerprint - assert body['auth_id'] == auth_id + response = client.simulate_get('/hostcerts/' + host_id + '/' + host_fingerprint) + assert response.status == falcon.HTTP_OK + body = json.loads(response.content) + assert 'host_id' in body + assert 'fingerprint' in body + assert 'auth_id' in body + assert 'key-cert.pub' in body + assert body['host_id'] == host_id + assert body['fingerprint'] == host_fingerprint + assert body['auth_id'] == auth_id + def test_get_host_doesnt_exist(client): - response = client.simulate_get('/hostcerts/' + random_uuid() + '/' + host_fingerprint) - assert response.status == falcon.HTTP_NOT_FOUND + response = client.simulate_get('/hostcerts/' + random_uuid() + '/' + host_fingerprint) + assert response.status == falcon.HTTP_NOT_FOUND + def test_get_host_with_bad_uuid(client): - response = client.simulate_get('/hostcerts/foobar/' + host_fingerprint) - assert response.status == falcon.HTTP_BAD_REQUEST + response = client.simulate_get('/hostcerts/foobar/' + host_fingerprint) + assert response.status == falcon.HTTP_BAD_REQUEST + def test_post_token_unknown_auth(client): - token = token_request(auth=random_uuid()) - response = client.simulate_post( - '/hosttokens', - body=json.dumps(token) - ) - assert response.status == falcon.HTTP_NOT_FOUND + token = token_request(auth=random_uuid()) + response = client.simulate_post( + '/hosttokens', + body=json.dumps(token) + ) + assert response.status == falcon.HTTP_NOT_FOUND + @pytest.mark.dependency(depends=['test_post_authority']) def test_post_host_with_bogus_token(client): - host = host_request(random_uuid(), random_uuid()) - response = client.simulate_post( - '/hostcerts', - body=json.dumps(host) - ) - assert response.status == falcon.HTTP_NOT_FOUND + host = host_request(random_uuid(), random_uuid()) + response = client.simulate_post( + '/hostcerts', + body=json.dumps(host) + ) + assert response.status == falcon.HTTP_NOT_FOUND + @pytest.mark.dependency(depends=['test_post_token_and_host']) def test_post_host_with_wrong_host_id(client): - # Get a new token for the same host_id as the base test. - token = token_request(host=random_uuid()) - response = client.simulate_post( - '/hosttokens', - body=json.dumps(token) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location_path = response.headers['location'].split('/') - assert location_path[1] == 'hosttokens' - # Use the token with a different host_id than it was created for. - # Use a different public key to avoid other error conditions. - key = RSA.generate(2048) - pub_key = key.publickey().exportKey('OpenSSH') - host = host_request(location_path[-1], random_uuid(), pub_key) - response = client.simulate_post( - '/hostcerts', - body=json.dumps(host) - ) - assert response.status == falcon.HTTP_CONFLICT + # Get a new token for the same host_id as the base test. + token = token_request(host=random_uuid()) + response = client.simulate_post( + '/hosttokens', + body=json.dumps(token) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location_path = response.headers['location'].split('/') + assert location_path[1] == 'hosttokens' + # Use the token with a different host_id than it was created for. + # Use a different public key to avoid other error conditions. + key = RSA.generate(2048) + pub_key = key.publickey().exportKey('OpenSSH') + host = host_request(location_path[-1], random_uuid(), pub_key) + response = client.simulate_post( + '/hostcerts', + body=json.dumps(host) + ) + assert response.status == falcon.HTTP_CONFLICT + @pytest.mark.dependency(depends=['test_post_token_and_host']) def test_post_host_different_public_key_fails(client): - # Use the same token compared to the test this depends on. - # Show that using the same host ID and different public key fails. - token = token_request() - response = client.simulate_post( - '/hosttokens', - body=json.dumps(token) - ) - assert response.status == falcon.HTTP_CREATED - assert 'location' in response.headers - location_path = response.headers['location'].split('/') - assert location_path[1] == 'hosttokens' - key = RSA.generate(2048) - pub_key = key.publickey().exportKey('OpenSSH') - host = host_request(location_path[-1], pub_key=pub_key) - response = client.simulate_post( - '/hostcerts', - body=json.dumps(host) - ) - assert response.status == falcon.HTTP_CONFLICT + # Use the same token compared to the test this depends on. + # Show that using the same host ID and different public key fails. + token = token_request() + response = client.simulate_post( + '/hosttokens', + body=json.dumps(token) + ) + assert response.status == falcon.HTTP_CREATED + assert 'location' in response.headers + location_path = response.headers['location'].split('/') + assert location_path[1] == 'hosttokens' + key = RSA.generate(2048) + pub_key = key.publickey().exportKey('OpenSSH') + host = host_request(location_path[-1], pub_key=pub_key) + response = client.simulate_post( + '/hostcerts', + body=json.dumps(host) + ) + assert response.status == falcon.HTTP_CONFLICT + @pytest.mark.dependency(depends=['test_post_token_and_host']) def test_post_host_with_used_token(client): - # Re-use the token from the test this depends on. - # Use the same host_id and different public key to avoid other errors. - key = RSA.generate(2048) - pub_key = key.publickey().exportKey('OpenSSH') - host = host_request(token_id, host_id, pub_key) - response = client.simulate_post( - '/hostcerts', - body=json.dumps(host) - ) - assert response.status == falcon.HTTP_CONFLICT + # Re-use the token from the test this depends on. + # Use the same host_id and different public key to avoid other errors. + key = RSA.generate(2048) + pub_key = key.publickey().exportKey('OpenSSH') + host = host_request(token_id, host_id, pub_key) + response = client.simulate_post( + '/hostcerts', + body=json.dumps(host) + ) + assert response.status == falcon.HTTP_CONFLICT diff --git a/tatu/utils.py b/tatu/utils.py index 2318633..d0ed459 100644 --- a/tatu/utils.py +++ b/tatu/utils.py @@ -1,45 +1,59 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import os import subprocess import uuid + def random_uuid(): - return str(uuid.uuid4()) + return str(uuid.uuid4()) + def generateCert(auth_key, entity_key, hostname=None, principals='root'): - # Temporarily write the authority private key and entity public key to files - prefix = uuid.uuid4().hex - # Todo: make the temporary directory configurable or secure it. - dir = '/tmp/sshaas' - ca_file = ''.join([dir, prefix]) - pub_file = ''.join([dir, prefix, '.pub']) - cert_file = ''.join([dir, prefix, '-cert.pub']) - cert = '' - try: - fd = os.open(ca_file, os.O_WRONLY | os.O_CREAT, 0o600) - os.close(fd) - with open(ca_file, "w") as text_file: - text_file.write(auth_key) - with open(pub_file, "w", 0o644) as text_file: - text_file.write(entity_key) - args = ['ssh-keygen', '-s', ca_file, '-I', 'testID', '-V', - '-1d:+365d'] - if hostname is None: - args.extend(['-n', principals, pub_file]) - else: - args.extend(['-h', pub_file]) - subprocess.check_output(args, stderr=subprocess.STDOUT) - # Read the contents of the certificate file + # Temporarily write the authority private key and entity public key to files + prefix = uuid.uuid4().hex + # Todo: make the temporary directory configurable or secure it. + dir = '/tmp/sshaas' + ca_file = ''.join([dir, prefix]) + pub_file = ''.join([dir, prefix, '.pub']) + cert_file = ''.join([dir, prefix, '-cert.pub']) cert = '' - with open(cert_file, 'r') as text_file: - cert = text_file.read() - except Exception as e: - print e - finally: - # Delete temporary files - for file in [ca_file, pub_file, cert_file]: - try: - os.remove(file) - pass - except: - pass - return cert + try: + fd = os.open(ca_file, os.O_WRONLY | os.O_CREAT, 0o600) + os.close(fd) + with open(ca_file, "w") as text_file: + text_file.write(auth_key) + with open(pub_file, "w", 0o644) as text_file: + text_file.write(entity_key) + args = ['ssh-keygen', '-s', ca_file, '-I', 'testID', '-V', + '-1d:+365d'] + if hostname is None: + args.extend(['-n', principals, pub_file]) + else: + args.extend(['-h', pub_file]) + subprocess.check_output(args, stderr=subprocess.STDOUT) + # Read the contents of the certificate file + cert = '' + with open(cert_file, 'r') as text_file: + cert = text_file.read() + except Exception as e: + print e + finally: + # Delete temporary files + for file in [ca_file, pub_file, cert_file]: + try: + os.remove(file) + pass + except: + pass + return cert