From 9c30a9497e6f2f5a56a461eaf7e1123cb96823c0 Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Thu, 29 Dec 2016 20:22:55 +0300 Subject: [PATCH] Add keycloak auth support Change-Id: Ie1b1a6467991f3c2cff6cc823f73103655844452 --- etc/glare-paste.ini | 7 ++ glare/api/middleware/context.py | 3 +- glare/api/middleware/keycloak_auth.py | 134 ++++++++++++++++++++++++++ glare/opts.py | 2 + requirements.txt | 1 + 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 glare/api/middleware/keycloak_auth.py diff --git a/etc/glare-paste.ini b/etc/glare-paste.ini index 5b19e9d..c85fdd2 100644 --- a/etc/glare-paste.ini +++ b/etc/glare-paste.ini @@ -6,6 +6,10 @@ pipeline = cors faultwrapper healthcheck http_proxy_to_wsgi versionnegotiation o [pipeline:glare-api-keystone] pipeline = cors faultwrapper healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken context glarev1api +# Use this pipeline for Keycloak auth +[pipeline:glare-api-keystone] +pipeline = cors faultwrapper healthcheck http_proxy_to_wsgi versionnegotiation osprofiler keycloak context glarev1api + [app:glarev1api] paste.app_factory = glare.api.v1.router:API.factory @@ -30,6 +34,9 @@ paste.filter_factory = glare.api.middleware.context:UnauthenticatedContextMiddle paste.filter_factory = keystonemiddleware.auth_token:filter_factory delay_auth_decision = true +[filter:keycloak] +paste.filter_factory = glare.api.middleware.keycloak_auth:KeycloakAuthMiddleware.factory + [filter:osprofiler] paste.filter_factory = osprofiler.web:WsgiMiddleware.factory diff --git a/glare/api/middleware/context.py b/glare/api/middleware/context.py index 2f85583..35fb34e 100644 --- a/glare/api/middleware/context.py +++ b/glare/api/middleware/context.py @@ -21,6 +21,7 @@ from oslo_middleware import base as base_middleware from oslo_middleware import request_id from oslo_serialization import jsonutils +from glare.common import exception from glare.common import policy from glare.i18n import _ @@ -84,7 +85,7 @@ class ContextMiddleware(base_middleware.ConfigurableMiddleware): elif CONF.allow_anonymous_access: req.context = ContextMiddleware._get_anonymous_context() else: - raise webob.exc.HTTPUnauthorized() + raise exception.Unauthorized() @staticmethod def _get_anonymous_context(): diff --git a/glare/api/middleware/keycloak_auth.py b/glare/api/middleware/keycloak_auth.py new file mode 100644 index 0000000..3eead9b --- /dev/null +++ b/glare/api/middleware/keycloak_auth.py @@ -0,0 +1,134 @@ +# Copyright 2010 OpenStack Foundation +# All Rights Reserved. +# +# 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 memcache +from oslo_config import cfg +from oslo_log import log as logging +import pprint +import requests +import webob.dec + +from glare.common import exception + +LOG = logging.getLogger(__name__) + +keycloak_oidc_opts = [ + cfg.StrOpt( + 'auth_url', + help='Keycloak base url (e.g. https://my.keycloak:8443/auth)' + ), + cfg.StrOpt( + 'insecure', + default=False, + help='If True, SSL/TLS certificate verification is disabled' + ), + cfg.StrOpt( + 'memcached_server', + default=None, + help='Url of memcached server to use for caching' + ), + cfg.IntOpt( + 'token_cache_time', + default=60, + min=0, + help='In order to prevent excessive effort spent validating ' + 'tokens, the middleware caches previously-seen tokens ' + 'for a configurable duration (in seconds).' + ), +] + +CONF = cfg.CONF +CONF.register_opts(keycloak_oidc_opts, group="keycloak_oidc") + + +class KeycloakAuthMiddleware(object): + def __init__(self, app): + self.app = app + mcserv_url = CONF.memcached_server + self.mcclient = memcache.Client(mcserv_url) if mcserv_url else None + + def authenticate(self, request): + realm_name = request.headers.get('X-Project-Id') + + user_info_endpoint = ( + "%s/realms/%s/protocol/openid-connect/userinfo" % + (CONF.keycloak_oidc.auth_url, realm_name) + ) + + access_token = request.headers.get('X-Auth-Token') + + info = None + if self.mcclient: + info = self.mcclient.get(access_token) + + if info is None: + resp = requests.get( + user_info_endpoint, + headers={"Authorization": "Bearer %s" % access_token}, + verify=not CONF.keycloak_oidc.insecure + ) + resp.raise_for_status() + if self.mcclient: + self.mcclient.set(access_token, resp.json(), + time=CONF.token_cache_time) + info = resp.json() + + LOG.debug( + "HTTP response from OIDC provider: %s" % + pprint.pformat(info) + ) + + return info + + def get_roles(self, request): + realm_name = request.headers.get('X-Project-Id') + + user_roles_endpoint = ( + "%s/realms/%s/roles" % + (CONF.keycloak_oidc.auth_url, realm_name) + ) + + access_token = request.headers.get('X-Auth-Token') + + roles = None + if self.mcclient: + roles = self.mcclient.get(realm_name) + + if roles is None: + resp = requests.get( + user_roles_endpoint, + headers={"Authorization": "Bearer %s" % access_token} + ) + roles = [role['name'] for role in resp.json()] + if self.mcclient: + self.mcclient.set(realm_name, roles, + time=CONF.token_cache_time) + + LOG.debug( + "Roles for realm %s: %s" % + (realm_name, pprint.pformat(roles)) + ) + + return roles + + @webob.dec.wsgify + def __call__(self, request): + if 'X-Project-Id' not in request.headers: + raise exception.Unauthorized() + self.authenticate(request) + roles = ','.join(self.get_roles(request)) + request.headers["X-Identity-Status"] = "Confirmed" + request.headers["X-Roles"] = roles + return request.get_response(self.app) diff --git a/glare/opts.py b/glare/opts.py index 0220f30..2043dde 100644 --- a/glare/opts.py +++ b/glare/opts.py @@ -22,6 +22,7 @@ import itertools from osprofiler import opts as profiler import glare.api.middleware.context +import glare.api.middleware.keycloak_auth import glare.api.v1.resource import glare.api.versions import glare.common.config @@ -33,6 +34,7 @@ import glare.objects.meta.registry _artifacts_opts = [ (None, list(itertools.chain( glare.api.middleware.context.context_opts, + glare.api.middleware.keycloak_auth.keycloak_oidc_opts, glare.api.v1.resource.list_configs, glare.api.versions.versions_opts, glare.common.config.common_opts, diff --git a/requirements.txt b/requirements.txt index ea630e8..0f30412 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ oslo.utils>=3.18.0 # Apache-2.0 futurist!=0.15.0,>=0.11.0 # Apache-2.0 keystoneauth1>=2.18.0 # Apache-2.0 keystonemiddleware>=4.12.0 # Apache-2.0 +python-memcached>=1.56 # PSF WSME>=0.8 # MIT # For paste.util.template used in keystone.common.template