Skip to content

Commit 9c8a7d5

Browse files
Malewarelabrenbesbernauerrazvan
authored
feat: Support OPA role mapping (#582)
* First skeleton of opa integration * WIP implementation opa-role-mapping * Add SecurityManager dynamically * Better OPA_IMPORT * Security manager own file in Docker-Images. Fixing python expression * making clippy happy for now * Updating some approaches * Adding more rules, more sophisticated handling of stuff * Defaults are working * Better interfering of package path * Happy Clippy * update OpaSupersetSecurityManager import path * import new opa_authorizer module * Removing some ToDo's. Better comments * Adding OpaRolesCache with 10 minutes default * pre commit becomes happy * create opa test basics * rename test directory * fix test-definition * fix typo * fix opa test scaffold * Adding rule_name to be defined by the user. defaults to empty string * Adding default to rule_name * StackableOpaRule to string as we interfere from CRDs * Adding ttl to crds. Default to 10. * cache_ttl now Duration type. Converted to seconds in superset_config.py * wip: integration tests * integrate feedback * create basic user role test * First documentation draft * Adding reference to opa user-info-fetcher * Updating changelog * fix rego and first check in integration test * fix formatting issues * Making rust fmt happy * lint with ruff formatter * use TtlCache from operator-rs * Regenerate charts * Making pre-commit happy * apply typos and formatting corrections Co-authored-by: Sebastian Bernauer <[email protected]> * update chart * adress feedback in PR and rename envs * fix changelog * Update opa tests. * support custom image for opa tests * create and assign new role via API * fix typos * add some comments * opa kuttl test is green (again) * silence most of Pyright errors and warnings * Update rust/crd/src/lib.rs Co-authored-by: Sebastian Bernauer <[email protected]> * Renaming fields and structs * format code * make field required * pass on opa endpoint instead of base url to the authorizer * Update docs/modules/superset/pages/usage-guide/security.adoc Co-authored-by: Sebastian Bernauer <[email protected]> * Update docs/modules/superset/pages/usage-guide/security.adoc Co-authored-by: Sebastian Bernauer <[email protected]> * Update docs/modules/superset/pages/usage-guide/security.adoc Co-authored-by: Sebastian Bernauer <[email protected]> * Update docs/modules/superset/pages/usage-guide/security.adoc Co-authored-by: Sebastian Bernauer <[email protected]> * update security.adoc * move constant * rename opa dimension * revert changes to smoke test * Update tests/templates/kuttl/opa/40_superset.yaml.j2 Co-authored-by: Sebastian Bernauer <[email protected]> * remove unused image field * add serde cache defaults * Update rust/operator-binary/src/authorization/opa.rs Co-authored-by: Sebastian Bernauer <[email protected]> * Update rust/operator-binary/src/superset_controller.rs Co-authored-by: Sebastian Bernauer <[email protected]> * add missing EOF * add vector aggregator config map * add openshift ns patch --------- Co-authored-by: Benedikt Labrenz <[email protected]> Co-authored-by: Sebastian Bernauer <[email protected]> Co-authored-by: Razvan-Daniel Mihai <[email protected]>
1 parent 087a1ee commit 9c8a7d5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1104
-85
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66

77
- Run a `containerdebug` process in the background of each Superset container to collect debugging information ([#578]).
88
- Aggregate emitted Kubernetes events on the CustomResources ([#585]).
9+
- Support OPA role mapping as optional custom security manager for Superset ([#582]).
910
- Support for version `4.1.1` ([#595]).
1011

1112
### Changed
1213

1314
- Default to OCI for image metadata and product image selection ([#586]).
1415

1516
[#578]: https://github.com/stackabletech/superset-operator/pull/578
17+
[#582]: https://github.com/stackabletech/superset-operator/pull/582
1618
[#585]: https://github.com/stackabletech/superset-operator/pull/585
1719
[#586]: https://github.com/stackabletech/superset-operator/pull/586
1820
[#595]: https://github.com/stackabletech/superset-operator/pull/595

Diff for: Cargo.lock

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.nix

+9-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
2323
serde_json = "1.0"
2424
serde_yaml = "0.9"
2525
snafu = "0.8"
26-
stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "stackable-operator-0.85.0" }
26+
stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "stackable-operator-0.86.0" }
2727
strum = { version = "0.26", features = ["derive"] }
2828
tokio = { version = "1.40", features = ["full"] }
2929
tracing = "0.1"

Diff for: crate-hashes.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: deploy/helm/superset-operator/crds/crds.yaml

+40
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,46 @@ spec:
7171
- authenticationClass
7272
type: object
7373
type: array
74+
authorization:
75+
description: |-
76+
Authorization options for Superset.
77+
78+
Currently only role assignment is supported. This means that roles are assigned to users in OPA but, due to the way Superset is implemented, the database also needs to be updated to reflect these assignments. Therefore, user roles and permissions must already exist in the Superset database before they can be assigned to a user. Warning: Any user roles assigned with the Superset UI are discarded.
79+
nullable: true
80+
properties:
81+
roleMappingFromOpa:
82+
description: Configure the OPA stacklet [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) and the name of the Rego package containing your authorization rules. Consult the [OPA authorization documentation](https://docs.stackable.tech/home/nightly/concepts/opa) to learn how to deploy Rego authorization rules with OPA.
83+
properties:
84+
cache:
85+
default:
86+
entryTimeToLive: 30s
87+
maxEntries: 10000
88+
description: Configuration for an Superset internal cache for calls to OPA
89+
properties:
90+
entryTimeToLive:
91+
default: 30s
92+
description: Time to live per entry
93+
type: string
94+
maxEntries:
95+
default: 10000
96+
description: Maximum number of entries in the cache; If this threshold is reached then the least recently used item is removed.
97+
format: uint32
98+
minimum: 0.0
99+
type: integer
100+
type: object
101+
configMapName:
102+
description: The [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) for the OPA stacklet that should be used for authorization requests.
103+
type: string
104+
package:
105+
description: The name of the Rego package containing the Rego rules for the product.
106+
nullable: true
107+
type: string
108+
required:
109+
- configMapName
110+
type: object
111+
required:
112+
- roleMappingFromOpa
113+
type: object
74114
clusterOperation:
75115
default:
76116
reconciliationPaused: false

Diff for: docs/modules/superset/pages/usage-guide/security.adoc

+65
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,71 @@ Further information for specifying an AuthenticationClass for an OIDC provider c
126126
Superset has a concept called `Roles` which allows you to grant user permissions based on roles.
127127
Have a look at the {superset-security}[Superset documentation on Security^{external-link-icon}^].
128128

129+
[opa]
130+
=== OPA role mapping
131+
132+
Stackable ships a custom security manager that makes it possible to assign roles to users via the Open Policy Agent integration.
133+
The roles must exist in the Superset database before they can be assigned to users.
134+
If a role is not present in the Superset database, an error will be logged by the security manager and the user login will proceed without it.
135+
Also the role names must match exactly the output of the Rego rule named `user_roles`.
136+
In the following example, a rego package is defined that assigns roles to the users `admin` and `guest`.
137+
138+
[source,yaml]
139+
----
140+
apiVersion: v1
141+
kind: ConfigMap
142+
metadata:
143+
name: superset-opa-regorules
144+
labels:
145+
opa.stackable.tech/bundle: "true"
146+
data:
147+
roles.rego: |
148+
package superset
149+
150+
default user_roles := []
151+
152+
user_roles := roles if {
153+
some user in users
154+
roles := user.roles
155+
user.username == input.username
156+
}
157+
users := [
158+
{"username": "admin", "roles": ["Admin", "Test"]}, #<1>
159+
{"username": "guest", "roles": ["Gamma"]} #<2>
160+
]
161+
----
162+
163+
<1> Assign the roles `Admin` and `Test` to the `admin` user. The `Test` role is not a standard Superset role and must be created before the assignment.
164+
<2> Assign the `Gamma` role to the `guest` user.
165+
166+
OPA rules can make use of the xref:opa:usage-guide:user-info-fetcher[user-info-fetcher] integration.
167+
168+
The following snippet shows how to use the OPA security manager in a Superset stacklet.
169+
170+
[source,yaml]
171+
----
172+
apiVersion: superset.stackable.tech/v1alpha1
173+
kind: SupersetCluster
174+
metadata:
175+
name: superset-with-opa-role-mapping
176+
spec:
177+
clusterConfig:
178+
authorization:
179+
roleMappingFromOpa:
180+
configMapName: superset-opa-regorules # <1>
181+
package: superset
182+
cache: # <2>
183+
entryTimeToLive: 10s # <3>
184+
maxEntries: 5 # <4>
185+
----
186+
187+
<1> ConfigMap name containing rego rules
188+
<2> Mandatory Opa caching. If not set, default settings apply.
189+
<3> Time for cached entries per user can live. Defaults to 30s.
190+
<4> Number of maximum entries, defaults to 1000. Cache will be disabled for maxEntries: 0.
191+
192+
IMPORTANT: Any role assignments done in the Superset UI are discarded and will be overridden by the OPA security manager.
193+
129194
=== Superset database
130195

131196
You can view all the available roles in the web interface of Superset and can also assign users to these roles.

Diff for: rust/crd/src/lib.rs

+51
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use snafu::{OptionExt, ResultExt, Snafu};
77
use stackable_operator::{
88
commons::{
99
affinity::StackableAffinity,
10+
cache::UserInformationCache,
1011
cluster_operation::ClusterOperation,
12+
opa::OpaConfig,
1113
product_image_selection::ProductImage,
1214
resources::{
1315
CpuLimitsFragment, MemoryLimitsFragment, NoRuntimeLimits, NoRuntimeLimitsFragment,
@@ -89,6 +91,12 @@ pub enum SupersetConfigOptions {
8991
AuthLdapTlsCertfile,
9092
AuthLdapTlsKeyfile,
9193
AuthLdapTlsCacertfile,
94+
CustomSecurityManager,
95+
AuthOpaRequestUrl,
96+
AuthOpaPackage,
97+
AuthOpaRule,
98+
AuthOpaCacheMaxEntries,
99+
AuthOpaCacheTtlInSec,
92100
}
93101

94102
impl SupersetConfigOptions {
@@ -119,6 +127,7 @@ impl FlaskAppConfigOptions for SupersetConfigOptions {
119127
SupersetConfigOptions::LoggingConfigurator => PythonType::Expression,
120128
SupersetConfigOptions::AuthType => PythonType::Expression,
121129
SupersetConfigOptions::AuthUserRegistration => PythonType::BoolLiteral,
130+
// Going to be an expression as we default it from env, if and only if opa is used
122131
SupersetConfigOptions::AuthUserRegistrationRole => PythonType::StringLiteral,
123132
SupersetConfigOptions::AuthRolesSyncAtLogin => PythonType::BoolLiteral,
124133
SupersetConfigOptions::AuthLdapServer => PythonType::StringLiteral,
@@ -136,6 +145,13 @@ impl FlaskAppConfigOptions for SupersetConfigOptions {
136145
SupersetConfigOptions::AuthLdapTlsCertfile => PythonType::StringLiteral,
137146
SupersetConfigOptions::AuthLdapTlsKeyfile => PythonType::StringLiteral,
138147
SupersetConfigOptions::AuthLdapTlsCacertfile => PythonType::StringLiteral,
148+
// Configuration options used by CustomOpaSecurityManager
149+
SupersetConfigOptions::CustomSecurityManager => PythonType::Expression,
150+
SupersetConfigOptions::AuthOpaRequestUrl => PythonType::StringLiteral,
151+
SupersetConfigOptions::AuthOpaPackage => PythonType::StringLiteral,
152+
SupersetConfigOptions::AuthOpaRule => PythonType::StringLiteral,
153+
SupersetConfigOptions::AuthOpaCacheMaxEntries => PythonType::IntLiteral,
154+
SupersetConfigOptions::AuthOpaCacheTtlInSec => PythonType::IntLiteral,
139155
}
140156
}
141157
}
@@ -179,6 +195,17 @@ pub struct SupersetClusterConfig {
179195
#[serde(default)]
180196
pub authentication: Vec<SupersetClientAuthenticationDetails>,
181197

198+
/// Authorization options for Superset.
199+
///
200+
/// Currently only role assignment is supported. This means that roles are assigned to users in
201+
/// OPA but, due to the way Superset is implemented, the database also needs to be updated
202+
/// to reflect these assignments.
203+
/// Therefore, user roles and permissions must already exist in the Superset database before
204+
/// they can be assigned to a user.
205+
/// Warning: Any user roles assigned with the Superset UI are discarded.
206+
#[serde(skip_serializing_if = "Option::is_none")]
207+
pub authorization: Option<SupersetAuthorization>,
208+
182209
/// The name of the Secret object containing the admin user credentials and database connection details.
183210
/// Read the
184211
/// [getting started guide first steps](DOCS_BASE_URL_PLACEHOLDER/superset/getting_started/first_steps)
@@ -242,6 +269,22 @@ impl CurrentlySupportedListenerClasses {
242269
}
243270
}
244271
}
272+
#[derive(Clone, Deserialize, Serialize, Eq, JsonSchema, Debug, PartialEq)]
273+
#[serde(rename_all = "camelCase")]
274+
pub struct SupersetOpaRoleMappingConfig {
275+
#[serde(flatten)]
276+
pub opa: OpaConfig,
277+
278+
/// Configuration for an Superset internal cache for calls to OPA
279+
#[serde(default)]
280+
pub cache: UserInformationCache,
281+
}
282+
283+
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
284+
#[serde(rename_all = "camelCase")]
285+
pub struct SupersetAuthorization {
286+
pub role_mapping_from_opa: SupersetOpaRoleMappingConfig,
287+
}
245288

246289
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
247290
#[serde(rename_all = "camelCase")]
@@ -476,6 +519,14 @@ impl SupersetCluster {
476519
}
477520
}
478521

522+
pub fn get_opa_config(&self) -> Option<&SupersetOpaRoleMappingConfig> {
523+
self.spec
524+
.cluster_config
525+
.authorization
526+
.as_ref()
527+
.map(|a| &a.role_mapping_from_opa)
528+
}
529+
479530
/// Retrieve and merge resource configs for role and role groups
480531
pub fn merged_config(
481532
&self,

Diff for: rust/operator-binary/src/authorization/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod opa;

Diff for: rust/operator-binary/src/authorization/opa.rs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use std::collections::BTreeMap;
2+
3+
use stackable_operator::{client::Client, commons::opa::OpaApiVersion, time::Duration};
4+
use stackable_superset_crd::{SupersetCluster, SupersetOpaRoleMappingConfig};
5+
6+
pub const OPA_IMPORTS: &[&str] =
7+
&["from opa_authorizer.opa_manager import OpaSupersetSecurityManager"];
8+
9+
pub struct SupersetOpaConfigResolved {
10+
opa_endpoint: String,
11+
cache_max_entries: u32,
12+
cache_ttl: Duration,
13+
}
14+
15+
impl SupersetOpaConfigResolved {
16+
pub async fn from_opa_config(
17+
client: &Client,
18+
superset: &SupersetCluster,
19+
opa_config: &SupersetOpaRoleMappingConfig,
20+
) -> Result<Self, stackable_operator::commons::opa::Error> {
21+
let opa_endpoint = opa_config
22+
.opa
23+
.full_document_url_from_config_map(client, superset, None, OpaApiVersion::V1)
24+
.await?;
25+
26+
Ok(SupersetOpaConfigResolved {
27+
opa_endpoint,
28+
cache_max_entries: opa_config.cache.max_entries.to_owned(),
29+
cache_ttl: opa_config.cache.entry_time_to_live.to_owned(),
30+
})
31+
}
32+
33+
// Adding necessary configurations. Imports are solved in config.rs
34+
pub fn as_config(&self) -> BTreeMap<String, String> {
35+
BTreeMap::from([
36+
(
37+
"CUSTOM_SECURITY_MANAGER".to_string(),
38+
"OpaSupersetSecurityManager".to_string(),
39+
),
40+
(
41+
"AUTH_OPA_REQUEST_URL".to_string(),
42+
self.opa_endpoint.to_owned(),
43+
),
44+
(
45+
"AUTH_OPA_CACHE_MAX_ENTRIES".to_string(),
46+
self.cache_max_entries.to_string(),
47+
),
48+
(
49+
"AUTH_OPA_CACHE_TTL_IN_SEC".to_string(),
50+
self.cache_ttl.as_secs().to_string(),
51+
),
52+
("AUTH_OPA_RULE".to_string(), "user_roles".to_string()),
53+
])
54+
}
55+
}

0 commit comments

Comments
 (0)