| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you 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. |
| # |
| from base64 import b64decode, b64encode |
| from urllib.parse import quote, unquote |
| from openserverless.couchdb.couchdb_util import CouchDB |
| from openserverless.error.api_error import EncodeError, DecodeError, AuthorizationError |
| |
| import json |
| import os |
| import logging |
| |
| USER_META_DBN = "users_metadata" |
| SUBJECT_META_DBN = "subjects" |
| |
| |
| class OpenwhiskAuthorize: |
| |
| def __init__(self, environ=os.environ): |
| self._db = CouchDB() |
| self._environ = environ |
| |
| def encode(self, username, password): |
| """Returns an HTTP basic authentication encrypted string given a valid |
| username and password. |
| """ |
| if ":" in username: |
| raise EncodeError |
| |
| username_password = f"{quote(username)}:{quote(password)}" |
| return f"Basic {b64encode(username_password.encode()).decode()}" |
| |
| def _parse_b64(self, encoded_str): |
| """ |
| Parse a base64 encoded string and return the username and password. |
| If the string is not base64 encoded, it will try to split it by ':'. |
| Raises DecodeError if the string cannot be decoded or parsed. |
| >>> oa = OpenwhiskAuthorize() |
| >>> oa._parse_b64("dXNlcm5hbWU6cGFzc3dvcmQ=") |
| ('username', 'password') |
| >>> oa._parse_b64("username:password") |
| ('username', 'password') |
| >>> oa._parse_b64("invalid_base64_string") |
| Traceback (most recent call last): |
| ... |
| openserverless.error.api_error.DecodeError: authentication token does not seems to be b64 encoded |
| """ |
| username = None |
| password = None |
| try: |
| decoded = b64decode(encoded_str) |
| |
| credentials = decoded.decode() |
| if credentials.count(":") != 1: |
| raise DecodeError("authentication token does not seems to be b64 encoded") |
| username, password = credentials.split(":", 1) |
| except: |
| # fallback in case the token is not bas64 encoded |
| if encoded_str.count(":") == 1: |
| username, password = encoded_str.split(":", 1) |
| |
| if not username or not password: |
| raise DecodeError("authentication token does not seems to be b64 encoded") |
| |
| return username, password |
| |
| def decode(self, encoded_str): |
| """Decode an encrypted HTTP basic authentication string. Returns a tuple of |
| the form (username, password), and raises a DecodeError exception if |
| nothing could be decoded. |
| >>> oa = OpenwhiskAuthorize() |
| >>> oa.decode("Basic dXNlcm5hbWU6cGFzc3dvcmQ=") |
| ('username', 'password') |
| >>> oa.decode("dXNlcm5hbWU6cGFzc3dvcmQ=") |
| ('username', 'password') |
| >>> oa.decode("invalid_base64_string") |
| Traceback (most recent call last): |
| ... |
| openserverless.error.api_error.DecodeError: authentication token does not seems to be b64 encoded |
| """ |
| split = encoded_str.strip().split(" ") |
| |
| # If split is only one element, try to decode the username and password |
| # directly. |
| if len(split) == 1: |
| username, password = self._parse_b64(split[0]) |
| |
| # If there are only two elements, check the first and ensure it says |
| # 'basic' so that we know we're about to decode the right thing. If not, |
| # bail out. |
| elif len(split) == 2: |
| if split[0].strip().lower() == "basic": |
| username, password = self._parse_b64(split[1]) |
| else: |
| raise DecodeError( |
| "authentication token provides more than 2 elements. could not parse properly" |
| ) |
| |
| # If there are more than 2 elements, something crazy must be happening. |
| # Bail. |
| else: |
| raise DecodeError("unpredictable error parsing authentication token") |
| |
| return unquote(username), unquote(password) |
| |
| def fetch_subject(self, uuid: str, key: str): |
| """ |
| Query the internal couchdb searching for the subject matching the given uuid, key. |
| Normally these stored in wsk or wsku in the form uuid:key |
| :param uuid the OW subject uuid |
| :param key the OW subject key |
| :return a ubject document |
| """ |
| logging.info(f"searching for openwhisk subject {uuid}") |
| try: |
| selector = { |
| "selector": {"namespaces": {"$elemMatch": {"uuid": uuid, "key": key}}} |
| } |
| |
| response = self._db.find_doc(SUBJECT_META_DBN, json.dumps(selector)) |
| |
| if response["docs"]: |
| docs = list(response["docs"]) |
| if len(docs) > 0: |
| logging.debug( |
| f"OpenServerless namespace for user {uuid} found. Returning Result." |
| ) |
| return docs[0] |
| |
| logging.warning(f"OpenServerless metadata for user {uuid} not found!") |
| return None |
| except Exception as e: |
| logging.error( |
| f"failed to query OpenServerless metadata for user {uuid}. Reason: {e}" |
| ) |
| return None |
| |
| def fetch_user_data(self, username: str): |
| """ |
| Query the internal couchdb searching for the given principal to retrieve all the |
| relevant metadata |
| """ |
| logging.info(f"searching for user {username} meta-data") |
| try: |
| selector = {"selector": {"login": {"$eq": username}}} |
| response = self._db.find_doc(USER_META_DBN, json.dumps(selector)) |
| |
| if response["docs"]: |
| docs = list(response["docs"]) |
| if len(docs) > 0: |
| logging.debug( |
| f"OpenServerless metadata for user {username} found. Returning Result." |
| ) |
| return docs[0] |
| |
| logging.warning(f"OpenServerless metadata for user {username} not found!") |
| return None |
| except Exception as e: |
| logging.error( |
| f"failed to query OpenServerless metadata for user {username}. Reason: {e}" |
| ) |
| return None |
| |
| def login(self, authorization: str): |
| """ |
| Attempt to login the user identified by the given Openwhisk authorization AUTH token as base64 |
| param: authorization a base64 encoded OpenWhisk AUTH entries |
| """ |
| uuid, key = self.decode(authorization) |
| subject = self.fetch_subject(uuid, key) |
| |
| if not subject: |
| raise AuthorizationError("Openwhisk subject not found.") |
| |
| user_data = self.fetch_user_data(subject["subject"]) |
| |
| if user_data: |
| return user_data |
| |
| raise AuthorizationError("Could not retrieve user metadata.") |
| |
| def subject_login(self, authorization: str): |
| """ |
| Attempt to login the user identified by the given Openwhisk authorization AUTH token as base64 |
| param: authorization a base64 encoded OpenWhisk AUTH entries |
| return: the subject entry |
| """ |
| uuid, key = self.decode(authorization) |
| subject = self.fetch_subject(uuid, key) |
| |
| if not subject: |
| raise AuthorizationError("Openwhisk subject not found.") |
| |
| return subject |