Skip to content

Commit

Permalink
prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
jameshawkes committed Mar 5, 2024
1 parent b6d8d2f commit 7bf4a0d
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 5 deletions.
10 changes: 5 additions & 5 deletions admin/polytope-admin/polytope_admin/api/Auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,12 @@ def fetch_key(self, login=True):
email = self.read_email
self._logger.info("Polytope user key found in session cache for user " + config["username"])
else:
key_file = Path(config["key_path"]) / config["username"]
key_file = Path(config["key_path"])
try:
with open(str(key_file), "r") as infile:
info = json.load(infile)
key = info["key"]
email = info["email"]
key = info["user_key"]
email = info["user_email"]
except FileNotFoundError:
key = None
email = None
Expand Down Expand Up @@ -190,7 +190,7 @@ def persist(self, key, email, username=None):
if not username:
username = config["username"]
os.makedirs(config["key_path"], exist_ok=True)
key_file = Path(config["key_path"]) / username
key_file = Path(config["key_path"])
with open(str(key_file), "w", encoding="utf8") as outfile:
json.dump({"key": key, "email": email}, outfile)
self.read_key = key
Expand All @@ -214,7 +214,7 @@ def erase(self, username=None):
config = self.config.get()
if not username:
username = config["username"]
key_path = Path(config["key_path"]) / username
key_path = Path(config["key_path"])
try:
os.remove(str(key_path))
self._logger.info("Credentials removed for " + username)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#
# Copyright 2022 European Centre for Medium-Range Weather Forecasts (ECMWF)
#
# 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.
#
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation nor
# does it submit to any jurisdiction.
#

import base64
import logging
import os

from keycloak import KeycloakOpenID
from keycloak.exceptions import KeycloakConnectionError

from ..auth import User
from ..caching import cache
from ..exceptions import ForbiddenRequest
from . import authentication


class KeycloakBearerAuthentication(authentication.Authentication):
def __init__(self, name, realm, config):
self.config = config

# URL of the keycloak API: e.g. https://keycloak.insitute.org/auth/"
self.url = config["url"]

# Keycloak client id and secret
self.client_id = config["client_id"] # e.g. polytope
self.client_secret = config["client_secret"]

# The keycloak realm to look for users
self.keycloak_realm = config["keycloak_realm"]

self.skipTLS = config.get("skip_tls", False)

# Connection parameters
self.timeout = config.get("timeout", 3)

# Mapping user attributes to keycloak attributes
self.attribute_map = config.get("attributes", {})

super().__init__(name, realm, config)

def authentication_type(self):
return "Basic"

def authentication_info(self):
return "Authenticate with Keycloak username and password"

@cache(lifetime=120)
def authenticate(self, credentials: str) -> User:

# credentials should be of the form 'base64(<username>:<API_key>)'
try:
decoded = base64.b64decode(credentials).decode("utf-8")
auth_user, auth_password = decoded.split(":", 1)
except UnicodeDecodeError:
raise ForbiddenRequest("Credentials could not be decoded")
except ValueError:
raise ForbiddenRequest("Credentials could not be unpacked")

_environ = dict(os.environ)
try:
os.environ["http_proxy"] = os.getenv("POLYTOPE_PROXY", "")
os.environ["https_proxy"] = os.getenv("POLYTOPE_PROXY", "")

logging.debug("Setting HTTPS_PROXY to {}".format(os.environ["https_proxy"]))

try:

# Open a session as a registered client
client = KeycloakOpenID(
server_url=self.url,
client_id=self.client_id,
realm_name=self.keycloak_realm,
client_secret_key=self.client_secret,
verify=(self.skipTLS is False),
)

client.connection.timeout = self.timeout

# Obtain a session token on behalf of the user
token = client.token(auth_user, auth_password)

except KeycloakConnectionError:
# Raise ForbiddenRequest rather than ServerError so that we are not blocked if Keycloak is down
raise ForbiddenRequest("Could not connect to Keycloak")
except Exception:
raise ForbiddenRequest("Invalid Keycloak credentials")

userinfo = client.userinfo(token["access_token"])

user = User(auth_user, self.realm())

logging.debug("Found user {} in keycloak".format(auth_user))

for k, v in self.attribute_map.items():
if v in userinfo:
user.attributes[k] = userinfo[v]
logging.debug("User {} has attribute {} : {}".format(user.username, k, user.attributes[k]))

return user

finally:
os.environ.clear()
os.environ.update(_environ)

def collect_metric_info(self):
return {}

0 comments on commit 7bf4a0d

Please sign in to comment.