barbican/barbican/api/controllers/secrets.py

371 lines
14 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 pecan
from six.moves.urllib import parse
from barbican import api
from barbican.api import controllers
from barbican.api.controllers import acls
from barbican.api.controllers import secretmeta
from barbican.common import exception
from barbican.common import hrefs
from barbican.common import quota
from barbican.common import resources as res
from barbican.common import utils
from barbican.common import validators
from barbican import i18n as u
from barbican.model import models
from barbican.model import repositories as repo
from barbican.plugin import resources as plugin
from barbican.plugin import util as putil
LOG = utils.getLogger(__name__)
def _secret_not_found():
"""Throw exception indicating secret not found."""
pecan.abort(404, u._('Not Found. Sorry but your secret is in '
'another castle.'))
def _secret_already_has_data():
"""Throw exception that the secret already has data."""
pecan.abort(409, u._("Secret already has data, cannot modify it."))
def _request_has_twsk_but_no_transport_key_id():
"""Throw exception for bad wrapping parameters.
Throw exception if transport key wrapped session key has been provided,
but the transport key id has not.
"""
pecan.abort(400, u._('Transport key wrapped session key has been '
'provided to wrap secrets for retrieval, but the '
'transport key id has not been provided.'))
class SecretController(controllers.ACLMixin):
"""Handles Secret retrieval and deletion requests."""
def __init__(self, secret):
LOG.debug('=== Creating SecretController ===')
self.secret = secret
self.transport_key_repo = repo.get_transport_key_repository()
def get_acl_tuple(self, req, **kwargs):
d = self.get_acl_dict_for_user(req, self.secret.secret_acls)
d['project_id'] = self.secret.project.external_id
d['creator_id'] = self.secret.creator_id
return 'secret', d
@pecan.expose()
def _lookup(self, sub_resource, *remainder):
if sub_resource == 'acl':
return acls.SecretACLsController(self.secret), remainder
elif sub_resource == 'metadata':
if len(remainder) == 0 or remainder == ('',):
return secretmeta.SecretMetadataController(self.secret), \
remainder
else:
request_method = pecan.request.method
allowed_methods = ['GET', 'PUT', 'DELETE']
if request_method in allowed_methods:
return secretmeta.SecretMetadatumController(self.secret), \
remainder
else:
# methods cannot be handled at controller level
pecan.abort(405)
else:
# only 'acl' and 'metadata' as sub-resource is supported
pecan.abort(405)
@pecan.expose(generic=True)
def index(self, **kwargs):
pecan.abort(405) # HTTP 405 Method Not Allowed as default
@index.when(method='GET')
@utils.allow_all_content_types
@controllers.handle_exceptions(u._('Secret retrieval'))
@controllers.enforce_rbac('secret:get')
def on_get(self, external_project_id, **kwargs):
if controllers.is_json_request_accept(pecan.request):
resp = self._on_get_secret_metadata(self.secret, **kwargs)
LOG.info(u._LI('Retrieved secret metadata for project: %s'),
external_project_id)
return resp
else:
LOG.warning(u._LW('Decrypted secret %s requested using deprecated '
'API call.'), self.secret.id)
return self._on_get_secret_payload(self.secret,
external_project_id,
**kwargs)
def _on_get_secret_metadata(self, secret, **kwargs):
"""GET Metadata-only for a secret."""
pecan.override_template('json', 'application/json')
secret_fields = putil.mime_types.augment_fields_with_content_types(
secret)
transport_key_id = self._get_transport_key_id_if_needed(
kwargs.get('transport_key_needed'), secret)
if transport_key_id:
secret_fields['transport_key_id'] = transport_key_id
return hrefs.convert_to_hrefs(secret_fields)
def _get_transport_key_id_if_needed(self, transport_key_needed, secret):
if transport_key_needed and transport_key_needed.lower() == 'true':
return plugin.get_transport_key_id_for_retrieval(secret)
return None
def _on_get_secret_payload(self, secret, external_project_id, **kwargs):
"""GET actual payload containing the secret."""
# With ACL support, the user token project does not have to be same as
# project associated with secret. The lookup project_id needs to be
# derived from the secret's data considering authorization is already
# done.
external_project_id = secret.project.external_id
project = res.get_or_create_project(external_project_id)
# default to application/octet-stream if there is no Accept header
accept_header = getattr(pecan.request.accept, 'header_value',
'application/octet-stream')
pecan.override_template('', accept_header)
twsk = kwargs.get('trans_wrapped_session_key', None)
transport_key = None
if twsk:
transport_key = self._get_transport_key(
kwargs.get('transport_key_id', None))
return plugin.get_secret(accept_header,
secret,
project,
twsk,
transport_key)
def _get_transport_key(self, transport_key_id):
if transport_key_id is None:
_request_has_twsk_but_no_transport_key_id()
transport_key_model = self.transport_key_repo.get(
entity_id=transport_key_id,
suppress_exception=True)
return transport_key_model.transport_key
@pecan.expose()
@utils.allow_all_content_types
@controllers.handle_exceptions(u._('Secret payload retrieval'))
@controllers.enforce_rbac('secret:decrypt')
def payload(self, external_project_id, **kwargs):
if pecan.request.method != 'GET':
pecan.abort(405)
resp = self._on_get_secret_payload(
self.secret,
external_project_id,
**kwargs
)
LOG.info(u._LI('Retrieved secret payload for project: %s'),
external_project_id)
return resp
@index.when(method='PUT')
@utils.allow_all_content_types
@controllers.handle_exceptions(u._('Secret update'))
@controllers.enforce_rbac('secret:put')
@controllers.enforce_content_types(['application/octet-stream',
'text/plain'])
def on_put(self, external_project_id, **kwargs):
if (not pecan.request.content_type or
pecan.request.content_type == 'application/json'):
pecan.abort(
415,
u._("Content-Type of '{content_type}' is not supported for "
"PUT.").format(content_type=pecan.request.content_type)
)
transport_key_id = kwargs.get('transport_key_id')
payload = pecan.request.body
if not payload:
raise exception.NoDataToProcess()
if validators.secret_too_big(payload):
raise exception.LimitExceeded()
if self.secret.encrypted_data or self.secret.secret_store_metadata:
_secret_already_has_data()
project_model = res.get_or_create_project(external_project_id)
content_type = pecan.request.content_type
content_encoding = pecan.request.headers.get('Content-Encoding')
plugin.store_secret(
unencrypted_raw=payload,
content_type_raw=content_type,
content_encoding=content_encoding,
secret_model=self.secret,
project_model=project_model,
transport_key_id=transport_key_id)
LOG.info(u._LI('Updated secret for project: %s'), external_project_id)
@index.when(method='DELETE')
@utils.allow_all_content_types
@controllers.handle_exceptions(u._('Secret deletion'))
@controllers.enforce_rbac('secret:delete')
def on_delete(self, external_project_id, **kwargs):
plugin.delete_secret(self.secret, external_project_id)
LOG.info(u._LI('Deleted secret for project: %s'), external_project_id)
class SecretsController(controllers.ACLMixin):
"""Handles Secret creation requests."""
def __init__(self):
LOG.debug('Creating SecretsController')
self.validator = validators.NewSecretValidator()
self.secret_repo = repo.get_secret_repository()
self.quota_enforcer = quota.QuotaEnforcer('secrets', self.secret_repo)
@pecan.expose()
def _lookup(self, secret_id, *remainder):
# NOTE(jaosorior): It's worth noting that even though this section
# actually does a lookup in the database regardless of the RBAC policy
# check, the execution only gets here if authentication of the user was
# previously successful.
controllers.assert_is_valid_uuid_from_uri(secret_id)
secret = self.secret_repo.get_secret_by_id(
entity_id=secret_id, suppress_exception=True)
if not secret:
_secret_not_found()
return SecretController(secret), remainder
@pecan.expose(generic=True)
def index(self, **kwargs):
pecan.abort(405) # HTTP 405 Method Not Allowed as default
@index.when(method='GET', template='json')
@controllers.handle_exceptions(u._('Secret(s) retrieval'))
@controllers.enforce_rbac('secrets:get')
def on_get(self, external_project_id, **kw):
def secret_fields(field):
return putil.mime_types.augment_fields_with_content_types(field)
LOG.debug('Start secrets on_get '
'for project-ID %s:', external_project_id)
name = kw.get('name', '')
if name:
name = parse.unquote_plus(name)
bits = kw.get('bits', 0)
try:
bits = int(bits)
except ValueError:
# as per Github issue 171, if bits is invalid then
# the default should be used.
bits = 0
ctxt = controllers._get_barbican_context(pecan.request)
user_id = None
if ctxt:
user_id = ctxt.user
result = self.secret_repo.get_by_create_date(
external_project_id,
offset_arg=kw.get('offset', 0),
limit_arg=kw.get('limit', None),
name=name,
alg=kw.get('alg'),
mode=kw.get('mode'),
bits=bits,
suppress_exception=True,
acl_only=kw.get('acl_only', None),
user_id=user_id
)
secrets, offset, limit, total = result
if not secrets:
secrets_resp_overall = {'secrets': [],
'total': total}
else:
secrets_resp = [
hrefs.convert_to_hrefs(secret_fields(s))
for s in secrets
]
secrets_resp_overall = hrefs.add_nav_hrefs(
'secrets', offset, limit, total,
{'secrets': secrets_resp}
)
secrets_resp_overall.update({'total': total})
LOG.info(u._LI('Retrieved secret list for project: %s'),
external_project_id)
return secrets_resp_overall
@index.when(method='POST', template='json')
@controllers.handle_exceptions(u._('Secret creation'))
@controllers.enforce_rbac('secrets:post')
@controllers.enforce_content_types(['application/json'])
def on_post(self, external_project_id, **kwargs):
LOG.debug('Start on_post for project-ID %s:...', external_project_id)
data = api.load_body(pecan.request, validator=self.validator)
project = res.get_or_create_project(external_project_id)
self.quota_enforcer.enforce(project)
transport_key_needed = data.get('transport_key_needed',
'false').lower() == 'true'
ctxt = controllers._get_barbican_context(pecan.request)
if ctxt: # in authenticated pipleline case, always use auth token user
data['creator_id'] = ctxt.user
secret_model = models.Secret(data)
new_secret, transport_key_model = plugin.store_secret(
unencrypted_raw=data.get('payload'),
content_type_raw=data.get('payload_content_type',
'application/octet-stream'),
content_encoding=data.get('payload_content_encoding'),
secret_model=secret_model,
project_model=project,
transport_key_needed=transport_key_needed,
transport_key_id=data.get('transport_key_id'))
url = hrefs.convert_secret_to_href(new_secret.id)
LOG.debug('URI to secret is %s', url)
pecan.response.status = 201
pecan.response.headers['Location'] = url
LOG.info(u._LI('Created a secret for project: %s'),
external_project_id)
if transport_key_model is not None:
tkey_url = hrefs.convert_transport_key_to_href(
transport_key_model.id)
return {'secret_ref': url, 'transport_key_ref': tkey_url}
else:
return {'secret_ref': url}