1+ from http .client import HTTPException
2+ import os
13from flask import g
24from flask_appbuilder .security .sqla .models import (Role , User )
35from opa_client .opa import OpaClient
46from superset .security .manager import SupersetSecurityManager
5- from typing import (Optional , List )
7+ from superset import conf
8+ from typing import (Optional , List , Tuple )
69
710import logging
811
912# logger = logging.get_logger(__name__)
10-
11- """
12- We want OPA to sync roles.
13- 1. Role Sync via OPA
14- 2. Automated sync ( how and where to sync configurable [Decision Role sync policy] )
15- 3. CRD option to turn sync on, off. [ ~Decision~ Standardized through op-rs ]
16- 4. CRD option for auto delete roles from OPA and how ( Prefix maybe ) [ Decision ]
17- --> Maybe we don't want that as we could reach permission states in dashboards, charts etc. which
18- had been RBAC but now the role is gone. What now? Unsecure state.
19- 5. Come up with a patch process for such things ( @Lars )
20- """
2113class OpaSupersetSecurityManager (SupersetSecurityManager ):
2214
23- """
24- This is called:
25- as get_user_permissions() in FlaskApplicationBuilder
26- - bootstrap_user_data() in superset views (REST APIs)
27- as get_user_roles
28- - get_rls_filter() -> row-level filter on tables
29- - dashboard rbac filter
30- - is_admin() -> used in many places as admin role in special
31-
32- Important!
33- user.roles can also be called directly, looks like you don't have to use the getter...
34-
35- Seems to not use user.roles:
36- - resource ownership (looks at owner attribute, not roles)
37- """
3815 def get_user_roles (self , user : Optional [User ] = None ) -> List [Role ]:
3916 if not user :
4017 user = g .user
18+
19+ default_role = self .resolve_role (self .get_default_role ())
4120
42- # TODO: Let the operator configure host and port
43- client = OpaClient (host = 'simple-opa' , port = 8081 )
44- response = client .query_rule (
45- input_data = {'username' : user .username },
46- package_path = 'superset' ,
47- rule_name = 'user_roles' )
48- logging .info (f'Query: { response } ' )
49- role_names = response ['result' ]
50- logging .info (f'found opa roles: { role_names } ' )
51- roles = list (map (self .find_role , role_names ))
21+ opa_role_names = self .get_opa_user_roles (user .username )
22+ logging .info (f'OPA returned roles: { opa_role_names } ' )
5223
53- # fairly primitive check if roles are already in database
54- # TODO: Sophisticate
55- for i , role in enumerate (roles ):
56- if role == None :
57- logging .info (f'Found None: { role } , adding role { role_names [i ]} ' )
58- self .add_role (role_names [i ])
24+ opa_roles = set (map (self .resolve_role , opa_role_names ))
25+ # Ensure that in case of a bad or no reponse from OPA each user will have at least one role.
26+ opa_roles .add (default_role )
27+
28+ if set (user .roles ) != opa_roles :
29+ logging .info (f'Found diff in { user .roles } vs. { opa_roles } ' )
30+ user .roles = list (opa_roles )
31+ self .update_user (user )
5932
60- roles = list (map (self .find_role , role_names ))
33+ return user .roles
34+
6135
62- # TODO: See if you want to delete roles and how
63- if set (user .roles ) != set (roles ):
64- logging .info (f'found diff in { user .roles } vs. { roles } ' )
65- user .roles = roles
66- self .update_user (user )
36+ def get_opa_user_roles (self , username : str ) -> set [str ]:
37+ """
38+ Queries an Open Policy Agent instance for the roles of a given user and returns a list of role names.
39+ Returns an empty list if an exception during the connection to Open Policy Agent is encountered or if the query result
40+ is not a list.
41+ """
42+ host , port , tls = self .resolve_opa_endpoint ()
43+ print (host )
44+ print (port )
45+ print (tls )
46+ client = OpaClient (host = host , port = port , ssl = tls )
47+ try :
48+ response = client .query_rule (
49+ input_data = {'username' : username },
50+ package_path = 'superset' ,
51+ rule_name = 'user_roles' )
52+ except HTTPException as e :
53+ logging .error (f'Encountered an exception while querying OPA:\n { e } ' )
54+ return []
55+ roles = response .get ('result' )
56+ # If OPA didn't return a result or if the result is not a list, return no roles.
57+ if roles is None or type (roles ).__name__ != "list" :
58+ logging .error (f'The OPA query didn\' t return a list: { response } ' )
59+ return []
60+ return roles
61+
6762
68- logging .info (f'found user roles: { user .roles } ' )
63+ def resolve_opa_endpoint (self ) -> Tuple [str , int , bool ]:
64+ opa_endpoint = os .getenv ('STACKABLE_OPA_ENDPOINT' )
65+ [protocol , host , port ] = opa_endpoint .split (":" )
66+ return host .lstrip ('/' ), int (port .rstrip ('/' )), protocol == 'https'
67+
6968
70- return roles
69+ def resolve_role (self , role_name : str ) -> Role :
70+ role = self .find_role (role_name )
71+ if role is None :
72+ logging .info (f'Creating role { role_name } as it doesn\' t already exist.' )
73+ self .add_role (role_name )
74+ return self .find_role (role_name )
75+
76+
77+ def get_default_role (self ) -> str :
78+ return conf ["AUTH_USER_REGISTRATION_ROLE" ] if conf ["AUTH_USER_REGISTRATION_ROLE" ] else "Public"
0 commit comments