4 space indentation

This commit is contained in:
Pino de Candia 2017-12-08 15:04:44 -06:00
parent 95c0f1011c
commit 7812e1e8b6
16 changed files with 941 additions and 676 deletions

View File

@ -1,4 +1,15 @@
#!/usr/bin/env python #!/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 json
import requests import requests

View File

@ -1,4 +1,16 @@
#!/usr/bin/env python #!/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 sys
import json import json
import yaml import yaml

View File

@ -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 json
import requests import requests
import os import os

View File

@ -1,4 +1,16 @@
#!/usr/bin/env python #!/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 argparse
import json import json
import requests import requests

View File

@ -1,4 +1,16 @@
#!/usr/bin/env python #!/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 argparse
import json import json
import os import os

View File

@ -1,4 +1,16 @@
#!/usr/bin/env python #!/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 sys
import json import json
import yaml import yaml

View File

@ -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 import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break # In python < 2.7.4, a lazy loading of package `pbr` will break

View File

@ -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 falcon
import models import models
import os.path import os.path
@ -11,6 +23,7 @@ CONF = cfg.CONF
if os.path.isfile(fname): if os.path.isfile(fname):
CONF(default_config_files=[fname]) CONF(default_config_files=[fname])
def create_app(sa): def create_app(sa):
api = falcon.API(middleware=[models.Logger(), sa]) api = falcon.API(middleware=[models.Logger(), sa])
api.add_route('/authorities', models.Authorities()) api.add_route('/authorities', models.Authorities())
@ -23,8 +36,10 @@ def create_app(sa):
api.add_route('/novavendordata', models.NovaVendorData()) api.add_route('/novavendordata', models.NovaVendorData())
return api return api
def get_app(): def get_app():
return create_app(SQLAlchemySessionManager()) return create_app(SQLAlchemySessionManager())
def main(global_config, **settings): def main(global_config, **settings):
return create_app(SQLAlchemySessionManager()) return create_app(SQLAlchemySessionManager())

View File

@ -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 falcon
import json import json
import logging import logging
@ -5,203 +17,208 @@ import uuid
from tatu.db import models as db from tatu.db import models as db
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
def validate_uuid(map, key): def validate_uuid(map, key):
try: try:
# Verify it's a valid UUID, then convert to canonical string representation # Verify it's a valid UUID, then convert to canonical string representation
# to avoiid DB errors. # to avoiid DB errors.
map[key] = str(uuid.UUID(map[key], version=4)) map[key] = str(uuid.UUID(map[key], version=4))
except ValueError: except ValueError:
msg = '{} is not a valid UUID'.format(map[key]) msg = '{} is not a valid UUID'.format(map[key])
raise falcon.HTTPBadRequest('Bad request', msg) raise falcon.HTTPBadRequest('Bad request', msg)
def validate_uuids(req, params): def validate_uuids(req, params):
id_keys = ['token_id', 'auth_id', 'host_id', 'user_id', 'project-id', 'instance-id'] id_keys = ['token_id', 'auth_id', 'host_id', 'user_id', 'project-id', 'instance-id']
if req.method in ('POST', 'PUT'): 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: for key in id_keys:
if key in req.body: if key in params:
validate_uuid(req.body, key) validate_uuid(params, key)
for key in id_keys:
if key in params:
validate_uuid(params, key)
def validate(req, resp, resource, params): def validate(req, resp, resource, params):
if req.content_length: if req.content_length:
# Store the body since we cannot read the stream again later # Store the body since we cannot read the stream again later
req.body = json.load(req.stream) req.body = json.load(req.stream)
elif req.method in ('POST', 'PUT'): elif req.method in ('POST', 'PUT'):
raise falcon.HTTPBadRequest('The POST/PUT request is missing a body.') raise falcon.HTTPBadRequest('The POST/PUT request is missing a body.')
validate_uuids(req, params) validate_uuids(req, params)
class Logger(object): class Logger(object):
def __init__(self): def __init__(self):
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
def process_resource(self, req, resp, resource, params): 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)) 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): 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): 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): 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): 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): def hostToJson(host):
return json.dumps({ return json.dumps({
'host_id': host.host_id, 'host_id': host.host_id,
'fingerprint': host.fingerprint, 'fingerprint': host.fingerprint,
'auth_id': host.auth_id, 'auth_id': host.auth_id,
'key-cert.pub': host.cert, 'key-cert.pub': host.cert,
}) })
class HostCerts(object): 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): 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): 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): class NovaVendorData(object):
@falcon.before(validate)
@falcon.before(validate) def on_post(self, req, resp):
def on_post(self, req, resp): # An example of the data nova sends to vendordata services:
# An example of the data nova sends to vendordata services: # {
# { # "hostname": "foo",
# "hostname": "foo", # "image-id": "75a74383-f276-4774-8074-8c4e3ff2ca64",
# "image-id": "75a74383-f276-4774-8074-8c4e3ff2ca64", # "instance-id": "2ae914e9-f5ab-44ce-b2a2-dcf8373d899d",
# "instance-id": "2ae914e9-f5ab-44ce-b2a2-dcf8373d899d", # "metadata": {},
# "metadata": {}, # "project-id": "039d104b7a5c4631b4ba6524d0b9e981",
# "project-id": "039d104b7a5c4631b4ba6524d0b9e981", # "user-data": null
# "user-data": null # }
# } try:
try: token = db.createToken(
token = db.createToken( self.session,
self.session, req.body['instance-id'],
req.body['instance-id'], req.body['project-id'],
req.body['project-id'], req.body['hostname']
req.body['hostname'] )
) except KeyError as e:
except KeyError as e: raise falcon.HTTPBadRequest(str(e))
raise falcon.HTTPBadRequest(str(e)) auth = db.getAuthority(self.session, req.body['project-id'])
auth = db.getAuthority(self.session, req.body['project-id']) if auth is None:
if auth is None: resp.status = falcon.HTTP_NOT_FOUND
resp.status = falcon.HTTP_NOT_FOUND return
return key = RSA.importKey(auth.user_key)
key = RSA.importKey(auth.user_key) pub_key = key.publickey().exportKey('OpenSSH')
pub_key = key.publickey().exportKey('OpenSSH') vendordata = {
vendordata = { 'token': token.token_id,
'token': token.token_id, 'auth_pub_key_user': pub_key,
'auth_pub_key_user': pub_key, 'principals': 'admin'
'principals': 'admin' }
} resp.body = json.dumps(vendordata)
resp.body = json.dumps(vendordata) resp.location = '/hosttokens/' + token.token_id
resp.location = '/hosttokens/' + token.token_id resp.status = falcon.HTTP_201
resp.status = falcon.HTTP_201

View File

@ -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.objects.passphrase import Passphrase
from castellan.common.utils import credential_factory from castellan.common.utils import credential_factory
from castellan.key_manager import API from castellan.key_manager import API
@ -16,6 +28,7 @@ CONF.register_opts(opts, group='tatu')
_context = None _context = None
_api = None _api = None
def validate_config(): def validate_config():
if CONF.tatu.use_barbican_key_manager: if CONF.tatu.use_barbican_key_manager:
set_castellan_defaults(CONF) set_castellan_defaults(CONF)
@ -23,17 +36,20 @@ def validate_config():
set_castellan_defaults(CONF, set_castellan_defaults(CONF,
api_class='tatu.castellano.TatuKeyManager') api_class='tatu.castellano.TatuKeyManager')
def context(): def context():
global _context global _context
if _context is None and CONF.tatu.use_barbican_key_manager: if _context is None and CONF.tatu.use_barbican_key_manager:
_context = credential_factory(conf=CONF) _context = credential_factory(conf=CONF)
return _context return _context
def api(): def api():
global _api global _api
if _api is None: if _api is None:
_api = API() _api = API()
return _api return _api
def delete_secret(id, ctx=None): def delete_secret(id, ctx=None):
"""delete a secret from the external key manager """delete a secret from the external key manager
@ -43,6 +59,7 @@ def delete_secret(id, ctx=None):
""" """
api().delete(ctx or context(), id) api().delete(ctx or context(), id)
def get_secret(id, ctx=None): def get_secret(id, ctx=None):
"""get a secret associated with an id """get a secret associated with an id
:param id: The identifier of the secret to retrieve :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) key = api().get(ctx or context(), id)
return key.get_encoded() return key.get_encoded()
def store_secret(secret, ctx=None): def store_secret(secret, ctx=None):
"""store a secret and return its identifier """store a secret and return its identifier
:param secret: The secret to store, this should be a string :param secret: The secret to store, this should be a string
@ -61,10 +79,13 @@ def store_secret(secret, ctx=None):
key = Passphrase(secret) key = Passphrase(secret)
return api().store(ctx or context(), key) return api().store(ctx or context(), key)
""" """
This module contains the KeyManager class that will be used by the This module contains the KeyManager class that will be used by the
castellan library, it is not meant for direct usage within tatu. castellan library, it is not meant for direct usage within tatu.
""" """
class TatuKeyManager(KeyManager): class TatuKeyManager(KeyManager):
"""Tatu specific key manager """Tatu specific key manager
This manager is a thin wrapper around the secret being stored. It is 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 This behavior allows Tatu to continue storing secrets in its database
while using the Castellan key manager abstraction. while using the Castellan key manager abstraction.
""" """
def __init__(self, configuration=None): def __init__(self, configuration=None):
pass pass

View File

@ -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 from datetime import datetime
import falcon import falcon
import os import os
@ -6,100 +18,110 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
import sshpubkeys import sshpubkeys
from tatu.castellano import get_secret, store_secret from tatu.castellano import get_secret, store_secret
from tatu.utils import generateCert,random_uuid from tatu.utils import generateCert, random_uuid
import uuid import uuid
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
Base = declarative_base() Base = declarative_base()
class Authority(Base):
__tablename__ = 'authorities'
auth_id = sa.Column(sa.String(36), primary_key=True) class Authority(Base):
user_key = sa.Column(sa.Text) __tablename__ = 'authorities'
host_key = sa.Column(sa.Text)
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): def getAuthority(session, auth_id):
return session.query(Authority).get(auth_id) return session.query(Authority).get(auth_id)
def getAuthUserKey(auth): def getAuthUserKey(auth):
return get_secret(auth.user_key) return get_secret(auth.user_key)
def getAuthHostKey(auth): def getAuthHostKey(auth):
return get_secret(auth.host_key) return get_secret(auth.host_key)
def createAuthority(session, auth_id): def createAuthority(session, auth_id):
user_key = RSA.generate(2048).exportKey('PEM') user_key = RSA.generate(2048).exportKey('PEM')
user_secret_id = store_secret(user_key) user_secret_id = store_secret(user_key)
host_key = RSA.generate(2048).exportKey('PEM') host_key = RSA.generate(2048).exportKey('PEM')
host_secret_id = store_secret(host_key) host_secret_id = store_secret(host_key)
auth = Authority(auth_id=auth_id, auth = Authority(auth_id=auth_id,
user_key=user_secret_id, user_key=user_secret_id,
host_key=host_secret_id) host_key=host_secret_id)
session.add(auth) session.add(auth)
try: try:
session.commit() session.commit()
except IntegrityError: except IntegrityError:
raise falcon.HTTPConflict("This certificate authority already exists.") raise falcon.HTTPConflict("This certificate authority already exists.")
return auth return auth
class UserCert(Base): 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): 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): def createUserCert(session, user_id, auth_id, pub):
# Retrieve the authority's private key and generate the certificate # Retrieve the authority's private key and generate the certificate
auth = getAuthority(session, auth_id) auth = getAuthority(session, auth_id)
if auth is None: 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() fingerprint = sshpubkeys.SSHKey(pub).hash_md5()
certRecord = session.query(UserCert).get([user_id, fingerprint]) certRecord = session.query(UserCert).get([user_id, fingerprint])
if certRecord is not None: if certRecord is not None:
return certRecord return certRecord
cert = generateCert(get_secret(auth.user_key), pub, principals='admin,root') cert = generateCert(get_secret(auth.user_key), pub, principals='admin,root')
if cert is None: if cert is None:
raise falcon.HTTPInternalServerError("Failed to generate the certificate") raise falcon.HTTPInternalServerError("Failed to generate the certificate")
user = UserCert( user = UserCert(
user_id=user_id, user_id=user_id,
fingerprint=fingerprint, fingerprint=fingerprint,
auth_id=auth_id, auth_id=auth_id,
cert=cert cert=cert
) )
session.add(user) session.add(user)
session.commit() session.commit()
return user return user
class Token(Base):
__tablename__ = 'tokens'
token_id = sa.Column(sa.String(36), primary_key=True, class Token(Base):
default=random_uuid) __tablename__ = 'tokens'
auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id'))
host_id = sa.Column(sa.String(36), index=True, unique=True) token_id = sa.Column(sa.String(36), primary_key=True,
hostname = sa.Column(sa.String(36)) default=random_uuid)
used = sa.Column(sa.Boolean, default=False) auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id'))
date_used = sa.Column(sa.DateTime, default=datetime.min) host_id = sa.Column(sa.String(36), index=True, unique=True)
fingerprint_used = sa.Column(sa.String(36)) 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): def createToken(session, host_id, auth_id, hostname):
# Validate the certificate authority # Validate the certificate authority
auth = getAuthority(session, auth_id) auth = getAuthority(session, auth_id)
if auth is None: if auth is None:
raise falcon.HTTPNotFound(description='No Authority found with that ID') raise falcon.HTTPNotFound(description='No Authority found with that ID')
#Check whether a token was already created for this host_id # Check whether a token was already created for this host_id
try: try:
token = session.query(Token).filter(Token.host_id == host_id).one() token = session.query(Token).filter(Token.host_id == host_id).one()
if token is not None: if token is not None:
return token return token
except: except:
pass pass
token = Token(host_id=host_id, token = Token(host_id=host_id,
auth_id=auth_id, auth_id=auth_id,
@ -108,54 +130,57 @@ def createToken(session, host_id, auth_id, hostname):
session.commit() session.commit()
return token return token
class HostCert(Base):
__tablename__ = 'host_certs'
host_id = sa.Column(sa.String(36), primary_key=True) class HostCert(Base):
fingerprint = sa.Column(sa.String(36), primary_key=True) __tablename__ = 'host_certs'
auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id'))
token_id = sa.Column(sa.String(36), sa.ForeignKey('tokens.token_id')) host_id = sa.Column(sa.String(36), primary_key=True)
pubkey = sa.Column(sa.Text) fingerprint = sa.Column(sa.String(36), primary_key=True)
cert = sa.Column(sa.Text) auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id'))
hostname = sa.Column(sa.String(36)) 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): 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): 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: 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: 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() fingerprint = sshpubkeys.SSHKey(pub).hash_md5()
if token.used: if token.used:
if token.fingerprint_used != fingerprint: if token.fingerprint_used != fingerprint:
raise falcon.HTTPConflict(description='The token was previously used with a different public key') 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. # The token was already used for same host and pub key. Return record.
host = session.query(HostCert).get([host_id, fingerprint]) host = session.query(HostCert).get([host_id, fingerprint])
if host is None: if host is None:
raise falcon.HTTPInternalServerError( raise falcon.HTTPInternalServerError(
description='The token was used, but no corresponding Host record was found.') description='The token was used, but no corresponding Host record was found.')
if host.token_id == token_id: if host.token_id == token_id:
return host return host
raise falcon.HTTPConflict(description='The presented token was previously used') raise falcon.HTTPConflict(description='The presented token was previously used')
auth = getAuthority(session, token.auth_id) auth = getAuthority(session, token.auth_id)
if auth is None: 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]) certRecord = session.query(HostCert).get([host_id, fingerprint])
if certRecord is not None: 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) cert = generateCert(get_secret(auth.host_key), pub, hostname=token.hostname)
if cert == '': if cert == '':
raise falcon.HTTPInternalServerError("Failed to generate the certificate") raise falcon.HTTPInternalServerError("Failed to generate the certificate")
host = HostCert(host_id=host_id, host = HostCert(host_id=host_id,
fingerprint=fingerprint, fingerprint=fingerprint,
auth_id=token.auth_id, auth_id=token.auth_id,
token_id=token_id, token_id=token_id,
pubkey = pub, pubkey=pub,
cert=cert, cert=cert,
hostname=token.hostname) hostname=token.hostname)
session.add(host) session.add(host)

View File

@ -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 import os
from sqlalchemy import create_engine from sqlalchemy import create_engine
@ -7,7 +19,8 @@ from tatu.db.models import Base
def get_url(): def get_url():
return os.getenv("DATABASE_URL", "sqlite:///development.db") return os.getenv("DATABASE_URL", "sqlite:///development.db")
#return os.getenv("DATABASE_URL", "sqlite:///:memory:") # return os.getenv("DATABASE_URL", "sqlite:///:memory:")
class SQLAlchemySessionManager: class SQLAlchemySessionManager:
""" """

View File

@ -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 json
import requests import requests
import os import os
@ -8,90 +20,93 @@ import uuid
server = 'http://172.24.4.1:18322' server = 'http://172.24.4.1:18322'
def vendordata_request(instance_id, project_id, hostname): def vendordata_request(instance_id, project_id, hostname):
return { return {
'instance-id': instance_id, 'instance-id': instance_id,
'project-id': project_id, 'project-id': project_id,
'hostname': hostname 'hostname': hostname
} }
def host_request(token, host, pub_key): def host_request(token, host, pub_key):
return { return {
'token_id': token, 'token_id': token,
'host_id': host, 'host_id': host,
'key.pub': pub_key 'key.pub': pub_key
} }
def test_host_certificate_generation(): def test_host_certificate_generation():
project_id = random_uuid() 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'
response = requests.post( response = requests.post(
server + '/noauth/hostcerts', server + '/authorities',
data=json.dumps(host_request(token, instance_id, pub_key)) data=json.dumps({'auth_id': project_id})
) )
assert response.status_code == 201 assert response.status_code == 201
assert 'location' in response.headers assert 'location' in response.headers
location = response.headers['location'] assert response.headers['location'] == '/authorities/' + project_id
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) response = requests.get(server + response.headers['location'])
assert response.status_code == 200 assert response.status_code == 200
hostcert = json.loads(response.content) auth = json.loads(response.content)
assert 'host_id' in hostcert assert 'auth_id' in auth
assert hostcert['host_id'] == instance_id assert auth['auth_id'] == project_id
assert 'fingerprint' in hostcert assert 'user_key.pub' in auth
assert hostcert['fingerprint'] assert 'host_key.pub' in auth
assert 'auth_id' in hostcert ca_user = auth['user_key.pub']
auth_id = str(uuid.UUID(hostcert['auth_id'], version=4))
assert auth_id == project_id key = RSA.generate(2048)
assert 'key-cert.pub' in hostcert 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

View File

@ -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_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
import oslo_messaging import oslo_messaging
@ -14,15 +26,15 @@ LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
DOMAIN = 'tatu' DOMAIN = 'tatu'
class NotificationEndpoint(object):
class NotificationEndpoint(object):
filter_rule = oslo_messaging.NotificationFilter( filter_rule = oslo_messaging.NotificationFilter(
publisher_id='^identity.*', publisher_id='^identity.*',
event_type='^identity.project.created') event_type='^identity.project.created')
def __init__(self): def __init__(self):
self.engine = create_engine(get_url()) 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)) self.Session = scoped_session(sessionmaker(self.engine))
def info(self, ctxt, publisher_id, event_type, payload, metadata): def info(self, ctxt, publisher_id, event_type, payload, metadata):
@ -46,12 +58,13 @@ class NotificationEndpoint(object):
else: else:
LOG.error("Status update or unknown") LOG.error("Status update or unknown")
def main(): def main():
logging.register_options(CONF) logging.register_options(CONF)
extra_log_level_defaults = ['tatu=DEBUG', '__main__=DEBUG'] extra_log_level_defaults = ['tatu=DEBUG', '__main__=DEBUG']
logging.set_defaults( logging.set_defaults(
default_log_levels=logging.get_default_log_levels() + default_log_levels=logging.get_default_log_levels() +
extra_log_level_defaults) extra_log_level_defaults)
logging.setup(CONF, DOMAIN) logging.setup(CONF, DOMAIN)
transport = oslo_messaging.get_notification_transport(CONF) transport = oslo_messaging.get_notification_transport(CONF)
@ -74,5 +87,6 @@ def main():
server.stop() server.stop()
server.wait() server.wait()
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@ -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 json
import falcon import falcon
from falcon import testing from falcon import testing
@ -12,14 +22,17 @@ from Crypto.PublicKey import RSA
import sshpubkeys import sshpubkeys
import time import time
@pytest.fixture @pytest.fixture
def db(): def db():
return SQLAlchemySessionManager() return SQLAlchemySessionManager()
@pytest.fixture @pytest.fixture
def client(db): def client(db):
api = create_app(db) api = create_app(db)
return testing.TestClient(api) return testing.TestClient(api)
token_id = '' token_id = ''
@ -36,404 +49,438 @@ user_fingerprint = sshpubkeys.SSHKey(user_pub_key).hash_md5()
auth_id = random_uuid() auth_id = random_uuid()
auth_user_pub_key = None auth_user_pub_key = None
@pytest.mark.dependency() @pytest.mark.dependency()
def test_post_authority(client, auth_id=auth_id): def test_post_authority(client, auth_id=auth_id):
body = { body = {
'auth_id': auth_id, 'auth_id': auth_id,
} }
response = client.simulate_post( response = client.simulate_post(
'/authorities', '/authorities',
body=json.dumps(body) body=json.dumps(body)
) )
assert response.status == falcon.HTTP_CREATED assert response.status == falcon.HTTP_CREATED
assert response.headers['location'] == '/authorities/' + auth_id assert response.headers['location'] == '/authorities/' + auth_id
@pytest.mark.dependency(depends=['test_post_authority']) @pytest.mark.dependency(depends=['test_post_authority'])
def test_post_authority_duplicate(client): def test_post_authority_duplicate(client):
body = { body = {
'auth_id': auth_id, 'auth_id': auth_id,
} }
response = client.simulate_post( response = client.simulate_post(
'/authorities', '/authorities',
body=json.dumps(body) body=json.dumps(body)
) )
assert response.status == falcon.HTTP_CONFLICT assert response.status == falcon.HTTP_CONFLICT
def test_post_no_body(client): def test_post_no_body(client):
for path in ['/authorities', '/usercerts', '/hosttokens', for path in ['/authorities', '/usercerts', '/hosttokens',
'/hostcerts', '/novavendordata']: '/hostcerts', '/novavendordata']:
response = client.simulate_post(path) response = client.simulate_post(path)
assert response.status == falcon.HTTP_BAD_REQUEST assert response.status == falcon.HTTP_BAD_REQUEST
def test_post_empty_body(client): def test_post_empty_body(client):
bodystr = json.dumps({}) bodystr = json.dumps({})
for path in ['/authorities', '/usercerts', '/hosttokens', for path in ['/authorities', '/usercerts', '/hosttokens',
'/hostcerts', '/novavendordata']: '/hostcerts', '/novavendordata']:
response = client.simulate_post(path, body=bodystr) response = client.simulate_post(path, body=bodystr)
assert response.status == falcon.HTTP_BAD_REQUEST assert response.status == falcon.HTTP_BAD_REQUEST
def test_post_authority_bad_uuid(client): def test_post_authority_bad_uuid(client):
body = { body = {
'auth_id': 'foobar', 'auth_id': 'foobar',
} }
response = client.simulate_post( response = client.simulate_post(
'/authorities', '/authorities',
body=json.dumps(body) body=json.dumps(body)
) )
assert response.status == falcon.HTTP_BAD_REQUEST assert response.status == falcon.HTTP_BAD_REQUEST
@pytest.mark.dependency(depends=['test_post_authority']) @pytest.mark.dependency(depends=['test_post_authority'])
def test_get_authority(client): def test_get_authority(client):
response = client.simulate_get('/authorities/' + auth_id) response = client.simulate_get('/authorities/' + auth_id)
assert response.status == falcon.HTTP_OK assert response.status == falcon.HTTP_OK
body = json.loads(response.content) body = json.loads(response.content)
assert 'auth_id' in body assert 'auth_id' in body
assert 'user_key.pub' in body assert 'user_key.pub' in body
global auth_user_pub_key global auth_user_pub_key
auth_user_pub_key = body['user_key.pub'] auth_user_pub_key = body['user_key.pub']
assert 'host_key.pub' in body assert 'host_key.pub' in body
assert 'user_key' not in body assert 'user_key' not in body
assert 'host_key' not in body assert 'host_key' not in body
def test_get_authority_doesnt_exist(client): def test_get_authority_doesnt_exist(client):
response = client.simulate_get('/authorities/' + random_uuid()) response = client.simulate_get('/authorities/' + random_uuid())
assert response.status == falcon.HTTP_NOT_FOUND assert response.status == falcon.HTTP_NOT_FOUND
def test_get_authority_with_bad_uuid(client): def test_get_authority_with_bad_uuid(client):
response = client.simulate_get('/authorities/foobar') response = client.simulate_get('/authorities/foobar')
assert response.status == falcon.HTTP_BAD_REQUEST assert response.status == falcon.HTTP_BAD_REQUEST
def user_request(auth=auth_id, user_id=user_id, pub_key=user_pub_key): def user_request(auth=auth_id, user_id=user_id, pub_key=user_pub_key):
return { return {
'user_id': user_id, 'user_id': user_id,
'auth_id': auth, 'auth_id': auth,
'key.pub': pub_key 'key.pub': pub_key
} }
def test_post_user_bad_uuid(client): def test_post_user_bad_uuid(client):
for key in ['user_id', 'auth_id']: for key in ['user_id', 'auth_id']:
body = user_request() body = user_request()
body[key] = 'foobar' body[key] = 'foobar'
response = client.simulate_post( response = client.simulate_post(
'/usercerts', '/usercerts',
body=json.dumps(body) body=json.dumps(body)
) )
assert response.status == falcon.HTTP_BAD_REQUEST assert response.status == falcon.HTTP_BAD_REQUEST
@pytest.mark.dependency(depends=['test_post_authority']) @pytest.mark.dependency(depends=['test_post_authority'])
def test_post_user(client): def test_post_user(client):
body = user_request() body = user_request()
response = client.simulate_post( response = client.simulate_post(
'/usercerts', '/usercerts',
body=json.dumps(body) body=json.dumps(body)
) )
assert response.status == falcon.HTTP_CREATED assert response.status == falcon.HTTP_CREATED
assert 'location' in response.headers assert 'location' in response.headers
location = response.headers['location'].split('/') location = response.headers['location'].split('/')
assert location[1] == 'usercerts' assert location[1] == 'usercerts'
assert location[2] == body['user_id'] assert location[2] == body['user_id']
assert location[3] == sshpubkeys.SSHKey(body['key.pub']).hash_md5() assert location[3] == sshpubkeys.SSHKey(body['key.pub']).hash_md5()
@pytest.mark.dependency(depends=['test_post_user']) @pytest.mark.dependency(depends=['test_post_user'])
def test_get_user(client): def test_get_user(client):
response = client.simulate_get('/usercerts/' + user_id + '/' + user_fingerprint) response = client.simulate_get('/usercerts/' + user_id + '/' + user_fingerprint)
assert response.status == falcon.HTTP_OK assert response.status == falcon.HTTP_OK
body = json.loads(response.content) body = json.loads(response.content)
assert 'user_id' in body assert 'user_id' in body
assert 'fingerprint' in body assert 'fingerprint' in body
assert 'auth_id' in body assert 'auth_id' in body
assert 'key-cert.pub' in body assert 'key-cert.pub' in body
assert body['auth_id'] == auth_id assert body['auth_id'] == auth_id
def test_get_user_doesnt_exist(client): def test_get_user_doesnt_exist(client):
response = client.simulate_get('/usercerts/' + random_uuid() + '/' + user_fingerprint) response = client.simulate_get('/usercerts/' + random_uuid() + '/' + user_fingerprint)
assert response.status == falcon.HTTP_NOT_FOUND assert response.status == falcon.HTTP_NOT_FOUND
def test_get_user_with_bad_uuid(client): def test_get_user_with_bad_uuid(client):
response = client.simulate_get('/usercerts/foobar/' + user_fingerprint) response = client.simulate_get('/usercerts/foobar/' + user_fingerprint)
assert response.status == falcon.HTTP_BAD_REQUEST assert response.status == falcon.HTTP_BAD_REQUEST
@pytest.mark.dependency(depends=['test_post_user']) @pytest.mark.dependency(depends=['test_post_user'])
def test_post_second_cert_same_user(client): def test_post_second_cert_same_user(client):
key = RSA.generate(2048) key = RSA.generate(2048)
pub_key = key.publickey().exportKey('OpenSSH') pub_key = key.publickey().exportKey('OpenSSH')
body = user_request(pub_key=pub_key) body = user_request(pub_key=pub_key)
response = client.simulate_post( response = client.simulate_post(
'/usercerts', '/usercerts',
body=json.dumps(body) body=json.dumps(body)
) )
assert response.status == falcon.HTTP_CREATED assert response.status == falcon.HTTP_CREATED
assert 'location' in response.headers assert 'location' in response.headers
location = response.headers['location'].split('/') location = response.headers['location'].split('/')
assert location[1] == 'usercerts' assert location[1] == 'usercerts'
assert location[2] == user_id assert location[2] == user_id
assert location[3] == sshpubkeys.SSHKey(pub_key).hash_md5() assert location[3] == sshpubkeys.SSHKey(pub_key).hash_md5()
def test_post_user_unknown_auth(client): def test_post_user_unknown_auth(client):
body = user_request(auth=random_uuid()) body = user_request(auth=random_uuid())
response = client.simulate_post( response = client.simulate_post(
'/usercerts', '/usercerts',
body=json.dumps(body) body=json.dumps(body)
) )
assert response.status == falcon.HTTP_NOT_FOUND assert response.status == falcon.HTTP_NOT_FOUND
@pytest.mark.dependency(depends=['test_post_user']) @pytest.mark.dependency(depends=['test_post_user'])
def test_post_same_user_and_key_returns_same_result(client): 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): def token_request(auth=auth_id, host=host_id):
return { return {
'host_id': host, 'host_id': host,
'auth_id': auth, 'auth_id': auth,
'hostname': 'testname.local' 'hostname': 'testname.local'
} }
def host_request(token, host=host_id, pub_key=host_pub_key): def host_request(token, host=host_id, pub_key=host_pub_key):
return { return {
'token_id': token, 'token_id': token,
'host_id': host, 'host_id': host,
'key.pub': pub_key 'key.pub': pub_key
} }
def vendordata_request(auth, host): def vendordata_request(auth, host):
return { return {
'instance-id': host, 'instance-id': host,
'project-id': auth, 'project-id': auth,
'hostname': 'mytest.testing' 'hostname': 'mytest.testing'
} }
def test_post_vendordata_bad_uuid(client): def test_post_vendordata_bad_uuid(client):
for key in ['instance-id', 'project-id']: for key in ['instance-id', 'project-id']:
body = vendordata_request(auth_id, host_id) body = vendordata_request(auth_id, host_id)
body[key] = 'foobar' body[key] = 'foobar'
response = client.simulate_post( response = client.simulate_post(
'/novavendordata', '/novavendordata',
body=json.dumps(body) body=json.dumps(body)
) )
assert response.status == falcon.HTTP_BAD_REQUEST assert response.status == falcon.HTTP_BAD_REQUEST
@pytest.mark.dependency(depends=['test_post_authority']) @pytest.mark.dependency(depends=['test_post_authority'])
def test_post_novavendordata(client): def test_post_novavendordata(client):
req = vendordata_request(auth_id, random_uuid()) 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'
response = client.simulate_post( response = client.simulate_post(
'/hosttokens', '/novavendordata',
body=json.dumps(body) body=json.dumps(req)
)
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)
) )
assert response.status == falcon.HTTP_CREATED assert response.status == falcon.HTTP_CREATED
assert 'location' in response.headers assert 'location' in response.headers
location_path = response.headers['location'].split('/') location_path = response.headers['location'].split('/')
assert location_path[1] == 'hosttokens' 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] token_id = location_path[-1]
# Verify that it's a valid UUID # Verify that it's a valid UUID
uuid.UUID(token_id, version=4) 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( response = client.simulate_post(
'/hostcerts', '/hostcerts',
body=json.dumps(host) body=json.dumps(host)
) )
assert response.status == falcon.HTTP_CREATED assert response.status == falcon.HTTP_CREATED
assert 'location' in response.headers assert 'location' in response.headers
location = response.headers['location'].split('/') location = response.headers['location'].split('/')
assert location[1] == 'hostcerts' assert location[1] == 'hostcerts'
assert location[2] == hid assert location[2] == host_id
assert location[3] == fingerprint assert location[3] == host_fingerprint
assert time.time() - start < 5 # 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']) @pytest.mark.dependency(depends=['test_post_token_and_host'])
def test_post_token_same_host_id(client): def test_post_token_same_host_id(client):
# Posting with the same host ID should return the same token # Posting with the same host ID should return the same token
token = token_request() token = token_request()
response = client.simulate_post( response = client.simulate_post(
'/hosttokens', '/hosttokens',
body=json.dumps(token) body=json.dumps(token)
) )
assert response.status == falcon.HTTP_CREATED assert response.status == falcon.HTTP_CREATED
assert 'location' in response.headers assert 'location' in response.headers
location_path = response.headers['location'].split('/') location_path = response.headers['location'].split('/')
assert location_path[1] == 'hosttokens' assert location_path[1] == 'hosttokens'
# The token id should be the same as that from the previous test. # The token id should be the same as that from the previous test.
assert token_id == location_path[-1] assert token_id == location_path[-1]
@pytest.mark.dependency(depends=['test_post_token_and_host']) @pytest.mark.dependency(depends=['test_post_token_and_host'])
def test_get_host(client): def test_get_host(client):
response = client.simulate_get('/hostcerts/' + host_id + '/' + host_fingerprint) response = client.simulate_get('/hostcerts/' + host_id + '/' + host_fingerprint)
assert response.status == falcon.HTTP_OK assert response.status == falcon.HTTP_OK
body = json.loads(response.content) body = json.loads(response.content)
assert 'host_id' in body assert 'host_id' in body
assert 'fingerprint' in body assert 'fingerprint' in body
assert 'auth_id' in body assert 'auth_id' in body
assert 'key-cert.pub' in body assert 'key-cert.pub' in body
assert body['host_id'] == host_id assert body['host_id'] == host_id
assert body['fingerprint'] == host_fingerprint assert body['fingerprint'] == host_fingerprint
assert body['auth_id'] == auth_id assert body['auth_id'] == auth_id
def test_get_host_doesnt_exist(client): def test_get_host_doesnt_exist(client):
response = client.simulate_get('/hostcerts/' + random_uuid() + '/' + host_fingerprint) response = client.simulate_get('/hostcerts/' + random_uuid() + '/' + host_fingerprint)
assert response.status == falcon.HTTP_NOT_FOUND assert response.status == falcon.HTTP_NOT_FOUND
def test_get_host_with_bad_uuid(client): def test_get_host_with_bad_uuid(client):
response = client.simulate_get('/hostcerts/foobar/' + host_fingerprint) response = client.simulate_get('/hostcerts/foobar/' + host_fingerprint)
assert response.status == falcon.HTTP_BAD_REQUEST assert response.status == falcon.HTTP_BAD_REQUEST
def test_post_token_unknown_auth(client): def test_post_token_unknown_auth(client):
token = token_request(auth=random_uuid()) token = token_request(auth=random_uuid())
response = client.simulate_post( response = client.simulate_post(
'/hosttokens', '/hosttokens',
body=json.dumps(token) body=json.dumps(token)
) )
assert response.status == falcon.HTTP_NOT_FOUND assert response.status == falcon.HTTP_NOT_FOUND
@pytest.mark.dependency(depends=['test_post_authority']) @pytest.mark.dependency(depends=['test_post_authority'])
def test_post_host_with_bogus_token(client): def test_post_host_with_bogus_token(client):
host = host_request(random_uuid(), random_uuid()) host = host_request(random_uuid(), random_uuid())
response = client.simulate_post( response = client.simulate_post(
'/hostcerts', '/hostcerts',
body=json.dumps(host) body=json.dumps(host)
) )
assert response.status == falcon.HTTP_NOT_FOUND assert response.status == falcon.HTTP_NOT_FOUND
@pytest.mark.dependency(depends=['test_post_token_and_host']) @pytest.mark.dependency(depends=['test_post_token_and_host'])
def test_post_host_with_wrong_host_id(client): def test_post_host_with_wrong_host_id(client):
# Get a new token for the same host_id as the base test. # Get a new token for the same host_id as the base test.
token = token_request(host=random_uuid()) token = token_request(host=random_uuid())
response = client.simulate_post( response = client.simulate_post(
'/hosttokens', '/hosttokens',
body=json.dumps(token) body=json.dumps(token)
) )
assert response.status == falcon.HTTP_CREATED assert response.status == falcon.HTTP_CREATED
assert 'location' in response.headers assert 'location' in response.headers
location_path = response.headers['location'].split('/') location_path = response.headers['location'].split('/')
assert location_path[1] == 'hosttokens' assert location_path[1] == 'hosttokens'
# Use the token with a different host_id than it was created for. # Use the token with a different host_id than it was created for.
# Use a different public key to avoid other error conditions. # Use a different public key to avoid other error conditions.
key = RSA.generate(2048) key = RSA.generate(2048)
pub_key = key.publickey().exportKey('OpenSSH') pub_key = key.publickey().exportKey('OpenSSH')
host = host_request(location_path[-1], random_uuid(), pub_key) host = host_request(location_path[-1], random_uuid(), pub_key)
response = client.simulate_post( response = client.simulate_post(
'/hostcerts', '/hostcerts',
body=json.dumps(host) body=json.dumps(host)
) )
assert response.status == falcon.HTTP_CONFLICT assert response.status == falcon.HTTP_CONFLICT
@pytest.mark.dependency(depends=['test_post_token_and_host']) @pytest.mark.dependency(depends=['test_post_token_and_host'])
def test_post_host_different_public_key_fails(client): def test_post_host_different_public_key_fails(client):
# Use the same token compared to the test this depends on. # Use the same token compared to the test this depends on.
# Show that using the same host ID and different public key fails. # Show that using the same host ID and different public key fails.
token = token_request() token = token_request()
response = client.simulate_post( response = client.simulate_post(
'/hosttokens', '/hosttokens',
body=json.dumps(token) body=json.dumps(token)
) )
assert response.status == falcon.HTTP_CREATED assert response.status == falcon.HTTP_CREATED
assert 'location' in response.headers assert 'location' in response.headers
location_path = response.headers['location'].split('/') location_path = response.headers['location'].split('/')
assert location_path[1] == 'hosttokens' assert location_path[1] == 'hosttokens'
key = RSA.generate(2048) key = RSA.generate(2048)
pub_key = key.publickey().exportKey('OpenSSH') pub_key = key.publickey().exportKey('OpenSSH')
host = host_request(location_path[-1], pub_key=pub_key) host = host_request(location_path[-1], pub_key=pub_key)
response = client.simulate_post( response = client.simulate_post(
'/hostcerts', '/hostcerts',
body=json.dumps(host) body=json.dumps(host)
) )
assert response.status == falcon.HTTP_CONFLICT assert response.status == falcon.HTTP_CONFLICT
@pytest.mark.dependency(depends=['test_post_token_and_host']) @pytest.mark.dependency(depends=['test_post_token_and_host'])
def test_post_host_with_used_token(client): def test_post_host_with_used_token(client):
# Re-use the token from the test this depends on. # Re-use the token from the test this depends on.
# Use the same host_id and different public key to avoid other errors. # Use the same host_id and different public key to avoid other errors.
key = RSA.generate(2048) key = RSA.generate(2048)
pub_key = key.publickey().exportKey('OpenSSH') pub_key = key.publickey().exportKey('OpenSSH')
host = host_request(token_id, host_id, pub_key) host = host_request(token_id, host_id, pub_key)
response = client.simulate_post( response = client.simulate_post(
'/hostcerts', '/hostcerts',
body=json.dumps(host) body=json.dumps(host)
) )
assert response.status == falcon.HTTP_CONFLICT assert response.status == falcon.HTTP_CONFLICT

View File

@ -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 os
import subprocess import subprocess
import uuid import uuid
def random_uuid(): def random_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
def generateCert(auth_key, entity_key, hostname=None, principals='root'): def generateCert(auth_key, entity_key, hostname=None, principals='root'):
# Temporarily write the authority private key and entity public key to files # Temporarily write the authority private key and entity public key to files
prefix = uuid.uuid4().hex prefix = uuid.uuid4().hex
# Todo: make the temporary directory configurable or secure it. # Todo: make the temporary directory configurable or secure it.
dir = '/tmp/sshaas' dir = '/tmp/sshaas'
ca_file = ''.join([dir, prefix]) ca_file = ''.join([dir, prefix])
pub_file = ''.join([dir, prefix, '.pub']) pub_file = ''.join([dir, prefix, '.pub'])
cert_file = ''.join([dir, prefix, '-cert.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
cert = '' cert = ''
with open(cert_file, 'r') as text_file: try:
cert = text_file.read() fd = os.open(ca_file, os.O_WRONLY | os.O_CREAT, 0o600)
except Exception as e: os.close(fd)
print e with open(ca_file, "w") as text_file:
finally: text_file.write(auth_key)
# Delete temporary files with open(pub_file, "w", 0o644) as text_file:
for file in [ca_file, pub_file, cert_file]: text_file.write(entity_key)
try: args = ['ssh-keygen', '-s', ca_file, '-I', 'testID', '-V',
os.remove(file) '-1d:+365d']
pass if hostname is None:
except: args.extend(['-n', principals, pub_file])
pass else:
return cert 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