from enum import Enum __all__ = [ "registered_claims", "BaseAuthClaim", ] registered_claims = [] class RegisterAuthClaimMeta(type): """ Metaclass to add subclassed claims to registered_claims. Intended for later convenient usage when parsing the token data. """ def __new__(mcs, name, bases, dct): cls = super().__new__(mcs, name, bases, dct) if bases: registered_claims.append(cls) return cls class BaseAuthClaim(metaclass=RegisterAuthClaimMeta): """ Base class to define auth claims. The main idea is to use claims classes over the code to decouple the code from token data. - `path` - path inside the app namespace - `value_field` - fiend with actual value inside the json claim value - `Values` - allowed values enum for filter and validation of claims that are interested for current app - `is_reserved` - mark if the claim is reserved (e.g. aud, iss, etc.) """ class Values(Enum): """Possible values for claim.""" @classmethod def list(cls): return list(map(lambda c: c.value, cls)) is_reserved = False # May be any field from claim value json, e.g. `id`, `slug` etc. value_field = "slug" @property def path(self): """ Related path inside the app namespace. Represents what we have in a ClaimName.path """ raise NotImplementedError @property def value(self): return self._value @property def id(self): return self.path, self.value @classmethod def from_token_value(cls, value): """ Generic factory that use specific factory, based on value type. """ if isinstance(value, list): obj = cls.from_list(value) elif isinstance(value, dict): obj = cls.from_dict(value) else: obj = cls.from_str(value) return obj @classmethod def from_dict(cls, claim_dict: dict): """ Creates object form dict. Throws ValueError if needed value is not found, is not allowed or the claim dict has wrong type """ if not isinstance(claim_dict, dict): raise ValueError("Expecting a dict") value = claim_dict.get(cls.value_field) return cls(cls.Values(value)) @classmethod def from_list(cls, claims_list: list): """ Creates object form list of dicts or strings. Throws ValueError if needed value is not found, is not allowed or the claims list has wrong type. Omits wrong list values. """ if not isinstance(claims_list, list): raise ValueError("Expecting a list") values = [] for claim_item in claims_list: if isinstance(claim_item, dict): try: values.append(claim_item[cls.value_field]) except Exception: # nosec pass # nosec elif cls.value_field is None: values.append(claim_item) if values and issubclass(cls.Values, Enum): return cls(cls.Values(values[0])) return cls(cls.Values(values)) @classmethod def from_str(cls, claim_string): """ Creates object form list of dicts. Throws ValueError if needed value is not found, is not allowed or the claims list has wrong type. Omits wrong list values. """ if not isinstance(claim_string, str): raise ValueError("Expecting a str") return cls(cls.Values(claim_string)) def __init__(self, value: Values): self._value = value def __str__(self): return f"{self.path}/{self.value.name}" def __eq__(self, other): if not isinstance(other, BaseAuthClaim): return False return self.id == other.id def __hash__(self): return hash(self.id)