365 lines
12 KiB
Python
365 lines
12 KiB
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 falcon
|
|
import json
|
|
import logging
|
|
import uuid
|
|
from Crypto.PublicKey import RSA
|
|
from oslo_log import log as logging
|
|
|
|
from tatu.config import CONF
|
|
from tatu.db import models as db
|
|
from tatu.dns import add_srv_records
|
|
from tatu.ks_utils import getProjectRoleNames, getProjectNameForID, getUserNameForID
|
|
from tatu.utils import canonical_uuid_string, datetime_to_string
|
|
|
|
if CONF.tatu.use_pat_bastions:
|
|
from tatu.pat import create_pat_entries, getAllPats, ip_port_tuples_to_string
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def validate_uuid(map, key):
|
|
try:
|
|
# Verify UUID is valid, then convert to canonical string representation
|
|
# to avoiid DB errors.
|
|
map[key] = canonical_uuid_string(map[key])
|
|
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'):
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
class Logger(object):
|
|
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_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 _authAsDict(auth):
|
|
return {
|
|
'auth_id': auth.auth_id,
|
|
'name': auth.name,
|
|
'user_pub_key': auth.user_pub_key,
|
|
'host_pub_key': auth.host_pub_key,
|
|
}
|
|
|
|
class Authorities(object):
|
|
@falcon.before(validate)
|
|
def on_post(self, req, resp):
|
|
id = req.body['auth_id']
|
|
try:
|
|
db.createAuthority(
|
|
self.session,
|
|
id,
|
|
getProjectNameForID(id)
|
|
)
|
|
except KeyError as e:
|
|
raise falcon.HTTPBadRequest(str(e))
|
|
resp.status = falcon.HTTP_201
|
|
resp.location = '/authorities/{}'.format(id)
|
|
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp):
|
|
body = {'CAs': [_authAsDict(auth)
|
|
for auth in db.getAuthorities(self.session)]}
|
|
resp.body = json.dumps(body)
|
|
resp.status = falcon.HTTP_OK
|
|
|
|
|
|
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
|
|
resp.body = json.dumps(_authAsDict(auth))
|
|
resp.status = falcon.HTTP_OK
|
|
|
|
def _userCertAsDict(cert):
|
|
return {
|
|
'user_id': cert.user_id,
|
|
'user_name': cert.user_name,
|
|
'principals': cert.principals,
|
|
'fingerprint': cert.fingerprint,
|
|
'auth_id': cert.auth_id,
|
|
'cert': cert.cert.strip('\n'),
|
|
'revoked': cert.revoked,
|
|
'serial': cert.serial,
|
|
'created_at': datetime_to_string(cert.created_at),
|
|
'expires_at': datetime_to_string(cert.expires_at),
|
|
}
|
|
|
|
class UserCerts(object):
|
|
@falcon.before(validate)
|
|
def on_post(self, req, resp):
|
|
# TODO(pino): validation
|
|
id = req.body['user_id']
|
|
try:
|
|
user_cert = db.createUserCert(
|
|
self.session,
|
|
id,
|
|
getUserNameForID(id),
|
|
req.body['auth_id'],
|
|
req.body['pub_key']
|
|
)
|
|
except KeyError as e:
|
|
raise falcon.HTTPBadRequest(str(e))
|
|
resp.status = falcon.HTTP_201
|
|
resp.location = '/usercerts/{}/{}'.format(id, user_cert.fingerprint)
|
|
resp.body = json.dumps(_userCertAsDict(user_cert))
|
|
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp):
|
|
body = {'certs': [_userCertAsDict(cert)
|
|
for cert in db.getUserCerts(self.session)]}
|
|
resp.body = json.dumps(body)
|
|
resp.status = falcon.HTTP_OK
|
|
|
|
|
|
class UserCert(object):
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp, serial):
|
|
user = db.getUserCertBySerial(self.session, serial)
|
|
if user is None:
|
|
resp.status = falcon.HTTP_NOT_FOUND
|
|
return
|
|
resp.body = json.dumps(_userCertAsDict(user))
|
|
resp.status = falcon.HTTP_OK
|
|
|
|
|
|
def _hostAsDict(host):
|
|
return {
|
|
'id': host.id,
|
|
'name': host.name,
|
|
'pat_bastions': host.pat_bastions,
|
|
'srv_url': host.srv_url,
|
|
}
|
|
|
|
|
|
class Hosts(object):
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp):
|
|
body = {'hosts': [_hostAsDict(host)
|
|
for host in db.getHosts(self.session)]}
|
|
resp.body = json.dumps(body)
|
|
resp.status = falcon.HTTP_OK
|
|
|
|
|
|
class Host(object):
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp, host_id):
|
|
host = db.getHost(self.session, host_id)
|
|
if host is None:
|
|
resp.status = falcon.HTTP_NOT_FOUND
|
|
return
|
|
resp.body = json.dumps(_hostAsDict(host))
|
|
resp.status = falcon.HTTP_OK
|
|
|
|
|
|
def _hostCertAsDict(cert):
|
|
return {
|
|
'host_id': cert.host_id,
|
|
'fingerprint': cert.fingerprint,
|
|
'auth_id': cert.auth_id,
|
|
'cert': cert.cert.strip('\n'),
|
|
'hostname': cert.hostname,
|
|
'created_at': datetime_to_string(cert.created_at),
|
|
'expires_at': datetime_to_string(cert.expires_at),
|
|
}
|
|
|
|
|
|
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 bit harder to steal the token.
|
|
try:
|
|
cert = db.createHostCert(
|
|
self.session,
|
|
req.body['token_id'],
|
|
req.body['host_id'],
|
|
req.body['pub_key']
|
|
)
|
|
except KeyError as e:
|
|
raise falcon.HTTPBadRequest(str(e))
|
|
resp.body = json.dumps(_hostCertAsDict(cert))
|
|
resp.status = falcon.HTTP_200
|
|
resp.location = '/hostcerts/' + cert.host_id + '/' + cert.fingerprint
|
|
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp):
|
|
body = {'certs': [_hostCertAsDict(cert)
|
|
for cert in db.getHostCerts(self.session)]}
|
|
resp.body = json.dumps(body)
|
|
resp.status = falcon.HTTP_OK
|
|
|
|
|
|
class HostCert(object):
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp, host_id, fingerprint):
|
|
cert = db.getHostCert(self.session, host_id, fingerprint)
|
|
if cert is None:
|
|
resp.status = falcon.HTTP_NOT_FOUND
|
|
return
|
|
resp.body = json.dumps(_hostCertAsDict(cert))
|
|
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
|
|
|
|
|
|
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
|
|
# }
|
|
instance_id = req.body['instance-id']
|
|
hostname = req.body['hostname']
|
|
project_id = req.body['project-id']
|
|
try:
|
|
token = db.createToken(
|
|
self.session,
|
|
instance_id,
|
|
project_id,
|
|
hostname,
|
|
)
|
|
except KeyError as e:
|
|
raise falcon.HTTPBadRequest(str(e))
|
|
auth = db.getAuthority(self.session, project_id)
|
|
if auth is None:
|
|
resp.status = falcon.HTTP_NOT_FOUND
|
|
return
|
|
roles = getProjectRoleNames(req.body['project-id'])
|
|
vendordata = {
|
|
'token': token.token_id,
|
|
'auth_pub_key_user': auth.user_pub_key,
|
|
'root_principals': '', #keep in case we want to use it later
|
|
'users': ','.join(roles),
|
|
'sudoers': ','.join([r for r in roles if "admin" in r]),
|
|
'ssh_port': CONF.tatu.ssh_port,
|
|
'api_endpoint': CONF.tatu.api_endpoint_for_vms,
|
|
}
|
|
resp.body = json.dumps(vendordata)
|
|
resp.location = '/hosttokens/' + token.token_id
|
|
resp.status = falcon.HTTP_201
|
|
|
|
host = db.getHost(self.session, instance_id)
|
|
if host is None:
|
|
# TODO(pino): make the whole workflow fault-tolerant
|
|
# TODO(pino): make this configurable per project or subnet
|
|
pat_bastions = ''
|
|
srv_url = ''
|
|
if CONF.tatu.use_pat_bastions:
|
|
ip_port_tuples = create_pat_entries(self.session, instance_id)
|
|
srv_url = add_srv_records(hostname, auth.name, ip_port_tuples)
|
|
pat_bastions = ip_port_tuples_to_string(ip_port_tuples)
|
|
# else, e.g. call LBaaS API
|
|
|
|
db.createHost(session=self.session,
|
|
id=instance_id,
|
|
name=hostname,
|
|
pat_bastions=pat_bastions,
|
|
srv_url=srv_url,
|
|
)
|
|
|
|
|
|
class RevokedUserKeys(object):
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp, auth_id):
|
|
body = {
|
|
'auth_id': auth_id,
|
|
'encoding': 'base64',
|
|
'revoked_keys_data': db.getRevokedKeysBase64(self.session, auth_id)
|
|
}
|
|
resp.body = json.dumps(body)
|
|
resp.status = falcon.HTTP_OK
|
|
|
|
@falcon.before(validate)
|
|
def on_post(self, req, resp, auth_id):
|
|
db.revokeUserKey(
|
|
self.session,
|
|
auth_id,
|
|
serial=req.body.get('serial', None),
|
|
)
|
|
resp.status = falcon.HTTP_OK
|
|
resp.body = json.dumps({})
|
|
|
|
|
|
class PATs(object):
|
|
@falcon.before(validate)
|
|
def on_get(self, req, resp):
|
|
items = []
|
|
if CONF.tatu.use_pat_bastions:
|
|
for p in getAllPats():
|
|
items.append({
|
|
'ip': str(p.ip_address),
|
|
'chassis': p.chassis.id,
|
|
'lport': p.lport.id,
|
|
})
|
|
body = {'pats': items}
|
|
resp.body = json.dumps(body)
|
|
resp.status = falcon.HTTP_OK
|