diff --git a/CHANGELOG.md b/CHANGELOG.md index 893ed1a4..ccf1ef86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Added regorule library for accessing user-info-fetcher ([#580]). + ### Changed - Rewrite of the OPA bundle builder ([#578]). @@ -13,6 +17,7 @@ All notable changes to this project will be documented in this file. - Bundle builder should no longer keep serving deleted rules until it is restarted ([#578]). [#578]: https://github.com/stackabletech/opa-operator/pull/578 +[#580]: https://github.com/stackabletech/opa-operator/pull/580 ## [24.7.0] - 2024-07-24 diff --git a/Cargo.lock b/Cargo.lock index 34806ed8..dff1ded2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2450,6 +2450,7 @@ dependencies = [ "futures", "hyper", "snafu 0.8.4", + "stackable-opa-regorule-library", "stackable-operator", "tar", "tokio", @@ -2493,6 +2494,10 @@ dependencies = [ "tracing", ] +[[package]] +name = "stackable-opa-regorule-library" +version = "0.0.0-dev" + [[package]] name = "stackable-opa-user-info-fetcher" version = "0.0.0-dev" diff --git a/Cargo.nix b/Cargo.nix index 6f2244f4..871093cd 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -65,6 +65,16 @@ rec { # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; + "stackable-opa-regorule-library" = rec { + packageId = "stackable-opa-regorule-library"; + build = internal.buildRustCrateWithFeatures { + packageId = "stackable-opa-regorule-library"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; "stackable-opa-user-info-fetcher" = rec { packageId = "stackable-opa-user-info-fetcher"; build = internal.buildRustCrateWithFeatures { @@ -7911,6 +7921,10 @@ rec { name = "snafu"; packageId = "snafu 0.8.4"; } + { + name = "stackable-opa-regorule-library"; + packageId = "stackable-opa-regorule-library"; + } { name = "stackable-operator"; packageId = "stackable-operator"; @@ -8078,6 +8092,20 @@ rec { } ]; + }; + "stackable-opa-regorule-library" = rec { + crateName = "stackable-opa-regorule-library"; + version = "0.0.0-dev"; + edition = "2021"; + # We can't filter paths with references in Nix 2.4 + # See https://github.com/NixOS/nix/issues/5410 + src = if ((lib.versionOlder builtins.nixVersion "2.4pre20211007") || (lib.versionOlder "2.5" builtins.nixVersion )) + then lib.cleanSourceWith { filter = sourceFilter; src = ./rust/regorule-library; } + else ./rust/regorule-library; + authors = [ + "Stackable GmbH " + ]; + }; "stackable-opa-user-info-fetcher" = rec { crateName = "stackable-opa-user-info-fetcher"; diff --git a/docs/modules/opa/pages/usage-guide/user-info-fetcher.adoc b/docs/modules/opa/pages/usage-guide/user-info-fetcher.adoc index 8ffdc0b6..9830b97d 100644 --- a/docs/modules/opa/pages/usage-guide/user-info-fetcher.adoc +++ b/docs/modules/opa/pages/usage-guide/user-info-fetcher.adoc @@ -61,91 +61,36 @@ Fetch groups and extra credentials, but not roles. NOTE: The OAuth2 Client in Keycloak must be given the `view-users` _Service Account Role_ for the realm that the users are in. -// TODO: Document how to use it in OPA regorules, e.g. to authorize based on group membership -== Example rego rule - -[NOTE] -.User-facing API & API stability -==== -Since the 24.07 SDP release we provide an example rego rule in our documentation using an HTTP request. -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. -This enables us to make underlying (for example breaking) changes to the HTTP API while keeping the rego rules API stable. - -The documentation will be updated to use the deployed rego rules once available. -==== - -[NOTE] -.About unencrypted HTTP -==== -The User info fetcher serves endpoints over clear-text HTTP. - -It is intended to only be accessed from the OPA Server via _localhost_ and to not be exposed outside of the Pod. -==== - -[source,rego] ----- -package test # <1> - -# Define a function to lookup by username -userInfoByUsername(username) := http.send({ - "method": "POST", - "url": "http://127.0.0.1:9476/user", - "body": {"username": username}, <2> - "headers": {"Content-Type": "application/json"}, - "raise_error": true -}).body - -# Define a function to lookup by a stable identifier -userInfoById(id) := http.send({ - "method": "POST", - "url": "http://127.0.0.1:9476/user", - "body": {"id": id}, <3> - "headers": {"Content-Type": "application/json"}, - "raise_error": true -}).body - -currentUserInfoByUsername := userInfoByUsername(input.username) -currentUserInfoById := userInfoById(input.id) ----- - -<1> The package name is important in the OPA URL used by the product. -<2> Lookup by username -<3> Lookup by id - -For more information on the request and response payloads, see <<_user_info_fetcher_api>> - == User info fetcher API -HTTP Post Requests must be sent to the `/user` endpoint with the following header set: `Content-Type: application/json`. +User information can be retrieved from regorules using the functions `userInfoByUsername(username)` and `userInfoById(id)` in `data.stackable.opa.userinfo.v1`. -You can either lookup the user info by stable identifier: +An example of the returned structure: [source,json] ---- { "id": "af07f12c-a2db-40a7-93e0-874537bdf3f5", + "username": "alice", + "groups": [ + "/admin" + ], + "customAttributes": {} } ---- -or by the username: +For example, the following rule will allow access for users in the `/admin` group: -[source,json] ----- -{ - "username": "alice", -} +[source,rego] ---- +package test -If the user is found, the following response structure will be returned: +import rego.v1 -[source,json] ----- -{ - "id": "af07f12c-a2db-40a7-93e0-874537bdf3f5", - "username": "alice", - "groups": [ - "/superset-admin" - ], - "customAttributes": {} +default allow := false + +allow if { + user := data.stackable.opa.userinfo.v1.userInfoById(input.userId) + "/admin" in user.groups } ---- diff --git a/rust/bundle-builder/Cargo.toml b/rust/bundle-builder/Cargo.toml index 99f4bc20..dbb99ced 100644 --- a/rust/bundle-builder/Cargo.toml +++ b/rust/bundle-builder/Cargo.toml @@ -11,6 +11,8 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +stackable-opa-regorule-library = { path = "../regorule-library" } + axum.workspace = true clap.workspace = true flate2.workspace = true diff --git a/rust/bundle-builder/src/main.rs b/rust/bundle-builder/src/main.rs index e1170a23..70ab3353 100644 --- a/rust/bundle-builder/src/main.rs +++ b/rust/bundle-builder/src/main.rs @@ -181,14 +181,19 @@ enum BundleError { #[snafu(display("ConfigMap is missing required metadata"))] ConfigMapMetadataMissing, - #[snafu(display("file {file_name:?} in {config_map} is too large ({file_size} bytes)"))] + #[snafu(display("file {file_path:?} is too large ({file_size} bytes)"))] FileSizeOverflow { source: TryFromIntError, - config_map: ObjectRef, - file_name: String, + file_path: String, file_size: usize, }, + #[snafu(display("failed to add static file {file_path:?} to tarball"))] + AddStaticRuleToTarball { + source: std::io::Error, + file_path: String, + }, + #[snafu(display("failed to add file {file_name:?} from {config_map} to tarball"))] AddFileToTarball { source: std::io::Error, @@ -211,11 +216,7 @@ impl BundleError { async fn build_bundle(store: Store) -> Result, BundleError> { use bundle_error::*; - fn file_header( - config_map: &ObjectRef, - file_name: &str, - data: &[u8], - ) -> Result { + fn file_header(file_path: &str, data: &[u8]) -> Result { let mut header = tar::Header::new_gnu(); header.set_mode(0o644); let file_size = data.len(); @@ -223,8 +224,7 @@ async fn build_bundle(store: Store) -> Result, BundleError> { file_size .try_into() .with_context(|_| FileSizeOverflowSnafu { - config_map: config_map.clone(), - file_name, + file_path, file_size, })?, ); @@ -237,6 +237,16 @@ async fn build_bundle(store: Store) -> Result, BundleError> { let mut tar = tar::Builder::new(GzEncoder::new(Vec::new(), flate2::Compression::default())); let mut resource_versions = BTreeMap::::new(); let mut bundle_file_paths = BTreeSet::::new(); + + for (file_path, data) in stackable_opa_regorule_library::REGORULES { + let mut header = file_header(file_path, data.as_bytes())?; + tar.append_data(&mut header, file_path, data.as_bytes()) + .context(AddStaticRuleToTarballSnafu { + file_path: *file_path, + })?; + bundle_file_paths.insert(file_path.to_string()); + } + for cm in store.state() { let ObjectMeta { name: Some(cm_ns), @@ -249,8 +259,8 @@ async fn build_bundle(store: Store) -> Result, BundleError> { }; let cm_ref = ObjectRef::from_obj(&*cm); for (file_name, data) in cm.data.iter().flatten() { - let mut header = file_header(&cm_ref, file_name, data.as_bytes())?; let file_path = format!("configmap/{cm_ns}/{cm_name}/{file_name}"); + let mut header = file_header(&file_path, data.as_bytes())?; tar.append_data(&mut header, &file_path, data.as_bytes()) .with_context(|_| AddFileToTarballSnafu { config_map: cm_ref.clone(), diff --git a/rust/regorule-library/Cargo.toml b/rust/regorule-library/Cargo.toml new file mode 100644 index 00000000..9835eeff --- /dev/null +++ b/rust/regorule-library/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "stackable-opa-regorule-library" +description = "Contains Stackable's library of common regorules" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/rust/regorule-library/README.md b/rust/regorule-library/README.md new file mode 100644 index 00000000..360e19ff --- /dev/null +++ b/rust/regorule-library/README.md @@ -0,0 +1,16 @@ +# Stackable library of shared regorules + +This contains regorules that are shipped by the Stackable Data Platform (SDP) as libraries to help simplify writing authorization rules. + +## What this is not + +This library should *not* contain rules that only concern one SDP product. Those are the responsibility of their individual operators. + +## Versioning + +All regorules exposed by this library should be versioned, according to Kubernetes conventions. + +This version covers *breaking changes to the interface*, not the implementation. If a proposed change breaks existing clients, +add a new version. Otherwise, change the latest version inline. + +Ideally, old versions should be implemented on top of newer versions, rather than carry independent implementations. diff --git a/rust/regorule-library/src/lib.rs b/rust/regorule-library/src/lib.rs new file mode 100644 index 00000000..b032dade --- /dev/null +++ b/rust/regorule-library/src/lib.rs @@ -0,0 +1,4 @@ +pub const REGORULES: &[(&str, &str)] = &[( + "stackable/opa/userinfo/v1.rego", + include_str!("userinfo/v1.rego"), +)]; diff --git a/rust/regorule-library/src/userinfo/v1.rego b/rust/regorule-library/src/userinfo/v1.rego new file mode 100644 index 00000000..5489e329 --- /dev/null +++ b/rust/regorule-library/src/userinfo/v1.rego @@ -0,0 +1,19 @@ +package stackable.opa.userinfo.v1 + +# Lookup by (human-readable) username +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 + +# Lookup by stable user identifier +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 diff --git a/tests/templates/kuttl/aas-user-info/10-install-opa.yaml.j2 b/tests/templates/kuttl/aas-user-info/10-install-opa.yaml.j2 index 9e9ac895..149a0a32 100644 --- a/tests/templates/kuttl/aas-user-info/10-install-opa.yaml.j2 +++ b/tests/templates/kuttl/aas-user-info/10-install-opa.yaml.j2 @@ -5,22 +5,6 @@ commands: - script: | kubectl apply -n $NAMESPACE -f - <