11# pylint: disable=missing-module-docstring
22import logging
33
4- from http .client import HTTPException
5- from typing import List , Optional , Tuple
4+ from typing import List , Optional
65from cachetools import cachedmethod , TTLCache
76from flask import current_app , g
87from flask_appbuilder .security .sqla .models import (
98 Role ,
109 User ,
1110)
12- from opa_client . opa import OpaClient
11+ import requests
1312from superset .security import SupersetSecurityManager
1413
1514
@@ -21,6 +20,9 @@ class OpaSupersetSecurityManager(SupersetSecurityManager):
2120 AUTH_OPA_CACHE_MAXSIZE_DEFAULT = 1000
2221 AUTH_OPA_CACHE_TTL_IN_SEC_DEFAULT = 30
2322 AUTH_OPA_REQUEST_URL_DEFAULT = "http://opa:8081/"
23+ AUTH_OPA_REQUEST_TIMEOUT_DEFAULT = 10
24+ AUTH_OPA_PACKAGE_DEFAULT = "superset"
25+ AUTH_OPA_RULE_DEFAULT = "user_roles"
2426
2527 def __init__ (self , appbuilder ):
2628 self .appbuilder = appbuilder
@@ -34,6 +36,7 @@ def __init__(self, appbuilder):
3436 "AUTH_OPA_CACHE_TTL_IN_SEC" , self .AUTH_OPA_CACHE_TTL_IN_SEC_DEFAULT
3537 ),
3638 )
39+ self .opa_session = requests .Session ()
3740
3841 def get_user_roles (self , user : Optional [User ] = None ) -> List [Role ]:
3942 """
@@ -78,37 +81,41 @@ def get_opa_user_roles(self, username: str) -> set[str]:
7881 :returns: A list of role names or an empty list if an exception during
7982 the connection to OPA is encountered or if OPA didn't return a list.
8083 """
81- host , port , tls = self .resolve_opa_base_url ()
82- client = OpaClient (host = host , port = port , ssl = tls )
83- try :
84- response = client .query_rule (
85- input_data = {"username" : username },
86- package_path = current_app .config .get ("STACKABLE_OPA_PACKAGE" ),
87- rule_name = current_app .config .get ("STACKABLE_OPA_RULE" ),
88- )
89- except HTTPException as exception :
90- logging .error ("Encountered an exception while querying OPA:%s" , exception )
84+
85+ opa_url = current_app .config .get (
86+ "AUTH_OPA_REQUEST_URL" , self .AUTH_OPA_REQUEST_URL_DEFAULT
87+ )
88+ package = current_app .config .get (
89+ "AUTH_OPA_PACKAGE" , self .AUTH_OPA_PACKAGE_DEFAULT
90+ )
91+ rule = current_app .config .get ("AUTH_OPA_RULE" , self .AUTH_OPA_RULE_DEFAULT )
92+ timeout = current_app .config .get (
93+ "AUTH_OPA_REQUEST_TIMEOUT" , self .AUTH_OPA_REQUEST_TIMEOUT_DEFAULT
94+ )
95+ input = {"input" : {"username" : username }}
96+ response = self .call_opa (
97+ url = f"{ opa_url } v1/data/{ package } /{ rule } " ,
98+ json = input ,
99+ timeout = timeout ,
100+ )
101+
102+ if response .status_code is None or response .status_code != 200 :
103+ logging .error ("Error while querying OPA." )
91104 return []
92- roles = response .get ("result" )
105+
106+ roles = response .json ().get ("result" )
93107 # If OPA didn't return a result or if the result is not a list, return no roles.
94108 if roles is None or type (roles ).__name__ != "list" :
95- logging .error ("The OPA query didn't return a list: %s" , response )
109+ logging .error ("The OPA query didn't return a list: %s" , response . json () )
96110 return []
97111 return roles
98112
99- def resolve_opa_base_url (self ) -> Tuple [str , int , bool ]:
100- """
101- Extracts connection parameters of an Open Policy Agent instance from config.
102-
103- :returns: Hostname, port and protocol (http/https).
104- """
105- opa_base_path = current_app .config .get (
106- "STACKABLE_OPA_BASE_URL" , self .AUTH_OPA_REQUEST_URL_DEFAULT
113+ def call_opa (self , url : str , json : dict , timeout : int ) -> requests .Response :
114+ return self .opa_session .post (
115+ url = url ,
116+ json = json ,
117+ timeout = timeout ,
107118 )
108- [protocol , host , port ] = opa_base_path .split (":" )
109- # remove any path be appended to the base url
110- port = int (port .split ("/" )[0 ])
111- return host .lstrip ("/" ), port , protocol == "https"
112119
113120 def resolve_role (self , role_name : str ) -> Role :
114121 """
0 commit comments