Esempio n. 1
0
class EncryptedStore(SqliteStore):
    master_key = PluginOption(str, REQUIRED, None)
    master_enctype = PluginOption(str, 'A256CBC-HS512', None)

    def __init__(self, config, section):
        super(EncryptedStore, self).__init__(config, section)
        with open(self.master_key) as f:
            data = f.read()
            key = json_decode(data)
            self.mkey = JWK(**key)

    def get(self, key):
        value = super(EncryptedStore, self).get(key)
        if value is None:
            return None
        try:
            jwe = JWE()
            jwe.deserialize(value, self.mkey)
            return jwe.payload.decode('utf-8')
        except Exception:
            self.logger.exception("Error parsing key %s", key)
            raise CSStoreError('Error occurred while trying to parse key')

    def set(self, key, value, replace=False):
        protected = json_encode({'alg': 'dir', 'enc': self.master_enctype})
        jwe = JWE(value, protected)
        jwe.add_recipient(self.mkey)
        cvalue = jwe.serialize(compact=True)
        return super(EncryptedStore, self).set(key, cvalue, replace)
Esempio n. 2
0
class UserNameSpace(HTTPAuthorizer):
    path = PluginOption(str, '/', 'User namespace path')
    store = PluginOption('store', None, None)

    def handle(self, request):
        # Only check if we are in the right (sub)path
        path = request.get('path', '/')
        if not path.startswith(self.path):
            self.logger.debug('%s is not contained in %s', path, self.path)
            return None

        name = request.get('remote_user', None)
        if name is None:
            # UserNameSpace requires a user ...
            self.audit_svc_access(log.AUDIT_SVC_AUTHZ_FAIL,
                                  request['client_id'], path)
            return False

        # pylint: disable=no-member
        namespace = self.path.rstrip('/') + '/' + name + '/'
        if not path.startswith(namespace):
            # Not in the namespace
            self.audit_svc_access(log.AUDIT_SVC_AUTHZ_FAIL,
                                  request['client_id'], path)
            return False

        request['default_namespace'] = name
        self.audit_svc_access(log.AUDIT_SVC_AUTHZ_PASS, request['client_id'],
                              path)
        return True
Esempio n. 3
0
class SimpleClientCertAuth(HTTPAuthenticator):
    header = PluginOption(str, 'CUSTODIA_CERT_AUTH', "header name")

    def handle(self, request):
        cert_auth = request['headers'].get(self.header, "false").lower()
        client_cert = request['client_cert']  # {} or None
        if not client_cert or cert_auth not in {'1', 'yes', 'true', 'on'}:
            self.logger.debug('Ignoring request no relevant header or cert'
                              ' provided')
            return None

        subject = client_cert.get('subject', {})
        dn = []
        name = None
        # TODO: check SAN first
        for rdn in subject:
            for key, value in rdn:
                dn.append('{}="{}"'.format(key, value.replace('"', r'\"')))
                if key == 'commonName':
                    name = value
                    break

        dn = ', '.join(dn)
        self.logger.debug('Client cert subject: {}, serial: {}'.format(
            dn, client_cert.get('serialNumber')))

        if name:
            self.audit_svc_access(log.AUDIT_SVC_AUTH_PASS,
                                  request['client_id'], name)
            request['remote_user'] = name
            return True

        self.audit_svc_access(log.AUDIT_SVC_AUTH_FAIL, request['client_id'],
                              dn)
        return False
class HeadHandler(HTTPConsumer):
    store = PluginOption('store', None, None)

    def __init__(self, config, section):
        super(HeadHandler, self).__init__(config, section)
        if self.store_name is not None:
            self.add_sub('secrets', HeadSecrets(config, section))

    def _find_handler(self, request):
        base = self
        command = request.get('command', 'GET')
        if command not in SUPPORTED_COMMANDS:
            raise HTTPError(501)
        trail = request.get('trail', None)
        if trail is not None:
            for comp in trail:
                subs = getattr(base, 'subs', {})
                if comp in subs:
                    base = subs[comp]
                    trail.pop(0)
                else:
                    break

        handler = getattr(base, command)
        if handler is None:
            raise HTTPError(400)

        return handler
Esempio n. 5
0
class ContainerAuth(HTTPAuthenticator):
    matchers = PluginOption('str_list', ['docker', 'rkt'], None)
    docker_regex = PluginOption('regex', DOCKER_REGEX, None)
    rkt_regex = PluginOption('regex', RKT_REGEX, None)

    def _read_cgroup(self, pid):
        with open('/proc/{:d}/cgroup'.format(pid)) as f:
            return list(f)

    def _match_docker(self, lines):
        for line in lines:
            # pylint: disable=no-member
            mo = self.docker_regex.search(line)
            if mo is not None:
                return mo.group(1)
        return None, None

    def _match_rkt(self, lines):
        for line in lines:
            # pylint: disable=no-member
            mo = self.docker_regex.search(line)
            if mo is not None:
                return '-'.join(mo.groups())

    def handle(self, request):
        creds = request.get('creds')
        if creds is None:
            self.logger.debug('Missing "creds" on request')
            return None

        pid = int(creds['pid'])
        try:
            lines = self._read_cgroup(pid)
        except OSError:
            self.logger.exception("Failed to read cgroup for pid %i", pid)
            return None

        for matcher in self.matchers:  # pylint: disable=not-an-iterable
            func = getattr(self, '_match_{}'.format(matcher))
            result = func(lines)
            if result is not None:
                creds['container'] = (matcher, result)
                self.logger.info("Detected %s://%s for pid %i", matcher,
                                 result, pid)
                return True

        return False
Esempio n. 6
0
class SimpleCredsAuth(HTTPAuthenticator):
    uid = PluginOption('pwd_uid', 0, "User id or name")
    gid = PluginOption('grp_gid', 0, "Group id or name")

    def handle(self, request):
        creds = request.get('creds')
        if creds is None:
            self.logger.debug('SCA: Missing "creds" from request')
            return False
        uid = int(creds['gid'])
        gid = int(creds['uid'])
        if self.gid == gid or self.uid == uid:
            self.audit_svc_access(log.AUDIT_SVC_AUTH_PASS,
                                  request['client_id'], "%d, %d" % (uid, gid))
            return True
        else:
            self.audit_svc_access(log.AUDIT_SVC_AUTH_FAIL,
                                  request['client_id'], "%d, %d" % (uid, gid))
            return False
Esempio n. 7
0
class Root(HTTPConsumer):
    store = PluginOption('store', None, None)

    def __init__(self, config, section):
        super(Root, self).__init__(config, section)
        if self.store_name is not None:
            self.add_sub('secrets', Secrets(config, section))

    def GET(self, request, response):
        msg = json.dumps({'message': "Quis custodiet ipsos custodes?"})
        return msg.encode('utf-8')
Esempio n. 8
0
class SimpleHeaderAuth(HTTPAuthenticator):
    header = PluginOption(str, 'REMOTE_USER', "header name")
    value = PluginOption('str_set', None,
                         "Comma-separated list of required values")

    def handle(self, request):
        if self.header not in request['headers']:
            self.logger.debug('SHA: No "headers" in request')
            return None
        value = request['headers'][self.header]
        if self.value is not None:
            # pylint: disable=unsupported-membership-test
            if value not in self.value:
                self.audit_svc_access(log.AUDIT_SVC_AUTH_FAIL,
                                      request['client_id'], value)
                return False

        self.audit_svc_access(log.AUDIT_SVC_AUTH_PASS, request['client_id'],
                              value)
        request['remote_user'] = value
        return True
Esempio n. 9
0
class SimpleAuthKeys(HTTPAuthenticator):
    id_header = PluginOption(str, 'CUSTODIA_AUTH_ID', "auth id header name")
    key_header = PluginOption(str, 'CUSTODIA_AUTH_KEY', "auth key header name")
    store = PluginOption('store', None, None)
    store_namespace = PluginOption(str, 'custodiaSAK', "")

    def _db_key(self, name):
        return os.path.join(self.store_namespace, name)

    def handle(self, request):
        name = request['headers'].get(self.id_header, None)
        key = request['headers'].get(self.key_header, None)
        if name is None and key is None:
            self.logger.debug('Ignoring request no relevant headers provided')
            return None

        validated = False
        try:
            val = self.store.get(self._db_key(name))
            if val is None:
                raise ValueError("No such ID")
            if constant_time.bytes_eq(val.encode('utf-8'),
                                      key.encode('utf-8')):
                validated = True
        except Exception:  # pylint: disable=broad-except
            self.audit_svc_access(log.AUDIT_SVC_AUTH_FAIL,
                                  request['client_id'], name)
            return False

        if validated:
            self.audit_svc_access(log.AUDIT_SVC_AUTH_PASS,
                                  request['client_id'], name)
            request['remote_user'] = name
            return True

        self.audit_svc_access(log.AUDIT_SVC_AUTH_FAIL, request['client_id'],
                              name)
        return False
Esempio n. 10
0
class IPAVault(CSStore):
    # vault arguments
    principal = PluginOption(
        str, None,
        "Service principal for service vault (auto-discovered from GSSAPI)"
    )
    user = PluginOption(
        str, None,
        "User name for user vault (auto-discovered from GSSAPI)"
    )
    vault_type = PluginOption(
        str, None,
        "vault type, one of 'user', 'service', 'shared', or "
        "auto-discovered from GSSAPI"
    )

    def __init__(self, config, section=None, api=None):
        super(IPAVault, self).__init__(config, section)
        self._vault_args = None
        self.ipa = None

    def finalize_init(self, config, cfgparser, context=None):
        super(IPAVault, self).finalize_init(config, cfgparser, context)

        if self.ipa is not None:
            return
        self.ipa = IPAInterface.from_config(config)
        self.ipa.finalize_init(config, cfgparser, context=self)

        # connect
        with self.ipa:
            # retrieve and cache KRA transport cert
            response = self.ipa.Command.vaultconfig_show()
            servers = response[u'result'].get(u'kra_server_server', ())
            if servers:
                self.logger.info("KRA server(s) %s", ', '.join(servers))

        service, user_host, realm = krb5_unparse_principal_name(
            self.ipa.principal)
        self._init_vault_args(service, user_host, realm)

    def _init_vault_args(self, service, user_host, realm):
        if self.vault_type is None:
            self.vault_type = 'user' if service is None else 'service'
            self.logger.info("Setting vault type to '%s' from Kerberos",
                             self.vault_type)

        if self.vault_type == 'shared':
            self._vault_args = {'shared': True}
        elif self.vault_type == 'user':
            if self.user is None:
                if service is not None:
                    msg = "{!r}: User vault requires 'user' parameter"
                    raise ValueError(msg.format(self))
                else:
                    self.user = user_host
                    self.logger.info(u"Setting username '%s' from Kerberos",
                                     self.user)
            if six.PY2 and isinstance(self.user, str):
                self.user = self.user.decode('utf-8')
            self._vault_args = {'username': self.user}
        elif self.vault_type == 'service':
            if self.principal is None:
                if service is None:
                    msg = "{!r}: Service vault requires 'principal' parameter"
                    raise ValueError(msg.format(self))
                else:
                    self.principal = u'/'.join((service, user_host))
                    self.logger.info(u"Setting principal '%s' from Kerberos",
                                     self.principal)
            if six.PY2 and isinstance(self.principal, str):
                self.principal = self.principal.decode('utf-8')
            self._vault_args = {'service': self.principal}
        else:
            msg = '{!r}: Invalid vault type {}'
            raise ValueError(msg.format(self, self.vault_type))

    def _mangle_key(self, key):
        if '__' in key:
            raise ValueError
        key = key.replace('/', '__')
        if isinstance(key, bytes):
            key = key.decode('utf-8')
        return key

    def get(self, key):
        key = self._mangle_key(key)
        with self.ipa as ipa:
            try:
                result = ipa.Command.vault_retrieve(
                    key, **self._vault_args)
            except NotFound as e:
                self.logger.info("Key '%s' not found: %s", key, e)
                return None
            except Exception:
                msg = "Failed to retrieve entry {}".format(key)
                self.logger.exception(msg)
                raise CSStoreError(msg)
            else:
                return result[u'result'][u'data']

    def set(self, key, value, replace=False):
        key = self._mangle_key(key)
        if not isinstance(value, bytes):
            value = value.encode('utf-8')
        with self.ipa as ipa:
            try:
                ipa.Command.vault_add(
                    key, ipavaulttype=u"standard", **self._vault_args)
            except DuplicateEntry as e:
                self.logger.info("Vault '%s' already exists: %s", key, e)
                if not replace:
                    raise CSStoreExists(key)
            except AuthorizationError:
                msg = "vault_add denied for entry {}".format(key)
                self.logger.exception(msg)
                raise CSStoreDenied(msg)
            except Exception:
                msg = "Failed to add entry {}".format(key)
                self.logger.exception(msg)
                raise CSStoreError(msg)
            try:
                ipa.Command.vault_archive(
                    key, data=value, **self._vault_args)
            except AuthorizationError:
                msg = "vault_archive denied for entry {}".format(key)
                self.logger.exception(msg)
                raise CSStoreDenied(msg)
            except Exception:
                msg = "Failed to archive entry {}".format(key)
                self.logger.exception(msg)
                raise CSStoreError(msg)

    def span(self, key):
        raise CSStoreUnsupported("span is not implemented")

    def list(self, keyfilter=None):
        with self.ipa as ipa:
            try:
                result = ipa.Command.vault_find(
                    ipavaulttype=u"standard", **self._vault_args)
            except AuthorizationError:
                msg = "vault_find denied"
                self.logger.exception(msg)
                raise CSStoreDenied(msg)
            except Exception:
                msg = "Failed to list entries"
                self.logger.exception(msg)
                raise CSStoreError(msg)

        names = []
        for entry in result[u'result']:
            cn = entry[u'cn'][0]
            key = cn.replace('__', '/')
            if keyfilter is not None and not key.startswith(keyfilter):
                continue
            names.append(key.rsplit('/', 1)[-1])
        return names

    def cut(self, key):
        key = self._mangle_key(key)
        with self.ipa as ipa:
            try:
                ipa.Command.vault_del(key, **self._vault_args)
            except NotFound:
                return False
            except AuthorizationError:
                msg = "vault_del denied for entry {}".format(key)
                self.logger.exception(msg)
                raise CSStoreDenied(msg)
            except Exception:
                msg = "Failed to delete entry {}".format(key)
                self.logger.exception(msg)
                raise CSStoreError(msg)
            else:
                return True
Esempio n. 11
0
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
                        break
                    elif t.strip() == 'application/octet-stream':
                        binary = True
            if binary is True:
                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.exception('Create: Key already exists')
            raise HTTPError(409)
        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
Esempio n. 12
0
class SqliteStore(CSStore):
    dburi = PluginOption(str, REQUIRED, None)
    table = PluginOption(str, "CustodiaSecrets", None)
    filemode = PluginOption(oct, '600', None)

    def __init__(self, config, section):
        super(SqliteStore, self).__init__(config, section)
        # Initialize the DB by trying to create the default table
        try:
            conn = sqlite3.connect(self.dburi)
            os.chmod(self.dburi, self.filemode)
            with conn:
                c = conn.cursor()
                self._create(c)
        except sqlite3.Error:
            self.logger.exception("Error creating table %s", self.table)
            raise CSStoreError('Error occurred while trying to init db')

    def get(self, key):
        self.logger.debug("Fetching key %s", key)
        query = "SELECT value from %s WHERE key=?" % self.table
        try:
            conn = sqlite3.connect(self.dburi)
            c = conn.cursor()
            r = c.execute(query, (key, ))
            value = r.fetchall()
        except sqlite3.Error:
            self.logger.exception("Error fetching key %s", key)
            raise CSStoreError('Error occurred while trying to get key')
        self.logger.debug("Fetched key %s got result: %r", key, value)
        if len(value) > 0:
            return value[0][0]
        else:
            return None

    def _create(self, cur):
        create = "CREATE TABLE IF NOT EXISTS %s " \
                 "(key PRIMARY KEY UNIQUE, value)" % self.table
        cur.execute(create)

    def set(self, key, value, replace=False):
        self.logger.debug("Setting key %s to value %s (replace=%s)", key,
                          value, replace)
        if key.endswith('/'):
            raise ValueError('Invalid Key name, cannot end in "/"')
        if replace:
            query = "INSERT OR REPLACE into %s VALUES (?, ?)"
        else:
            query = "INSERT into %s VALUES (?, ?)"
        setdata = query % (self.table, )
        try:
            conn = sqlite3.connect(self.dburi)
            with conn:
                c = conn.cursor()
                self._create(c)
                c.execute(setdata, (key, value))
        except sqlite3.IntegrityError as err:
            raise CSStoreExists(str(err))
        except sqlite3.Error as err:
            self.logger.exception("Error storing key %s", key)
            raise CSStoreError('Error occurred while trying to store key')

    def span(self, key):
        name = key.rstrip('/')
        self.logger.debug("Creating container %s", name)
        query = "INSERT into %s VALUES (?, '')"
        setdata = query % (self.table, )
        try:
            conn = sqlite3.connect(self.dburi)
            with conn:
                c = conn.cursor()
                self._create(c)
                c.execute(setdata, (name, ))
        except sqlite3.IntegrityError as err:
            raise CSStoreExists(str(err))
        except sqlite3.Error:
            self.logger.exception("Error creating key %s", name)
            raise CSStoreError('Error occurred while trying to span container')

    def list(self, keyfilter=''):
        path = keyfilter.rstrip('/')
        self.logger.debug("Listing keys matching %s", path)
        child_prefix = path if path == '' else path + '/'
        search = "SELECT key FROM %s WHERE key LIKE ?" % self.table
        key = "%s%%" % (path, )
        try:
            conn = sqlite3.connect(self.dburi)
            r = conn.execute(search, (key, ))
            rows = r.fetchall()
        except sqlite3.Error:
            self.logger.exception("Error listing %s: [%r]", keyfilter)
            raise CSStoreError('Error occurred while trying to list keys')
        self.logger.debug("Searched for %s got result: %r", path, rows)
        if len(rows) > 0:
            parent_exists = False
            value = list()
            for row in rows:
                if row[0] == path or row[0] == child_prefix:
                    parent_exists = True
                    continue
                if not row[0].startswith(child_prefix):
                    continue
                value.append(row[0][len(child_prefix):].lstrip('/'))

            if value:
                self.logger.debug("Returning sorted values %r", value)
                return sorted(value)
            elif parent_exists:
                self.logger.debug("Returning empty list")
                return []
        elif keyfilter == '':
            self.logger.debug("Returning empty list")
            return []
        self.logger.debug("Returning 'Not Found'")
        return None

    def cut(self, key):
        self.logger.debug("Removing key %s", key)
        query = "DELETE from %s WHERE key=?" % self.table
        try:
            conn = sqlite3.connect(self.dburi)
            with conn:
                c = conn.cursor()
                r = c.execute(query, (key, ))
        except sqlite3.Error:
            self.logger.error("Error removing key %s", key)
            raise CSStoreError('Error occurred while trying to cut key')
        self.logger.debug("Key %s %s", key,
                          "removed" if r.rowcount > 0 else "not found")
        if r.rowcount > 0:
            return True
        return False
Esempio n. 13
0
class EncryptedOverlay(CSStore):
    """Encrypted overlay for storage backends

    Arguments:
        backing_store (required):
            name of backing storage
        master_key (required)
            path to master key (JWK JSON)
        autogen_master_key (default: false)
            auto-generate key file if missing?
        master_enctype (default: A256CBC_HS512)
            JWE algorithm name
        secret_protection (default: 'encrypt'):
            Determine the kind of protection used to save keys:
            - 'encrypt': this is the classic method (backwards compatible)
            - 'pinning': this adds a protected header with the key name as
            add data, to prevent key swapping in the db
            - 'migrate': as pinning, but on missing key information the
            secret is updated instead of throwing an exception.
    """
    key_sizes = {
        'A128CBC-HS256': 256,
        'A256CBC-HS512': 512,
    }

    backing_store = PluginOption(str, REQUIRED, None)
    master_enctype = PluginOption(str, 'A256CBC-HS512', None)
    master_key = PluginOption(str, REQUIRED, None)
    autogen_master_key = PluginOption(bool, False, None)
    secret_protection = PluginOption(str, False, 'encrypt')

    def __init__(self, config, section):
        super(EncryptedOverlay, self).__init__(config, section)
        self.store_name = self.backing_store
        self.store = None
        self.protected_header = None

        if (not os.path.isfile(self.master_key)
                and self.autogen_master_key):
            # XXX https://github.com/latchset/jwcrypto/issues/50
            size = self.key_sizes.get(self.master_enctype, 512)
            key = JWK(generate='oct', size=size)
            with open(self.master_key, 'w') as f:
                os.fchmod(f.fileno(), 0o600)
                f.write(key.export())

        with open(self.master_key) as f:
            data = f.read()
            key = json_decode(data)
            self.mkey = JWK(**key)

    def get(self, key):
        value = self.store.get(key)
        if value is None:
            return None
        try:
            jwe = JWE()
            jwe.deserialize(value, self.mkey)
            value = jwe.payload.decode('utf-8')
        except Exception as err:
            self.logger.error("Error parsing key %s: [%r]" % (key, repr(err)))
            raise CSStoreError('Error occurred while trying to parse key')
        if self.secret_protection == 'encrypt':
            return value
        if 'custodia.key' not in jwe.jose_header:
            if self.secret_protection == 'migrate':
                self.set(key, value, replace=True)
            else:
                raise CSStoreError('Secret Pinning check failed!'
                                   + 'Missing custodia.key element')
        elif jwe.jose_header['custodia.key'] != key:
            raise CSStoreError(
                'Secret Pinning check failed! Expected {} got {}'.format(
                    key, jwe.jose_header['custodia.key']))
        return value

    def set(self, key, value, replace=False):
        self.protected_header = {'alg': 'dir', 'enc': self.master_enctype}
        if self.secret_protection != 'encrypt':
            self.protected_header['custodia.key'] = key
        protected = json_encode(self.protected_header)
        jwe = JWE(value, protected)
        jwe.add_recipient(self.mkey)
        cvalue = jwe.serialize(compact=True)
        return self.store.set(key, cvalue, replace)

    def span(self, key):
        return self.store.span(key)

    def list(self, keyfilter=''):
        return self.store.list(keyfilter)

    def cut(self, key):
        return self.store.cut(key)
Esempio n. 14
0
class EncryptedOverlay(CSStore):
    """Encrypted overlay for storage backends

    Arguments:
        backing_store (required):
            name of backing storage
        master_key (required)
            path to master key (JWK JSON)
        autogen_master_key (default: false)
            auto-generate key file if missing?
        master_enctype (default: A256CBC_HS512)
            JWE algorithm name
    """
    key_sizes = {
        'A128CBC-HS256': 256,
        'A256CBC-HS512': 512,
    }

    backing_store = PluginOption(str, REQUIRED, None)
    master_enctype = PluginOption(str, 'A256CBC-HS512', None)
    master_key = PluginOption(str, REQUIRED, None)
    autogen_master_key = PluginOption(bool, False, None)

    def __init__(self, config, section):
        super(EncryptedOverlay, self).__init__(config, section)
        self.store_name = self.backing_store
        self.store = None

        if (not os.path.isfile(self.master_key) and self.autogen_master_key):
            # XXX https://github.com/latchset/jwcrypto/issues/50
            size = self.key_sizes.get(self.master_enctype, 512)
            key = JWK(generate='oct', size=size)
            with open(self.master_key, 'w') as f:
                os.fchmod(f.fileno(), 0o600)
                f.write(key.export())

        with open(self.master_key) as f:
            data = f.read()
            key = json_decode(data)
            self.mkey = JWK(**key)

    def get(self, key):
        value = self.store.get(key)
        if value is None:
            return None
        try:
            jwe = JWE()
            jwe.deserialize(value, self.mkey)
            return jwe.payload.decode('utf-8')
        except Exception as err:
            self.logger.error("Error parsing key %s: [%r]" % (key, repr(err)))
            raise CSStoreError('Error occurred while trying to parse key')

    def set(self, key, value, replace=False):
        protected = json_encode({'alg': 'dir', 'enc': self.master_enctype})
        jwe = JWE(value, protected)
        jwe.add_recipient(self.mkey)
        cvalue = jwe.serialize(compact=True)
        return self.store.set(key, cvalue, replace)

    def span(self, key):
        return self.store.span(key)

    def list(self, keyfilter=''):
        return self.store.list(keyfilter)

    def cut(self, key):
        return self.store.cut(key)
Esempio n. 15
0
class IPACertRequest(CSStore):
    """IPA cert request store

    The IPACertRequest store plugin generates or revokes certificates on the
    fly. It uses a backing store to cache certs and private keys.

    The request ```GET /secrets/certs/HTTP/client1.ipa.example``` generates a
    private key and CSR for the service ```HTTP/client1.ipa.example``` with
    DNS subject alternative name ```client1.ipa.example```.

    A DELETE request removes the cert/key pair from the backing store and
    revokes the cert at the same time.
    """
    backing_store = PluginOption(str, REQUIRED, None)

    key_size = PluginOption(int, 2048, 'RSA key size')
    cert_profile = PluginOption(str, 'caIPAserviceCert', 'IPA cert profile')
    add_principal = PluginOption(bool, True, 'Add missing principal')
    chain = PluginOption(bool, True, 'Return full cert chain')
    allowed_services = PluginOption('str_set', {'HTTP'}, 'Service prefixes')
    revocation_reason = PluginOption(int, 4,
                                     'Cert revocation reason (4: superseded)')

    def __init__(self, config, section=None):
        super(IPACertRequest, self).__init__(config, section)
        self.store_name = self.backing_store
        self.store = None
        self.ipa = IPAInterface.get_instance()
        if not isinstance(self.cert_profile, six.text_type):
            self.cert_profile = self.cert_profile.decode('utf-8')

    def _parse_key(self, key):
        if not isinstance(key, six.text_type):
            key = key.decode('utf-8')
        parts = key.split(u'/')
        # XXX why is 'keys' added in in Secrets._db_key()?
        if len(parts) != 3 or parts[0] != 'keys':
            raise CSStoreError("Invalid cert request key '{}'".format(key))
        service, hostname = parts[1:3]
        # pylint: disable=unsupported-membership-test
        if service not in self.allowed_services:
            raise CSStoreError("Invalid service '{}'".format(key))
        principal = krb5_format_service_principal_name(service, hostname,
                                                       self.ipa.env.realm)
        # use cert prefix in storage key
        key = u"cert/{}/{}".format(service, hostname)
        return key, hostname, principal

    def get(self, key):
        # check key first
        key, hostname, principal = self._parse_key(key)
        value = self.store.get(key)
        if value is not None:
            # TODO: validate certificate
            self.logger.info("Found cached certificate for %s", principal)
            return value
        # found no cached key/cert pair, generate one
        try:
            data = self._request_cert(hostname, principal)
        except AuthorizationError:
            msg = "Unauthorized request for '{}' ({})".format(
                hostname, principal)
            self.logger.info(msg, exc_info=True)
            raise CSStoreError(msg)
        except NotFound:
            msg = "Host '{}' or principal '{}' not found".format(
                hostname, principal)
            self.logger.info(msg, exc_info=True)
            raise CSStoreError(msg)
        except Exception:
            msg = "Failed to request cert '{}' ({})".format(
                hostname, principal)
            self.logger.error(msg, exc_info=True)
            raise CSStoreError(msg)
        self.store.set(key, data, replace=True)
        return data

    def set(self, key, value, replace=False):
        key, hostname, principal = self._parse_key(key)
        del hostname, principal
        return self.store.set(key, value, replace)

    def span(self, key):
        key, hostname, principal = self._parse_key(key)
        del hostname, principal
        return self.store.span(key)

    def list(self, keyfilter=''):
        return self.store.list(keyfilter)

    def cut(self, key):
        key, hostname, principal = self._parse_key(key)
        certs = self._revoke_certs(hostname, principal)
        return self.store.cut(key) or certs

    def _request_cert(self, hostname, principal):
        self.logger.info("Requesting certificate for %s", hostname)
        csrgen = _ServerCSRGenerator(plugin=self)
        builder = csrgen.build_csr(hostname=hostname)
        response, pem = csrgen.request_cert(builder, principal=principal)
        self.logger.info(
            "Got certificate for '%s', request id %s, serial number %s",
            response[u'result'][u'subject'],
            response[u'result'][u'request_id'],
            response[u'result'][u'serial_number'],
        )
        return pem

    def _revoke_certs(self, hostname, principal):
        with self.ipa as ipa:
            response = ipa.Command.cert_find(
                service=principal,
                validnotafter_from=datetime.datetime.utcnow(),
            )
            # XXX cert_find has no filter for valid cert
            certs = list(cert for cert in response['result']
                         if not cert[u'revoked'])
            for cert in certs:
                self.logger.info('Revoking cert %i (subject: %s, issuer: %s)',
                                 cert[u'serial_number'], cert[u'subject'],
                                 cert[u'issuer'])
                ipa.Command.cert_revoke(
                    cert[u'serial_number'],
                    revocation_reason=self.revocation_reason,
                )
            return certs
Esempio n. 16
0
class IPAInterface(HTTPAuthenticator):
    """IPA interface authenticator

    Custodia uses a forking server model. We can bootstrap FreeIPA API in
    the main process. Connections must be created in the client process.
    """
    # Kerberos flags
    krb5config = PluginOption(str, None, "Kerberos krb5.conf override")
    keytab = PluginOption(str, None, "Kerberos keytab for auth")
    ccache = PluginOption(str, None,
                          "Kerberos ccache, e,g. FILE:/path/to/ccache")

    # ipalib.api arguments
    ipa_confdir = PluginOption(str, None, "IPA confdir override")
    ipa_context = PluginOption(str, "cli", "IPA bootstrap context")
    ipa_debug = PluginOption(bool, False, "debug mode for ipalib")

    # filled by gssapi()
    principal = False

    # singleton
    _instance = None

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            raise RuntimeError("{} not initialized".format(IPA_SECTIONNAME))
        return cls._instance

    @classmethod
    def set_instance(cls, instance):
        cls._instance = instance

    def __init__(self, config, section=None, api=None):
        super(IPAInterface, self).__init__(config, section)
        # only one instance of this plugin is supported
        if section != IPA_SECTIONNAME:
            raise ValueError(section)

        if api is None:
            self._api = ipalib.api
        else:
            self._api = api

        if self._api.isdone('bootstrap'):
            raise RuntimeError("IPA API already initialized")

        self._ipa_config = dict(
            context=self.ipa_context,
            debug=self.ipa_debug,
            log=None,  # disable logging to file
        )
        if self.ipa_confdir is not None:
            self._ipa_config['confdir'] = self.ipa_confdir

        self._gssapi_config()
        self._bootstrap()

        with self:
            self.logger.info("IPA server '%s': %s", self.env.server,
                             self.Command.ping()[u'summary'])

        self.set_instance(self)

    def handle(self, request):
        request[IPA_SECTIONNAME] = self
        return None

    # rest is interface and initialization

    def _gssapi_config(self):
        # set client keytab env var for authentication
        if self.keytab is not None:
            os.environ['KRB5_CLIENT_KTNAME'] = self.keytab
        if self.ccache is not None:
            os.environ['KRB5CCNAME'] = self.ccache
        if self.krb5config is not None:
            os.environ['KRB5_CONFIG'] = self.krb5config

        self.principal = self._gssapi_cred()
        self.logger.info(u"Kerberos principal '%s'", self.principal)

    def _gssapi_cred(self):
        try:
            return get_principal()
        except Exception:
            self.logger.error(
                "Unable to get principal from GSSAPI. Are you missing a "
                "TGT or valid Kerberos keytab?")
            raise

    def _bootstrap(self):
        # TODO: bandaid for "A PKCS #11 module returned CKR_DEVICE_ERROR"
        # https://github.com/avocado-framework/avocado/issues/1112#issuecomment-206999400
        os.environ['NSS_STRICT_NOFORK'] = 'DISABLED'
        self._api.bootstrap(**self._ipa_config)
        self._api.finalize()

    @property
    def Command(self):
        return self._api.Command  # pylint: disable=no-member

    @property
    def env(self):
        return self._api.env  # pylint: disable=no-member

    def __enter__(self):
        # pylint: disable=no-member
        self._gssapi_cred()
        if not self._api.Backend.rpcclient.isconnected():
            self._api.Backend.rpcclient.connect()
        # pylint: enable=no-member
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # pylint: disable=no-member
        if self._api.Backend.rpcclient.isconnected():
            self._api.Backend.rpcclient.disconnect()
Esempio n. 17
0
class OpenShiftHostnameAuthz(HTTPAuthorizer):
    """

    https://github.com/openshift/origin/blob/master/docs/cluster_up_down.md

    $ oc project
    test
    $ oc create serviceaccount custodia
    $ oc policy add-role-to-user view system:serviceaccount:test:custodia
    $ oc describe serviceaccount custodia
    ...
    Mountable secrets:      custodia-token-...
    ...
    $ oc describe secret custodia-token-...
    """
    oc_uri = PluginOption(str, REQUIRED, None)
    token = PluginOption(str, REQUIRED, None)
    project = PluginOption(str, REQUIRED, None)
    tls_verify = PluginOption(bool, True, None)
    tls_cafile = PluginOption(str, INHERIT_GLOBAL(None), 'Path to CA file')
    hostname_annotation = PluginOption(
        str,
        "latchset.github.io/custodia.hostname",
        None
    )

    def __init__(self, config, section=None):
        super(OpenShiftHostnameAuthz, self).__init__(config, section)
        self.session = None
        self.oc_uri = self.oc_uri.rstrip('/')

    def finalize_init(self, config, cfgparser, context=None):
        super(OpenShiftHostnameAuthz, self).finalize_init(
            config, cfgparser, context)
        self.session = requests.Session()
        self.session.headers["Authorization"] = "Bearer {}".format(self.token)
        if self.tls_cafile is not None:
            self.session.verify = self.tls_cafile
        else:
            self.session.verify = self.tls_verify

    def get_pods(self):
        url = "{0.oc_uri}/api/v1/namespaces/{0.project}/pods".format(self)
        response = self.session.get(url)
        response.raise_for_status()
        return response.json()

    def find_pod(self, data, containerid):
        for pod in data['items']:
            for status in pod[u'status'][u'containerStatuses']:
                if status[u'containerID'] == containerid:
                    return pod

    def handle(self, request):
        creds = request.get('creds')
        if creds is None:
            self.logger.debug('Missing "creds" on request')
            return None

        container = creds.get('container')
        if not container:
            self.logger.debug('Missing "container" on request')
            return None
        containerid = "{}://{}".format(*container)
        pods = self.get_pods()
        pod = self.find_pod(pods, containerid)
        if pod is None:
            self.logger.error('No pod for container %s found',
                              containerid)
            self.audit_svc_access(log.AUDIT_SVC_AUTHZ_FAIL,
                                  request['client_id'], request['path'])
            return False
        metadata = pod[u'metadata']
        self.logger.info(
            "Found pod %s for %s", metadata[u'selfLink'], containerid
        )
        hostname = metadata[u'annotations'].get(self.hostname_annotation)
        if not hostname:
            self.logger.error("No hostname configured for %s",
                              metadata[u'selfLink'])
            self.audit_svc_access(log.AUDIT_SVC_AUTHZ_FAIL,
                                  request['client_id'], request['path'])
            return False
        if hostname != request['path_chain'][-1]:
            self.logger.error(
                "Path %s does not match hostname '%s' for %s",
                request['path_chain'], hostname, containerid
            )
            self.audit_svc_access(log.AUDIT_SVC_AUTHZ_FAIL,
                                  request['client_id'], request['path'])
            return False

        self.logger.debug(
            "Container %s matches %s: '%s'",
            metadata[u'selfLink'], self.hostname_annotation, hostname
        )
        self.audit_svc_access(log.AUDIT_SVC_AUTHZ_PASS,
                              request['client_id'], request['path'])

        return True
Esempio n. 18
0
class IPAInterface(HTTPAuthenticator):
    """IPA interface authenticator

    Custodia uses a forking server model. We can bootstrap FreeIPA API in
    the main process. Connections must be created in the client process.
    """
    # Kerberos flags
    krb5config = PluginOption(str, None, "Kerberos krb5.conf override")
    keytab = PluginOption(str, None, "Kerberos keytab for auth")
    ccache = PluginOption(str, None,
                          "Kerberos ccache, e,g. FILE:/path/to/ccache")

    # ipalib.api arguments
    ipa_confdir = PluginOption(str, None, "IPA confdir override")
    ipa_context = PluginOption(str, "cli", "IPA bootstrap context")
    ipa_debug = PluginOption(bool, False, "debug mode for ipalib")

    # filled by gssapi()
    principal = False

    def __init__(self, config, section=None, api=None):
        super(IPAInterface, self).__init__(config, section)
        # only one instance of this plugin is supported
        if section != IPA_SECTIONNAME:
            raise ValueError(section)

        if api is None:
            self._api = ipalib.api
        else:
            self._api = api

        if self._api.isdone('bootstrap'):
            raise RuntimeError("IPA API already initialized")

        self._ipa_config = dict(
            context=self.ipa_context,
            debug=self.ipa_debug,
            log=None,  # disable logging to file
        )
        if self.ipa_confdir is not None:
            self._ipa_config['confdir'] = self.ipa_confdir

    @classmethod
    def from_config(cls, config):
        return config['authenticators']['ipa']

    def finalize_init(self, config, cfgparser, context=None):
        super(IPAInterface, self).finalize_init(config, cfgparser, context)

        if self.principal:
            # already initialized
            return

        # get rundir from own section or DEFAULT
        rundir = cfgparser.get(self.section, 'rundir', fallback=None)
        if rundir:
            self._ipa_config['dot_ipa'] = rundir
            self._ipa_config['home'] = rundir
            # workaround https://pagure.io/freeipa/issue/6761#comment-440329
            # monkey-patch ipalib.constants and all loaded ipa modules
            ipalib.constants.USER_CACHE_PATH = rundir
            for name, mod in six.iteritems(sys.modules):
                if (name.startswith(('ipalib.', 'ipaclient.'))
                        and hasattr(mod, 'USER_CACHE_PATH')):
                    mod.USER_CACHE_PATH = rundir

        self._gssapi_config()
        self._bootstrap()
        with self:
            self.logger.info("IPA server '%s': %s", self.env.server,
                             self.Command.ping()[u'summary'])

    def handle(self, request):
        request[IPA_SECTIONNAME] = self
        return None

    # rest is interface and initialization

    def _gssapi_config(self):
        # set client keytab env var for authentication
        if self.keytab is not None:
            os.environ['KRB5_CLIENT_KTNAME'] = self.keytab
        if self.ccache is not None:
            os.environ['KRB5CCNAME'] = self.ccache
        if self.krb5config is not None:
            os.environ['KRB5_CONFIG'] = self.krb5config

        self.principal = self._gssapi_cred()
        self.logger.info(u"Kerberos principal '%s'", self.principal)

    def _gssapi_cred(self):
        try:
            return get_principal()
        except Exception:
            self.logger.exception(
                "Unable to get principal from GSSAPI. Are you missing a "
                "TGT or valid Kerberos keytab?")
            raise

    def _bootstrap(self):
        # TODO: bandaid for "A PKCS #11 module returned CKR_DEVICE_ERROR"
        # https://github.com/avocado-framework/avocado/issues/1112#issuecomment-206999400
        os.environ['NSS_STRICT_NOFORK'] = 'DISABLED'
        self._api.bootstrap(**self._ipa_config)
        self._api.finalize()

    @property
    def Command(self):
        return self._api.Command  # pylint: disable=no-member

    @property
    def env(self):
        return self._api.env  # pylint: disable=no-member

    def __enter__(self):
        # pylint: disable=no-member
        self._gssapi_cred()
        if not self._api.Backend.rpcclient.isconnected():
            self._api.Backend.rpcclient.connect()
        # pylint: enable=no-member
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # pylint: disable=no-member
        if self._api.Backend.rpcclient.isconnected():
            self._api.Backend.rpcclient.disconnect()
Esempio n. 19
0
class Forwarder(HTTPConsumer):
    forward_uri = PluginOption(str, REQUIRED, None)
    tls_cafile = PluginOption(str, INHERIT_GLOBAL(None), 'Path to CA file')
    tls_certfile = PluginOption(str, None,
                                'Path to cert file for client cert auth')
    tls_keyfile = PluginOption(str, None,
                               'Path to key file for client cert auth')
    forward_headers = PluginOption('json', '{}', None)
    prefix_remote_user = PluginOption(bool, True, None)

    def __init__(self, config, section):
        super(Forwarder, self).__init__(config, section)
        self.client = CustodiaHTTPClient(self.forward_uri)
        if self.tls_certfile is not None:
            self.client.set_client_cert(self.tls_certfile, self.tls_keyfile)
        if self.tls_cafile is not None:
            self.client.set_ca_cert(self.tls_cafile)
        self.uuid = str(uuid.uuid4())
        # pylint: disable=unsubscriptable-object
        # pylint: disable=unsupported-assignment-operation
        self.forward_headers['X-LOOP-CUSTODIA'] = self.uuid

    def _path(self, request):
        trail = request.get('trail', [])
        if self.prefix_remote_user:
            prefix = [request.get('remote_user', 'guest').rstrip('/')]
        else:
            prefix = []
        return '/'.join(prefix + trail)

    def _headers(self, request):
        headers = {}
        headers.update(self.forward_headers)
        loop = request['headers'].get('X-LOOP-CUSTODIA', None)
        if loop is not None:
            headers['X-LOOP-CUSTODIA'] += ',' + loop
        return headers

    def _response(self, reply, response):
        if reply.status_code < 200 or reply.status_code > 299:
            raise HTTPError(reply.status_code)
        response['code'] = reply.status_code
        if reply.content:
            response['output'] = reply.content

    def _request(self, cmd, request, response, path, **kwargs):
        if self.uuid in request['headers'].get('X-LOOP-CUSTODIA', ''):
            raise HTTPError(502, "Loop detected")
        reply = cmd(path, **kwargs)
        self._response(reply, response)

    def GET(self, request, response):
        self._request(self.client.get,
                      request,
                      response,
                      self._path(request),
                      params=request.get('query', None),
                      headers=self._headers(request))

    def PUT(self, request, response):
        self._request(self.client.put,
                      request,
                      response,
                      self._path(request),
                      data=request.get('body', None),
                      params=request.get('query', None),
                      headers=self._headers(request))

    def DELETE(self, request, response):
        self._request(self.client.delete,
                      request,
                      response,
                      self._path(request),
                      params=request.get('query', None),
                      headers=self._headers(request))

    def POST(self, request, response):
        self._request(self.client.post,
                      request,
                      response,
                      self._path(request),
                      data=request.get('body', None),
                      params=request.get('query', None),
                      headers=self._headers(request))
Esempio n. 20
0
class EtcdStore(CSStore):
    etcd_server = PluginOption(str, '127.0.0.1', None)
    etcd_port = PluginOption(int, '4001', None)
    namespace = PluginOption(str, '/custodia', None)

    def __init__(self, config, section):
        super(EtcdStore, self).__init__(config, section)
        # Initialize the DB by trying to create the default table
        try:
            self.etcd = Client(self.etcd_server, self.etcd_port)
            self.etcd.write(self.namespace, None, dir=True)
        except EtcdNotFile:
            # Already exists
            pass
        except EtcdException:
            self.logger.exception("Error creating namespace %s",
                                  self.namespace)
            raise CSStoreError('Error occurred while trying to init db')

    def _absolute_key(self, key):
        """Get absolute path to key and validate key"""
        if '//' in key:
            raise ValueError("Invalid empty components in key '%s'" % key)
        parts = key.split('/')
        if set(parts).intersection({'.', '..'}):
            raise ValueError("Invalid relative components in key '%s'" % key)
        return '/'.join([self.namespace] + parts).replace('//', '/')

    def get(self, key):
        self.logger.debug("Fetching key %s", key)
        try:
            result = self.etcd.get(self._absolute_key(key))
        except EtcdException:
            self.logger.exception("Error fetching key %s", key)
            raise CSStoreError('Error occurred while trying to get key')
        self.logger.debug("Fetched key %s got result: %r", key, result)
        return result.value  # pylint: disable=no-member

    def set(self, key, value, replace=False):
        self.logger.debug("Setting key %s to value %s (replace=%s)",
                          key, value, replace)
        path = self._absolute_key(key)
        try:
            self.etcd.write(path, value, prevExist=replace)
        except EtcdAlreadyExist as err:
            raise CSStoreExists(str(err))
        except EtcdException:
            self.logger.exception("Error storing key %s", key)
            raise CSStoreError('Error occurred while trying to store key')

    def span(self, key):
        path = self._absolute_key(key)
        self.logger.debug("Creating directory %s", path)
        try:
            self.etcd.write(path, None, dir=True, prevExist=False)
        except EtcdAlreadyExist as err:
            raise CSStoreExists(str(err))
        except EtcdException:
            self.logger.exception("Error storing key %s", key)
            raise CSStoreError('Error occurred while trying to store key')

    def list(self, keyfilter='/'):
        path = self._absolute_key(keyfilter)
        if path != '/':
            path = path.rstrip('/')
        self.logger.debug("Listing keys matching %s", path)
        try:
            result = self.etcd.read(path, recursive=True)
        except EtcdKeyNotFound:
            return None
        except EtcdException:
            self.logger.exception("Error listing %s", keyfilter)
            raise CSStoreError('Error occurred while trying to list keys')
        self.logger.debug("Searched for %s got result: %r", path, result)
        value = set()
        for entry in result.get_subtree():
            if entry.key == path:
                continue
            name = entry.key[len(path):]
            if entry.dir and not name.endswith('/'):
                name += '/'
            value.add(name.lstrip('/'))
        return sorted(value)

    def cut(self, key):
        self.logger.debug("Removing key %s", key)
        try:
            self.etcd.delete(self._absolute_key(key))
        except EtcdKeyNotFound:
            self.logger.debug("Key %s not found", key)
            return False
        except EtcdException:
            self.logger.exception("Error removing key %s", key)
            raise CSStoreError('Error occurred while trying to cut key')
        self.logger.debug("Key %s removed", key)
        return True