verdan / flaskoidc

A wrapper of Flask with pre-configured OIDC support
Apache License 2.0
52 stars 35 forks source link

[Feature request] Give way for injecting non-string configs from environment #5

Closed pPanda-beta closed 3 years ago

pPanda-beta commented 3 years ago

You have recently introduced generic env variable based configuration injection at https://github.com/verdan/flaskoidc/blob/3400bc399648e8f54cdb9b7222d8d5458b2ac162/flaskoidc/config.py#L40-L48

This is instant relief to pass n no of configs directly to flask-oidc. But flask app often needs configs of various types. Example

  1. Config of non string type. OIDC_ID_TOKEN_COOKIE_TTL
  2. Config of dict type. OIDC_CLIENT_SECRETS (it is union type of either string, i.e. json file path or dict holding the actual configs)
  3. A python class/function/object. CUSTOM_SECURITY_MANAGER

Suggestions:

Recently I was implementing a purely env variable based keycloak integration for apache superset, and built something similar.

A snippet of the environment variables looked like :


  ENV_SERIALIZED_VARS: OAUTH_PROVIDERS, AUTH_USER_REGISTRATION, OPENID_PROVIDERS, OIDC_CLIENT_SECRETS, ENABLE_PROXY_FIX
  ENV_PYTHON_IMPORT_VARS: CUSTOM_SECURITY_MANAGER, AUTH_TYPE

  SUPERSET__ENABLE_PROXY_FIX: "true"
  SUPERSET__CUSTOM_SECURITY_MANAGER: fab_oidc.security:SupersetOIDCSecurityManager
  SUPERSET__AUTH_TYPE: flask_appbuilder.security.manager:AUTH_OID

  SUPERSET__AUTH_USER_REGISTRATION: "true"
  SUPERSET__AUTH_USER_REGISTRATION_ROLE: {{ .Values.auth.initialRole }}

  SUPERSET__OIDC_CLIENT_SECRETS: |+
    {
      "web": {
        "issuer": "{{ .Values.keycloak.authServerUrl }}",
     ... # omitted for brevity 
      }
    }

The shortcut code that I had added into superset is something like :


class EnvInjector:
    ENV_VAR_PATTERN = r"SUPERSET__(?P<KEY>.*)"
    SERIALIZED_FIELDS = re.split(r"\s+|,\s*", os.getenv("ENV_SERIALIZED_VARS", ""))
    PYTHON_IMPORT_FIELDS = re.split(r"\s+|,\s*", os.getenv("ENV_PYTHON_IMPORT_VARS", ""))

    @classmethod
    def is_superset_config_key(cls, key: str) -> bool:
        return re.match(cls.ENV_VAR_PATTERN, key) is not None

    @classmethod
    def extract_key(cls, key: str) -> str:
        return re.search(cls.ENV_VAR_PATTERN, key).group('KEY')

    @classmethod
    def extract_value(cls, prefixed_key: str, value: str) -> object:
        import json
        from werkzeug.utils import import_string

        key = cls.extract_key(prefixed_key)
        if key in cls.SERIALIZED_FIELDS or prefixed_key in cls.SERIALIZED_FIELDS:
            return json.loads(value)
        if key in cls.PYTHON_IMPORT_FIELDS or prefixed_key in cls.PYTHON_IMPORT_FIELDS:
            return import_string(value)
        return value

    @classmethod
    def get_configs(cls) -> dict:
        return {cls.extract_key(k): cls.extract_value(k, v)
                for k, v in os.environ.items()
                if cls.is_superset_config_key(k)}

    @classmethod
    def inject_into(cls, obj):
        configs = cls.get_configs()
        for k, v in configs.items():
            setattr(obj, k, v)

# In case of seuperset, config is the entire module. In case of flaskoidc it is just an object

import sys
__module__ = sys.modules[__name__]
EnvInjector.inject_into(__module__)
pPanda-beta commented 3 years ago

/keep_fresh

verdan commented 3 years ago

@pPanda-beta I am moving away from the flask-oidc package entirely, in favor of Authlib, and I don't think this issue is valid anymore. please make sure to check out the new version, it's much more cleaner.