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

View File

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

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

View File

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

View File

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

View File

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

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
# 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 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())

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

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

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

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
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:
"""

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

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_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())

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

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