mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-23 15:40:01 -06:00
c27233e3a0
See: https://pagure.io/freeipa/issue/8882 Signed-off-by: Christian Heimes <cheimes@redhat.com> Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Rob Crittenden <rcritten@redhat.com>
400 lines
14 KiB
Python
400 lines
14 KiB
Python
# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file
|
|
from __future__ import absolute_import
|
|
|
|
import json
|
|
import os
|
|
from base64 import b64decode, b64encode
|
|
|
|
from ipaserver.custodia import log
|
|
from ipaserver.custodia.message.common import UnallowedMessage
|
|
from ipaserver.custodia.message.common import UnknownMessageType
|
|
from ipaserver.custodia.message.formats import Validator
|
|
from ipaserver.custodia.plugin import (
|
|
CSStoreDenied, CSStoreError, CSStoreExists, CSStoreUnsupported
|
|
)
|
|
from ipaserver.custodia.plugin import HTTPConsumer, HTTPError, PluginOption
|
|
|
|
|
|
class Secrets(HTTPConsumer):
|
|
allowed_keytypes = PluginOption('str_set', 'simple', None)
|
|
store = PluginOption('store', None, None)
|
|
|
|
def __init__(self, config, section):
|
|
super(Secrets, self).__init__(config, section)
|
|
self._validator = Validator(self.allowed_keytypes)
|
|
|
|
def _db_key(self, trail):
|
|
if len(trail) < 2:
|
|
self.logger.debug(
|
|
"Forbidden action: Operation only permitted within a "
|
|
"container")
|
|
raise HTTPError(403)
|
|
return os.path.join('keys', *trail)
|
|
|
|
def _db_container_key(self, default, trail):
|
|
f = None
|
|
if len(trail) > 1:
|
|
f = self._db_key(trail)
|
|
elif len(trail) == 1 and trail[0] != '':
|
|
self.logger.debug(
|
|
"Forbidden action: Wrong container path. Container names must "
|
|
"end with '/'")
|
|
raise HTTPError(403)
|
|
elif default is None:
|
|
self.logger.debug("Forbidden action: No default namespace")
|
|
raise HTTPError(403)
|
|
else:
|
|
# Use the default namespace
|
|
f = self._db_key([default, ''])
|
|
return f
|
|
|
|
def _parse(self, request, query, name):
|
|
return self._validator.parse(request, query, name)
|
|
|
|
def _parse_query(self, request, name):
|
|
# default to simple
|
|
query = request.get('query', '')
|
|
if len(query) == 0:
|
|
query = {'type': 'simple', 'value': ''}
|
|
return self._parse(request, query, name)
|
|
|
|
def _parse_bin_body(self, request, name):
|
|
body = request.get('body')
|
|
if body is None:
|
|
raise HTTPError(400)
|
|
value = b64encode(bytes(body)).decode('utf-8')
|
|
payload = {'type': 'simple', 'value': value}
|
|
return self._parse(request, payload, name)
|
|
|
|
def _parse_body(self, request, name):
|
|
body = request.get('body')
|
|
if body is None:
|
|
raise HTTPError(400)
|
|
value = json.loads(bytes(body).decode('utf-8'))
|
|
return self._parse(request, value, name)
|
|
|
|
def _parse_maybe_body(self, request, name):
|
|
body = request.get('body')
|
|
if body is None:
|
|
value = {'type': 'simple', 'value': ''}
|
|
else:
|
|
value = json.loads(bytes(body).decode('utf-8'))
|
|
return self._parse(request, value, name)
|
|
|
|
def _parent_exists(self, default, trail):
|
|
# check that the containers exist
|
|
basename = self._db_container_key(trail[0], trail[:-1] + [''])
|
|
try:
|
|
keylist = self.root.store.list(basename)
|
|
except CSStoreError:
|
|
raise HTTPError(500)
|
|
|
|
self.logger.debug('parent_exists: %s (%s, %r) -> %r',
|
|
basename, default, trail, keylist)
|
|
|
|
if keylist is not None:
|
|
return True
|
|
|
|
# create default namespace if it is the only missing piece
|
|
if len(trail) == 2 and default == trail[0]:
|
|
container = self._db_container_key(default, '')
|
|
self.root.store.span(container)
|
|
return True
|
|
|
|
return False
|
|
|
|
def _format_reply(self, request, response, handler, output):
|
|
reply = handler.reply(output)
|
|
# special case to allow *very* simple clients
|
|
if handler.msg_type == 'simple':
|
|
binary = False
|
|
accept = request.get('headers', {}).get('Accept', None)
|
|
if accept is not None:
|
|
types = accept.split(',')
|
|
for t in types:
|
|
if t.strip() == 'application/json':
|
|
binary = False
|
|
elif t.strip() == 'application/octet-stream':
|
|
binary = True
|
|
if binary:
|
|
response['headers'][
|
|
'Content-Type'] = 'application/octet-stream'
|
|
response['output'] = b64decode(reply['value'])
|
|
return
|
|
|
|
if reply is not None:
|
|
response['headers'][
|
|
'Content-Type'] = 'application/json; charset=utf-8'
|
|
response['output'] = reply
|
|
|
|
def GET(self, request, response):
|
|
trail = request.get('trail', [])
|
|
if len(trail) == 0 or trail[-1] == '':
|
|
self._list(trail, request, response)
|
|
else:
|
|
self._get_key(trail, request, response)
|
|
|
|
def PUT(self, request, response):
|
|
trail = request.get('trail', [])
|
|
if len(trail) == 0 or trail[-1] == '':
|
|
raise HTTPError(405)
|
|
else:
|
|
self._set_key(trail, request, response)
|
|
|
|
def DELETE(self, request, response):
|
|
trail = request.get('trail', [])
|
|
if len(trail) == 0:
|
|
raise HTTPError(405)
|
|
if trail[-1] == '':
|
|
self._destroy(trail, request, response)
|
|
else:
|
|
self._del_key(trail, request, response)
|
|
|
|
def POST(self, request, response):
|
|
trail = request.get('trail', [])
|
|
if len(trail) > 0 and trail[-1] == '':
|
|
self._create(trail, request, response)
|
|
else:
|
|
raise HTTPError(405)
|
|
|
|
def _list(self, trail, request, response):
|
|
try:
|
|
name = '/'.join(trail)
|
|
msg = self._parse_query(request, name)
|
|
except Exception as e:
|
|
raise HTTPError(406, str(e))
|
|
default = request.get('default_namespace', None)
|
|
basename = self._db_container_key(default, trail)
|
|
try:
|
|
keylist = self.root.store.list(basename)
|
|
self.logger.debug('list %s returned %r', basename, keylist)
|
|
if keylist is None:
|
|
raise HTTPError(404)
|
|
response['headers'][
|
|
'Content-Type'] = 'application/json; charset=utf-8'
|
|
response['output'] = msg.reply(keylist)
|
|
except CSStoreDenied:
|
|
self.logger.exception(
|
|
"List: Permission to perform this operation was denied")
|
|
raise HTTPError(403)
|
|
except CSStoreError:
|
|
self.logger.exception('List: Internal server error')
|
|
raise HTTPError(500)
|
|
except CSStoreUnsupported:
|
|
self.logger.exception('List: Unsupported operation')
|
|
raise HTTPError(501)
|
|
|
|
def _create(self, trail, request, response):
|
|
try:
|
|
name = '/'.join(trail)
|
|
msg = self._parse_maybe_body(request, name)
|
|
except Exception as e:
|
|
raise HTTPError(406, str(e))
|
|
default = request.get('default_namespace', None)
|
|
basename = self._db_container_key(None, trail)
|
|
try:
|
|
if len(trail) > 2:
|
|
ok = self._parent_exists(default, trail[:-1])
|
|
if not ok:
|
|
raise HTTPError(404)
|
|
|
|
self.root.store.span(basename)
|
|
except CSStoreDenied:
|
|
self.logger.exception(
|
|
"Create: Permission to perform this operation was denied")
|
|
raise HTTPError(403)
|
|
except CSStoreExists:
|
|
self.logger.debug('Create: Key already exists')
|
|
response['code'] = 200
|
|
return
|
|
except CSStoreError:
|
|
self.logger.exception('Create: Internal server error')
|
|
raise HTTPError(500)
|
|
except CSStoreUnsupported:
|
|
self.logger.exception('Create: Unsupported operation')
|
|
raise HTTPError(501)
|
|
|
|
output = msg.reply(None)
|
|
if output is not None:
|
|
response['headers'][
|
|
'Content-Type'] = 'application/json; charset=utf-8'
|
|
response['output'] = output
|
|
response['code'] = 201
|
|
|
|
def _destroy(self, trail, request, response):
|
|
try:
|
|
name = '/'.join(trail)
|
|
msg = self._parse_maybe_body(request, name)
|
|
except Exception as e:
|
|
raise HTTPError(406, str(e))
|
|
basename = self._db_container_key(None, trail)
|
|
try:
|
|
keylist = self.root.store.list(basename)
|
|
if keylist is None:
|
|
raise HTTPError(404)
|
|
if len(keylist) != 0:
|
|
raise HTTPError(409)
|
|
ret = self.root.store.cut(basename.rstrip('/'))
|
|
except CSStoreDenied:
|
|
self.logger.exception(
|
|
"Delete: Permission to perform this operation was denied")
|
|
raise HTTPError(403)
|
|
except CSStoreError:
|
|
self.logger.exception('Delete: Internal server error')
|
|
raise HTTPError(500)
|
|
except CSStoreUnsupported:
|
|
self.logger.exception('Delete: Unsupported operation')
|
|
raise HTTPError(501)
|
|
|
|
if ret is False:
|
|
raise HTTPError(404)
|
|
|
|
output = msg.reply(None)
|
|
if output is None:
|
|
response['code'] = 204
|
|
else:
|
|
response['headers'][
|
|
'Content-Type'] = 'application/json; charset=utf-8'
|
|
response['output'] = output
|
|
response['code'] = 200
|
|
|
|
def _client_name(self, request):
|
|
if 'remote_user' in request:
|
|
return request['remote_user']
|
|
elif 'creds' in request:
|
|
creds = request['creds']
|
|
return '<pid={pid:d} uid={uid:d} gid={gid:d}>'.format(**creds)
|
|
else:
|
|
return 'Unknown'
|
|
|
|
def _audit(self, ok, fail, fn, trail, request, response):
|
|
action = fail
|
|
client = self._client_name(request)
|
|
key = '/'.join(trail)
|
|
try:
|
|
fn(trail, request, response)
|
|
action = ok
|
|
finally:
|
|
self.audit_key_access(action, client, key)
|
|
|
|
def _get_key(self, trail, request, response):
|
|
self._audit(log.AUDIT_GET_ALLOWED, log.AUDIT_GET_DENIED,
|
|
self._int_get_key, trail, request, response)
|
|
|
|
def _int_get_key(self, trail, request, response):
|
|
try:
|
|
name = '/'.join(trail)
|
|
handler = self._parse_query(request, name)
|
|
except Exception as e:
|
|
raise HTTPError(406, str(e))
|
|
key = self._db_key(trail)
|
|
try:
|
|
output = self.root.store.get(key)
|
|
if output is None:
|
|
raise HTTPError(404)
|
|
elif len(output) == 0:
|
|
raise HTTPError(406)
|
|
self._format_reply(request, response, handler, output)
|
|
except CSStoreDenied:
|
|
self.logger.exception(
|
|
"Get: Permission to perform this operation was denied")
|
|
raise HTTPError(403)
|
|
except CSStoreError:
|
|
self.logger.exception('Get: Internal server error')
|
|
raise HTTPError(500)
|
|
except CSStoreUnsupported:
|
|
self.logger.exception('Get: Unsupported operation')
|
|
raise HTTPError(501)
|
|
|
|
def _set_key(self, trail, request, response):
|
|
self._audit(log.AUDIT_SET_ALLOWED, log.AUDIT_SET_DENIED,
|
|
self._int_set_key, trail, request, response)
|
|
|
|
def _int_set_key(self, trail, request, response):
|
|
try:
|
|
name = '/'.join(trail)
|
|
|
|
content_type = request.get('headers', {}).get('Content-Type', '')
|
|
content_type_value = content_type.split(';')[0].strip()
|
|
if content_type_value == 'application/octet-stream':
|
|
msg = self._parse_bin_body(request, name)
|
|
elif content_type_value == 'application/json':
|
|
msg = self._parse_body(request, name)
|
|
else:
|
|
raise ValueError('Invalid Content-Type')
|
|
except UnknownMessageType as e:
|
|
raise HTTPError(406, str(e))
|
|
except UnallowedMessage as e:
|
|
raise HTTPError(406, str(e))
|
|
except Exception as e:
|
|
raise HTTPError(400, str(e))
|
|
|
|
# must _db_key first as access control is done here for now
|
|
# otherwise users would e able to probe containers in namespaces
|
|
# they do not have access to.
|
|
key = self._db_key(trail)
|
|
|
|
try:
|
|
default = request.get('default_namespace', None)
|
|
ok = self._parent_exists(default, trail)
|
|
if not ok:
|
|
raise HTTPError(404)
|
|
|
|
ok = self.root.store.set(key, msg.payload)
|
|
except CSStoreDenied:
|
|
self.logger.exception(
|
|
"Set: Permission to perform this operation was denied")
|
|
raise HTTPError(403)
|
|
except CSStoreExists:
|
|
self.logger.exception('Set: Key already exist')
|
|
raise HTTPError(409)
|
|
except CSStoreError:
|
|
self.logger.exception('Set: Internal Server Error')
|
|
raise HTTPError(500)
|
|
except CSStoreUnsupported:
|
|
self.logger.exception('Set: Unsupported operation')
|
|
raise HTTPError(501)
|
|
|
|
output = msg.reply(None)
|
|
if output is not None:
|
|
response['headers'][
|
|
'Content-Type'] = 'application/json; charset=utf-8'
|
|
response['output'] = output
|
|
response['code'] = 201
|
|
|
|
def _del_key(self, trail, request, response):
|
|
self._audit(log.AUDIT_DEL_ALLOWED, log.AUDIT_DEL_DENIED,
|
|
self._int_del_key, trail, request, response)
|
|
|
|
def _int_del_key(self, trail, request, response):
|
|
try:
|
|
name = '/'.join(trail)
|
|
msg = self._parse_maybe_body(request, name)
|
|
except Exception as e:
|
|
raise HTTPError(406, str(e))
|
|
key = self._db_key(trail)
|
|
try:
|
|
ret = self.root.store.cut(key)
|
|
except CSStoreDenied:
|
|
self.logger.exception(
|
|
"Delete: Permission to perform this operation was denied")
|
|
raise HTTPError(403)
|
|
except CSStoreError:
|
|
self.logger.exception('Delete: Internal Server Error')
|
|
raise HTTPError(500)
|
|
except CSStoreUnsupported:
|
|
self.logger.exception('Delete: Unsupported operation')
|
|
raise HTTPError(501)
|
|
|
|
if ret is False:
|
|
raise HTTPError(404)
|
|
|
|
output = msg.reply(None)
|
|
if output is None:
|
|
response['code'] = 204
|
|
else:
|
|
response['headers'][
|
|
'Content-Type'] = 'application/json; charset=utf-8'
|
|
response['output'] = output
|
|
response['code'] = 200
|