Skip to content

Commit d27ba87

Browse files
nightkrNickLarsenNZsbernauer
authored
Deploy userinfofetcher regorules (#580)
* Add group fetcher container * Spike group fetcher functionality * Rename group fetcher to user info fetcher * Generalize enrichment endpoints to have room for arbitrary user info * Expose user roles * Make UIF configurable * Make UIF credentials configurable too * OPA 0.45 -> 0.51 * Broke out UIF into its own crate * UIF error handling * Ingest custom attributes * Shut down on SIGTERM * Split keycloak backend out into separate module * Fix UIF startup when using none backend * Rename GroupMembershipRequest to UserInfoRequest * Cache fetched UserInfo * Switch UIF to also use workspace dependencies * Configurable UIF cache ttl * UIF crate metadata * Move userInfo rule into helm chart * Turn UIF custom attributes into a multidict * UIF smoke test * Remove rules from Helm chart until we have a better way to deploy them * Lint-b-gone * Update CRD * UIF readme * SNAFU error for UIF config * Revert Cmd wrapper enum * docs * Prototype of new bundle builder * Include cm ns in bundle path * Update bundle builder to Axum 0.7 * Disable unused futures 0.1.x compat layer * Deploy bundle builder v2 * Fix bundle builder log collection * Avoid cloning bundle for status requests * Handle and report errors * Respect watch namespace * Fix bundle builder Cargo metadata * Move dependencies into the workspace level * Loosen dependency bounds for consistency * Formatting * Log bundled files * Log bundle invalidations * Ship userinfo rego module in bundle builder * Fix userinfo/v1 syntax errors * Change userinfo documentation to refer to regolib * Enable userinfofetcher docs in nav * Migrate tests to use regolib * Changelog * Upgrade to operator-rs main and Kube-rs 0.92 * Switch bundle builder from multilog to tracing-appender * Log bundle builder output as JSON * Update operator-rs * Regenerate Nix definitions * Update docs/modules/opa/pages/usage-guide/user-info-fetcher.adoc Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com> * Changelog * Bump operator-rs * Update operator-rs * Use released build of operator-rs * Remove dead bundle builder log bytes constant * Move bundle builder bugfix to unreleased * Add comment about bundle builder log rotation * Consistency * Consistently mention the /admin group * Update rust/regorule-library/src/userinfo/v1.rego Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.de> --------- Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com> Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.de>
1 parent 2b32700 commit d27ba87

File tree

14 files changed

+131
-116
lines changed

14 files changed

+131
-116
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Added regorule library for accessing user-info-fetcher ([#580]).
10+
711
### Changed
812

913
- Rewrite of the OPA bundle builder ([#578]).
@@ -13,6 +17,7 @@ All notable changes to this project will be documented in this file.
1317
- Bundle builder should no longer keep serving deleted rules until it is restarted ([#578]).
1418

1519
[#578]: https://github.com/stackabletech/opa-operator/pull/578
20+
[#580]: https://github.com/stackabletech/opa-operator/pull/580
1621

1722
## [24.7.0] - 2024-07-24
1823

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.nix

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/modules/opa/pages/usage-guide/user-info-fetcher.adoc

Lines changed: 16 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -61,91 +61,36 @@ Fetch groups and extra credentials, but not roles.
6161

6262
NOTE: The OAuth2 Client in Keycloak must be given the `view-users` _Service Account Role_ for the realm that the users are in.
6363

64-
// TODO: Document how to use it in OPA regorules, e.g. to authorize based on group membership
65-
== Example rego rule
66-
67-
[NOTE]
68-
.User-facing API & API stability
69-
====
70-
Since the 24.07 SDP release we provide an example rego rule in our documentation using an HTTP request.
71-
However, our plan is that the user-facing API of the SPD is *not* the HTTP API of user-info-fetcher, but instead regorules that will automatically be shipped to the OPA server.
72-
This enables us to make underlying (for example breaking) changes to the HTTP API while keeping the rego rules API stable.
73-
74-
The documentation will be updated to use the deployed rego rules once available.
75-
====
76-
77-
[NOTE]
78-
.About unencrypted HTTP
79-
====
80-
The User info fetcher serves endpoints over clear-text HTTP.
81-
82-
It is intended to only be accessed from the OPA Server via _localhost_ and to not be exposed outside of the Pod.
83-
====
84-
85-
[source,rego]
86-
----
87-
package test # <1>
88-
89-
# Define a function to lookup by username
90-
userInfoByUsername(username) := http.send({
91-
"method": "POST",
92-
"url": "http://127.0.0.1:9476/user",
93-
"body": {"username": username}, <2>
94-
"headers": {"Content-Type": "application/json"},
95-
"raise_error": true
96-
}).body
97-
98-
# Define a function to lookup by a stable identifier
99-
userInfoById(id) := http.send({
100-
"method": "POST",
101-
"url": "http://127.0.0.1:9476/user",
102-
"body": {"id": id}, <3>
103-
"headers": {"Content-Type": "application/json"},
104-
"raise_error": true
105-
}).body
106-
107-
currentUserInfoByUsername := userInfoByUsername(input.username)
108-
currentUserInfoById := userInfoById(input.id)
109-
----
110-
111-
<1> The package name is important in the OPA URL used by the product.
112-
<2> Lookup by username
113-
<3> Lookup by id
114-
115-
For more information on the request and response payloads, see <<_user_info_fetcher_api>>
116-
11764
== User info fetcher API
11865

119-
HTTP Post Requests must be sent to the `/user` endpoint with the following header set: `Content-Type: application/json`.
66+
User information can be retrieved from regorules using the functions `userInfoByUsername(username)` and `userInfoById(id)` in `data.stackable.opa.userinfo.v1`.
12067

121-
You can either lookup the user info by stable identifier:
68+
An example of the returned structure:
12269

12370
[source,json]
12471
----
12572
{
12673
"id": "af07f12c-a2db-40a7-93e0-874537bdf3f5",
74+
"username": "alice",
75+
"groups": [
76+
"/admin"
77+
],
78+
"customAttributes": {}
12779
}
12880
----
12981

130-
or by the username:
82+
For example, the following rule will allow access for users in the `/admin` group:
13183

132-
[source,json]
133-
----
134-
{
135-
"username": "alice",
136-
}
84+
[source,rego]
13785
----
86+
package test
13887
139-
If the user is found, the following response structure will be returned:
88+
import rego.v1
14089
141-
[source,json]
142-
----
143-
{
144-
"id": "af07f12c-a2db-40a7-93e0-874537bdf3f5",
145-
"username": "alice",
146-
"groups": [
147-
"/superset-admin"
148-
],
149-
"customAttributes": {}
90+
default allow := false
91+
92+
allow if {
93+
user := data.stackable.opa.userinfo.v1.userInfoById(input.userId)
94+
"/admin" in user.groups
15095
}
15196
----

rust/bundle-builder/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ publish = false
1111
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1212

1313
[dependencies]
14+
stackable-opa-regorule-library = { path = "../regorule-library" }
15+
1416
axum.workspace = true
1517
clap.workspace = true
1618
flate2.workspace = true

rust/bundle-builder/src/main.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,19 @@ enum BundleError {
181181
#[snafu(display("ConfigMap is missing required metadata"))]
182182
ConfigMapMetadataMissing,
183183

184-
#[snafu(display("file {file_name:?} in {config_map} is too large ({file_size} bytes)"))]
184+
#[snafu(display("file {file_path:?} is too large ({file_size} bytes)"))]
185185
FileSizeOverflow {
186186
source: TryFromIntError,
187-
config_map: ObjectRef<ConfigMap>,
188-
file_name: String,
187+
file_path: String,
189188
file_size: usize,
190189
},
191190

191+
#[snafu(display("failed to add static file {file_path:?} to tarball"))]
192+
AddStaticRuleToTarball {
193+
source: std::io::Error,
194+
file_path: String,
195+
},
196+
192197
#[snafu(display("failed to add file {file_name:?} from {config_map} to tarball"))]
193198
AddFileToTarball {
194199
source: std::io::Error,
@@ -211,20 +216,15 @@ impl BundleError {
211216

212217
async fn build_bundle(store: Store<ConfigMap>) -> Result<Vec<u8>, BundleError> {
213218
use bundle_error::*;
214-
fn file_header(
215-
config_map: &ObjectRef<ConfigMap>,
216-
file_name: &str,
217-
data: &[u8],
218-
) -> Result<tar::Header, BundleError> {
219+
fn file_header(file_path: &str, data: &[u8]) -> Result<tar::Header, BundleError> {
219220
let mut header = tar::Header::new_gnu();
220221
header.set_mode(0o644);
221222
let file_size = data.len();
222223
header.set_size(
223224
file_size
224225
.try_into()
225226
.with_context(|_| FileSizeOverflowSnafu {
226-
config_map: config_map.clone(),
227-
file_name,
227+
file_path,
228228
file_size,
229229
})?,
230230
);
@@ -237,6 +237,16 @@ async fn build_bundle(store: Store<ConfigMap>) -> Result<Vec<u8>, BundleError> {
237237
let mut tar = tar::Builder::new(GzEncoder::new(Vec::new(), flate2::Compression::default()));
238238
let mut resource_versions = BTreeMap::<String, String>::new();
239239
let mut bundle_file_paths = BTreeSet::<String>::new();
240+
241+
for (file_path, data) in stackable_opa_regorule_library::REGORULES {
242+
let mut header = file_header(file_path, data.as_bytes())?;
243+
tar.append_data(&mut header, file_path, data.as_bytes())
244+
.context(AddStaticRuleToTarballSnafu {
245+
file_path: *file_path,
246+
})?;
247+
bundle_file_paths.insert(file_path.to_string());
248+
}
249+
240250
for cm in store.state() {
241251
let ObjectMeta {
242252
name: Some(cm_ns),
@@ -249,8 +259,8 @@ async fn build_bundle(store: Store<ConfigMap>) -> Result<Vec<u8>, BundleError> {
249259
};
250260
let cm_ref = ObjectRef::from_obj(&*cm);
251261
for (file_name, data) in cm.data.iter().flatten() {
252-
let mut header = file_header(&cm_ref, file_name, data.as_bytes())?;
253262
let file_path = format!("configmap/{cm_ns}/{cm_name}/{file_name}");
263+
let mut header = file_header(&file_path, data.as_bytes())?;
254264
tar.append_data(&mut header, &file_path, data.as_bytes())
255265
.with_context(|_| AddFileToTarballSnafu {
256266
config_map: cm_ref.clone(),

rust/regorule-library/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "stackable-opa-regorule-library"
3+
description = "Contains Stackable's library of common regorules"
4+
version.workspace = true
5+
authors.workspace = true
6+
license.workspace = true
7+
edition.workspace = true
8+
repository.workspace = true
9+
publish = false
10+
11+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12+
13+
[dependencies]

rust/regorule-library/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Stackable library of shared regorules
2+
3+
This contains regorules that are shipped by the Stackable Data Platform (SDP) as libraries to help simplify writing authorization rules.
4+
5+
## What this is not
6+
7+
This library should *not* contain rules that only concern one SDP product. Those are the responsibility of their individual operators.
8+
9+
## Versioning
10+
11+
All regorules exposed by this library should be versioned, according to Kubernetes conventions.
12+
13+
This version covers *breaking changes to the interface*, not the implementation. If a proposed change breaks existing clients,
14+
add a new version. Otherwise, change the latest version inline.
15+
16+
Ideally, old versions should be implemented on top of newer versions, rather than carry independent implementations.

rust/regorule-library/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub const REGORULES: &[(&str, &str)] = &[(
2+
"stackable/opa/userinfo/v1.rego",
3+
include_str!("userinfo/v1.rego"),
4+
)];
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package stackable.opa.userinfo.v1
2+
3+
# Lookup by (human-readable) username
4+
userInfoByUsername(username) := http.send({
5+
"method": "POST",
6+
"url": "http://127.0.0.1:9476/user",
7+
"body": {"username": username},
8+
"headers": {"Content-Type": "application/json"},
9+
"raise_error": true
10+
}).body
11+
12+
# Lookup by stable user identifier
13+
userInfoById(id) := http.send({
14+
"method": "POST",
15+
"url": "http://127.0.0.1:9476/user",
16+
"body": {"id": id},
17+
"headers": {"Content-Type": "application/json"},
18+
"raise_error": true
19+
}).body

tests/templates/kuttl/aas-user-info/10-install-opa.yaml.j2

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,6 @@ commands:
55
- script: |
66
kubectl apply -n $NAMESPACE -f - <<EOF
77
---
8-
apiVersion: v1
9-
kind: ConfigMap
10-
metadata:
11-
name: test
12-
labels:
13-
opa.stackable.tech/bundle: "true"
14-
data:
15-
test.rego: |
16-
package test
17-
18-
userInfoByUsername(username) := http.send({"method": "POST", "url": "http://127.0.0.1:9476/user", "body": {"username": username}, "headers": {"Content-Type": "application/json"}, "raise_error": true}).body
19-
userInfoById(id) := http.send({"method": "POST", "url": "http://127.0.0.1:9476/user", "body": {"id": id}, "headers": {"Content-Type": "application/json"}, "raise_error": true}).body
20-
21-
currentUserInfoByUsername := userInfoByUsername(input.username)
22-
currentUserInfoById := userInfoById(input.id)
23-
---
248
apiVersion: opa.stackable.tech/v1alpha1
259
kind: OpaCluster
2610
metadata:

tests/templates/kuttl/aas-user-info/30-assert.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ kind: TestAssert
44
metadata:
55
name: test-regorule
66
commands:
7-
- script: kubectl exec -n $NAMESPACE test-regorule-0 -- python /tmp/test-regorule.py -u 'http://test-opa-server-default:8081/v1/data/test'
7+
- script: kubectl exec -n $NAMESPACE test-regorule-0 -- python /tmp/test-regorule.py -u 'http://test-opa-server-default:8081/v1/data/stackable/opa/userinfo/v1'

tests/templates/kuttl/keycloak-user-info/10-install-opa.yaml.j2

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,6 @@ commands:
55
- script: |
66
kubectl apply -n $NAMESPACE -f - <<EOF
77
---
8-
apiVersion: v1
9-
kind: ConfigMap
10-
metadata:
11-
name: test
12-
labels:
13-
opa.stackable.tech/bundle: "true"
14-
data:
15-
test.rego: |
16-
package test
17-
18-
userInfoByUsername(username) := http.send({"method": "POST", "url": "http://127.0.0.1:9476/user", "body": {"username": username}, "headers": {"Content-Type": "application/json"}, "raise_error": true}).body
19-
userInfoById(id) := http.send({"method": "POST", "url": "http://127.0.0.1:9476/user", "body": {"id": id}, "headers": {"Content-Type": "application/json"}, "raise_error": true}).body
20-
21-
currentUserInfoByUsername := userInfoByUsername(input.username)
22-
currentUserInfoById := userInfoById(input.id)
23-
---
248
apiVersion: opa.stackable.tech/v1alpha1
259
kind: OpaCluster
2610
metadata:

tests/templates/kuttl/keycloak-user-info/30-assert.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ kind: TestAssert
44
metadata:
55
name: test-regorule
66
commands:
7-
- script: kubectl exec -n $NAMESPACE test-regorule-0 -- python /tmp/test-regorule.py -u 'http://test-opa-server-default:8081/v1/data/test'
7+
- script: kubectl exec -n $NAMESPACE test-regorule-0 -- python /tmp/test-regorule.py -u 'http://test-opa-server-default:8081/v1/data/stackable/opa/userinfo/v1'

0 commit comments

Comments
 (0)