Skip to content

Commit 1a15b1b

Browse files
committed
create patch for superset-opa
1 parent 95a7233 commit 1a15b1b

File tree

5 files changed

+113
-155
lines changed

5 files changed

+113
-155
lines changed

superset-opa-integration/TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
- Test with UIF + caching in OPA (+ document this)
44
- Implement changes in operator and CRD
55
- Documentation (how to write rego rules for this)
6-
- Create a patch for superset image with OPA integration behind feature flag
76
- Tests
87
- "Sync interval" mechanism in superset to improve latency and not spam OPA
98
- Mount OPA service discovery configMap
@@ -14,3 +13,4 @@
1413
- OPA rego rules returning user-to-role mappings
1514
- Error handling in case OPA is not available or the API call returns a non 200 code or if the result data is garbage (e.g. the UIF source system is not available)
1615
- Make OPA address etc configurable via service discovery
16+
- Create a patch for superset image with OPA integration
Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1-
from http.client import HTTPException
2-
import os
3-
from flask import g
1+
from flask import current_app, g
42
from flask_appbuilder.security.sqla.models import (Role, User)
3+
from http.client import HTTPException
54
from opa_client.opa import OpaClient
65
from superset.security.manager import SupersetSecurityManager
76
from superset import conf
87
from typing import (Optional, List, Tuple)
98

109
import logging
1110

12-
# logger = logging.get_logger(__name__)
1311
class OpaSupersetSecurityManager(SupersetSecurityManager):
14-
1512
def get_user_roles(self, user: Optional[User] = None) -> List[Role]:
1613
if not user:
1714
user = g.user
1815

19-
default_role = self.resolve_role(self.get_default_role())
16+
default_role = self.resolve_role(current_app.config.get("AUTH_USER_REGISTRATION_ROLE"))
2017

2118
opa_role_names = self.get_opa_user_roles(user.username)
2219
logging.info(f'OPA returned roles: {opa_role_names}')
@@ -35,20 +32,18 @@ def get_user_roles(self, user: Optional[User] = None) -> List[Role]:
3532

3633
def get_opa_user_roles(self, username: str) -> set[str]:
3734
"""
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.
35+
Queries an Open Policy Agent instance for the roles of a given user.
36+
37+
:returns: A list of role names or an empty list if an exception during the connection to OPA
38+
is encountered or if OPA didn't return a list.
4139
"""
4240
host, port, tls = self.resolve_opa_endpoint()
43-
print(host)
44-
print(port)
45-
print(tls)
4641
client = OpaClient(host = host, port = port, ssl = tls)
4742
try:
4843
response = client.query_rule(
4944
input_data = {'username': username},
50-
package_path = 'superset',
51-
rule_name = 'user_roles')
45+
package_path = current_app.config.get('STACKABLE_OPA_PACKAGE'),
46+
rule_name = current_app.config.get('STACKABLE_OPA_RULE'))
5247
except HTTPException as e:
5348
logging.error(f'Encountered an exception while querying OPA:\n{e}')
5449
return []
@@ -58,12 +53,12 @@ def get_opa_user_roles(self, username: str) -> set[str]:
5853
logging.error(f'The OPA query didn\'t return a list: {response}')
5954
return []
6055
return roles
61-
56+
6257

6358
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'
59+
opa_endpoint = current_app.config.get('STACKABLE_OPA_ENDPOINT')
60+
[protocol, host, port] = opa_endpoint.split(":")
61+
return host.lstrip('/'), int(port.rstrip('/')), protocol == 'https'
6762

6863

6964
def resolve_role(self, role_name: str) -> Role:
@@ -72,7 +67,3 @@ def resolve_role(self, role_name: str) -> Role:
7267
logging.info(f'Creating role {role_name} as it doesn\'t already exist.')
7368
self.add_role(role_name)
7469
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"

superset-opa-integration/superset-custom-opa.yaml

Lines changed: 11 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ metadata:
88
}
99
spec:
1010
image:
11-
custom: docker.stackable.tech/sandbox/superset:4.0.2-stackable0.0.0-dev-opaV2
11+
custom: docker.stackable.tech/stackable/superset:4.0.2-stackable0.0.0-dev-opaV6
1212
productVersion: 4.0.2
1313
pullPolicy: Never
1414
clusterConfig:
@@ -30,88 +30,13 @@ spec:
3030
configMapKeyRef:
3131
key: OPA
3232
name: simple-opa
33+
envOverrides:
34+
AUTH_USER_REGISTRATION_ROLE: Gamma
3335
configOverrides:
3436
superset_config.py:
3537
EXPERIMENTAL_FILE_HEADER: |
36-
from http.client import HTTPException
37-
import os
38-
from flask import g
39-
from flask_appbuilder.security.sqla.models import (Role, User)
40-
from opa_client.opa import OpaClient
41-
from superset.security.manager import SupersetSecurityManager
42-
from superset import conf
43-
from typing import (Optional, Tuple, List)
44-
45-
import logging
46-
47-
# logger = logging.get_logger(__name__)
48-
class OpaSupersetSecurityManager(SupersetSecurityManager):
49-
50-
def get_user_roles(self, user: Optional[User] = None) -> List[Role]:
51-
if not user:
52-
user = g.user
53-
54-
default_role = self.resolve_role(self.get_default_role())
55-
56-
opa_role_names = self.get_opa_user_roles(user.username)
57-
logging.info(f'OPA returned roles: {opa_role_names}')
58-
59-
opa_roles = set(map(self.resolve_role, opa_role_names))
60-
# Ensure that in case of a bad or no reponse from OPA each user will have at least one role.
61-
opa_roles.add(default_role)
62-
63-
if set(user.roles) != opa_roles:
64-
logging.info(f'Found diff in {user.roles} vs. {opa_roles}')
65-
user.roles = list(opa_roles)
66-
self.update_user(user)
67-
68-
return user.roles
69-
70-
71-
def get_opa_user_roles(self, username: str) -> set[str]:
72-
"""
73-
Queries an Open Policy Agent instance for the roles of a given user and returns a list of role names.
74-
Returns an empty list if an exception during the connection to Open Policy Agent is encountered or if the query result
75-
is not a list.
76-
"""
77-
78-
host, port, tls = self.resolve_opa_endpoint()
79-
client = OpaClient(host = host, port = port, ssl = tls)
80-
try:
81-
response = client.query_rule(
82-
input_data = {'username': username},
83-
package_path = 'superset',
84-
rule_name = 'user_roles')
85-
except HTTPException as e:
86-
logging.error(f'Encountered an exception while querying OPA:\n{e}')
87-
return []
88-
roles = response.get('result')
89-
# If OPA didn't return a result or if the result is not a list, return no roles.
90-
if roles is None or type(roles).__name__ != "list":
91-
logging.error(f'The OPA query didn\'t return a list: {response}')
92-
return []
93-
return roles
94-
95-
96-
def resolve_opa_endpoint(self) -> Tuple[str, int, bool]:
97-
opa_endpoint = os.getenv('STACKABLE_OPA_ENDPOINT')
98-
[protocol, host, port] = opa_endpoint.split(":")
99-
return host.lstrip('/'), int(port.rstrip('/')), protocol == 'https'
100-
101-
102-
def resolve_role(self, role_name: str) -> Role:
103-
role = self.find_role(role_name)
104-
if role is None:
105-
logging.info(f'Creating role {role_name} as it doesn\'t already exist.')
106-
self.add_role(role_name)
107-
return self.find_role(role_name)
108-
109-
110-
def get_default_role(self) -> str:
111-
return conf["AUTH_USER_REGISTRATION_ROLE"] if conf["AUTH_USER_REGISTRATION_ROLE"] else "Public"
112-
AUTH_USER_REGISTRATION_ROLE: Gamma
38+
from superset.security.manager import OpaSupersetSecurityManager
11339
# Maybe also ENABLE_TEMPLATE_PROCESSING
114-
CUSTOM_SECURITY_MANAGER: OpaSupersetSecurityManager
11540
FEATURE_FLAGS: |-
11641
{
11742
'ENABLE_JAVASCRIPT_CONTROLS': True,
@@ -143,3 +68,10 @@ spec:
14368
{
14469
False
14570
}
71+
# TODO: Add these line with superset operator
72+
CUSTOM_SECURITY_MANAGER: OpaSupersetSecurityManager
73+
AUTH_USER_REGISTRATION_ROLE: os.getenv('AUTH_USER_REGISTRATION_ROLE', 'Public')
74+
STACKABLE_OPA_ENDPOINT: os.getenv('STACKABLE_OPA_ENDPOINT')
75+
STACKABLE_OPA_PACKAGE: |-
76+
"superset"
77+
STACKABLE_OPA_RULE: os.getenv('STACKABLE_OPA_RULE', 'user_roles')
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
diff --git a/superset/security/manager.py b/superset/security/manager.py
2+
index e5a32e97a..6971cf59a 100644
3+
--- a/superset/security/manager.py
4+
+++ b/superset/security/manager.py
5+
@@ -21,7 +21,7 @@ import logging
6+
import re
7+
import time
8+
from collections import defaultdict
9+
-from typing import Any, Callable, cast, NamedTuple, Optional, TYPE_CHECKING, Union
10+
+from typing import Any, Callable, cast, List, NamedTuple, Optional, Tuple, TYPE_CHECKING, Union
11+
12+
from flask import current_app, Flask, g, Request
13+
from flask_appbuilder import Model
14+
@@ -45,7 +45,9 @@ from flask_appbuilder.security.views import (
15+
from flask_appbuilder.widgets import ListWidget
16+
from flask_babel import lazy_gettext as _
17+
from flask_login import AnonymousUserMixin, LoginManager
18+
+from http.client import HTTPException
19+
from jwt.api_jwt import _jwt_global_obj
20+
+from opa_client.opa import OpaClient
21+
from sqlalchemy import and_, inspect, or_
22+
from sqlalchemy.engine.base import Connection
23+
from sqlalchemy.orm import eagerload
24+
@@ -2465,3 +2467,64 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
25+
return current_app.config["AUTH_ROLE_ADMIN"] in [
26+
role.name for role in self.get_user_roles()
27+
]
28+
+
29+
+
30+
+class OpaSupersetSecurityManager(SupersetSecurityManager):
31+
+ def get_user_roles(self, user: Optional[User] = None) -> List[Role]:
32+
+ if not user:
33+
+ user = g.user
34+
+
35+
+ default_role = self.resolve_role(current_app.config.get("AUTH_USER_REGISTRATION_ROLE"))
36+
+
37+
+ opa_role_names = self.get_opa_user_roles(user.username)
38+
+ logging.info(f'OPA returned roles: {opa_role_names}')
39+
+
40+
+ opa_roles = set(map(self.resolve_role, opa_role_names))
41+
+ # Ensure that in case of a bad or no reponse from OPA each user will have at least one role.
42+
+ opa_roles.add(default_role)
43+
+
44+
+ if set(user.roles) != opa_roles:
45+
+ logging.info(f'Found diff in {user.roles} vs. {opa_roles}')
46+
+ user.roles = list(opa_roles)
47+
+ self.update_user(user)
48+
+
49+
+ return user.roles
50+
+
51+
+
52+
+ def get_opa_user_roles(self, username: str) -> set[str]:
53+
+ """
54+
+ Queries an Open Policy Agent instance for the roles of a given user.
55+
+
56+
+ :returns: A list of role names or an empty list if an exception during the connection to OPA
57+
+ is encountered or if OPA didn't return a list.
58+
+ """
59+
+ host, port, tls = self.resolve_opa_endpoint()
60+
+ client = OpaClient(host = host, port = port, ssl = tls)
61+
+ try:
62+
+ response = client.query_rule(
63+
+ input_data = {'username': username},
64+
+ package_path = current_app.config.get('STACKABLE_OPA_PACKAGE'),
65+
+ rule_name = current_app.config.get('STACKABLE_OPA_RULE'))
66+
+ except HTTPException as e:
67+
+ logging.error(f'Encountered an exception while querying OPA:\n{e}')
68+
+ return []
69+
+ roles = response.get('result')
70+
+ # If OPA didn't return a result or if the result is not a list, return no roles.
71+
+ if roles is None or type(roles).__name__ != "list":
72+
+ logging.error(f'The OPA query didn\'t return a list: {response}')
73+
+ return []
74+
+ return roles
75+
+
76+
+
77+
+ def resolve_opa_endpoint(self) -> Tuple[str, int, bool]:
78+
+ opa_endpoint = current_app.config.get('STACKABLE_OPA_ENDPOINT')
79+
+ [protocol, host, port] = opa_endpoint.split(":")
80+
+ return host.lstrip('/'), int(port.rstrip('/')), protocol == 'https'
81+
+
82+
+
83+
+ def resolve_role(self, role_name: str) -> Role:
84+
+ role = self.find_role(role_name)
85+
+ if role is None:
86+
+ logging.info(f'Creating role {role_name} as it doesn\'t already exist.')
87+
+ self.add_role(role_name)
88+
+ return self.find_role(role_name)

superset/stackable/patches/4.0.2/OpaSupersetSecurityManager.py

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)