-
Notifications
You must be signed in to change notification settings - Fork 63
Add external Policy Decision Point for Authorization #1170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fcfcc92
82ae3ff
bd9f1f7
9302521
c2b8b9d
e6244cf
6f9e61a
61216b3
2f60fe1
f9aaaa8
96412ee
f6754af
784a96b
945fac0
1f9b5c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,38 @@ | ||
| # Running a Local Keycloak Instance for Authentication | ||
| # Running a Local Keycloak Instance for Authentication and OPA for Authorization | ||
|
|
||
| 1. In this directory, run `docker compose up`. | ||
|
|
||
| This will start three services defined in the Docker Compose file: | ||
| - **Keycloak**: Handles authentication. | ||
| - **oauth2-proxy**: Acts as a proxy to authenticate users. | ||
| - **OPA (Open Policy Agent)**: Manages authorization based on defined policies. The current example uses role-based access control, granting permissions according to user roles. | ||
|
|
||
| 2. Start the Tiled server using the configuration file located at `example_configs/keycloak_oidc/config.yaml`. | ||
| 3. Open your browser and go to [http://localhost:4180](http://localhost:4180) (served by oauth2-proxy). You will be prompted to log in. Use `admin` for both the username and password. | ||
|
|
||
| 4. After logging in as `admin`, you will have access to all resources. | ||
|
|
||
| The diagram below illustrates how the different services work together to provide authentication and authorization for the Tiled server. | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| actor User | ||
| participant OAuth2Proxy as OAuth2 Proxy | ||
| participant Keycloak | ||
| participant Tiled | ||
| participant OPA as Open Policy Agent (OPA) | ||
| User->>OAuth2Proxy: Request access to application | ||
| OAuth2Proxy->>Keycloak: Redirect user for authentication | ||
| activate Keycloak | ||
| Keycloak-->>OAuth2Proxy: Return JWT Access Token | ||
| deactivate Keycloak | ||
| OAuth2Proxy->>Tiled: Forward request with JWT Access Token | ||
| Tiled->>OPA: Validate token & request authorization | ||
| activate OPA | ||
| OPA-->>Tiled: Return authorization decision (allow/deny) | ||
| deactivate OPA | ||
| Tiled->>User: Provide resources if authentication & authorization succeed | ||
| ``` | ||
|
|
||
| > **Note:** This configuration exposes all secrets and passwords to make it easier to use as an example. **Do not use this setup in production.** | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import numpy | ||
|
|
||
| from tiled.client import from_uri | ||
|
|
||
| client = from_uri("http://localhost:4180") | ||
|
|
||
| client.write_array(access_tags=["public"], array=numpy.ones((10, 10)), key="A") | ||
| client.write_array(access_tags=["beamline_x_user"], array=numpy.ones((10, 10)), key="B") | ||
| client.write_array(access_tags=["beamline_y_user"], array=numpy.ones((10, 10)), key="C") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package rbac | ||
|
|
||
| import data.token | ||
|
|
||
| public_tag := {"public"} | ||
|
|
||
| admin_tag := "facility_admin" | ||
| tag_permissions := { | ||
| "beamline_y_user": [ | ||
| "read:data", | ||
| "read:metadata", | ||
| ], | ||
| admin_tag: [ | ||
| "read:data", | ||
| "read:metadata", | ||
| "write:data", | ||
| "write:metadata", | ||
| "create", | ||
| "register", | ||
| "delete:node", | ||
| "delete:revision" | ||
| ], | ||
| "beamline_x_user": [ | ||
| "read:data", | ||
| "read:metadata", | ||
| ], | ||
| "public": [ | ||
| "read:data", | ||
| "write:data", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This tag configures what unauthenticated requests can do. Generally I would expect those would never be allowed to write (or create, register, delete). |
||
| "read:metadata", | ||
| "write:metadata", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the intent here to make |
||
| "create", | ||
| "register", | ||
| "delete:node", | ||
| "delete:revision" | ||
| ], | ||
| } | ||
|
|
||
| users := { | ||
| "alice": {"tags": ["beamline_x_user"]}, | ||
| "bob": {"tags": ["beamline_y_user"]}, | ||
| "cara": {"tags": [admin_tag]}, | ||
| "admin": {"tags": [admin_tag, "beamline_x_user"]}, | ||
| } | ||
|
|
||
| default is_admin := false | ||
|
|
||
| is_admin if { | ||
| admin_tag in users[token.name].tags | ||
| } | ||
|
|
||
| tags contains tag if { | ||
| some tag in users[token.name].tags | ||
| } | ||
|
|
||
| tags contains tag if { | ||
| some tag in public_tag | ||
| } | ||
|
|
||
| tags contains tag if { | ||
| is_admin | ||
| some tag in object.keys(tag_permissions) | ||
| } | ||
|
|
||
| input_tags contains tag if some tag in input.access_blob.tags | ||
| allowed_tags := tags & input_tags | ||
|
|
||
| scopes contains p if { | ||
| some tag in allowed_tags | ||
| some p in tag_permissions[tag] | ||
| } | ||
|
|
||
| scopes contains p if { | ||
| is_admin | ||
| some p in tag_permissions.public | ||
| } | ||
|
|
||
| tag_valid if { | ||
| every tag in input_tags { | ||
| tag in object.keys(tag_permissions) | ||
| } | ||
| } | ||
|
|
||
| user_tags contains tag if some tag in users[token.name].tags | ||
|
|
||
| extra_tags := input_tags - user_tags | ||
|
|
||
| default allow := false | ||
|
|
||
| allow if { | ||
| tag_valid | ||
| count(extra_tags) == 0 | ||
| } | ||
|
|
||
| allow if { | ||
| tag_valid | ||
| is_admin | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| package rbac_test | ||
|
|
||
| import data.rbac | ||
|
|
||
| admin_tag := "facility_admin" | ||
|
|
||
| users := { | ||
| "alice": {"tags": ["beamline_x_user"]}, | ||
| "bob": {"tags": ["facility_user"]}, | ||
| "cara": {"tags": [admin_tag]}, | ||
| "admin": {"tags": [admin_tag, "beamline_x_user"]}, | ||
| } | ||
|
|
||
| test_allowed_to_every_tag_if_admin if { | ||
| rbac.allow with input as {"access_blob": {"tags": ["public"]}} | ||
| with data.token as {"name": "admin"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_not_allowed_to_add_invalid_tags if { | ||
| not rbac.allow with input as {"access_blob": {"tags": ["beamline_y_user"]}} | ||
| with data.token as {"name": "admin"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_user_allowed_to_add_user_tags if { | ||
| rbac.allow with input as {"access_blob": {"tags": ["facility_user"]}} | ||
| with data.token as {"name": "bob"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_user_not_allowed_to_add_invalid_tags if { | ||
| not rbac.allow with input as {"access_blob": {"tags": ["beamline_x_user"]}} | ||
| with data.token as {"name": "bob"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_user_is_admin if { | ||
| rbac.is_admin with data.token as {"name": "admin"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_user_is_not_admin if { | ||
| not rbac.is_admin with data.token as {"name": "alice"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_admin_has_all_tags if { | ||
| rbac.tags == {"facility_user", "facility_admin", "beamline_x_user", "public"} with data.token as {"name": "admin"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_beamline_user_has_only_beamline_tags if { | ||
| rbac.tags == {"beamline_x_user", "public"} with data.token as {"name": "alice"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_allowed_tags if { | ||
| rbac.allowed_tags = {"beamline_x_user"} with input as {"access_blob": {"tags": ["beamline_x_user"]}} | ||
| with data.token as {"name": "admin"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_allowed_tags_for_public_tag if { | ||
| rbac.allowed_tags == {"public"} with input as {"access_blob": {"tags": ["public"]}} | ||
| with data.token as {"name": "alice"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_allowed_scopes_for_admin if { | ||
| rbac.scopes == { | ||
| "read:data", | ||
| "read:metadata", | ||
| "write:data", | ||
| "write:metadata", | ||
| "create", | ||
| "register", | ||
| } with input as {"access_blob": {"tags": ["facility_admin"]}} | ||
| with data.token as {"name": "admin"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_allowed_scopes_for_admin_for_any_resource if { | ||
| rbac.scopes == { | ||
| "read:data", | ||
| "read:metadata", | ||
| "write:data", | ||
| "write:metadata", | ||
| "create", | ||
| "register", | ||
| } with input as {"access_blob": {"tags": ["beamline_x_user"]}} | ||
| with data.token as {"name": "admin"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_allowed_scopes_for_unauthorized_user if { | ||
| count(rbac.scopes) == 0 with input as {"access_blob": {"tags": ["facility_admin"]}} | ||
| with data.token as {"name": "alice"} | ||
| with rbac.users as users | ||
| } | ||
|
|
||
| test_allowed_scopes_for_user if { | ||
| rbac.scopes == { | ||
| "read:data", | ||
| "read:metadata", | ||
| } with input as {"access_blob": {"tags": ["beamline_x_user"]}} | ||
| with data.token as {"name": "alice"} | ||
| with rbac.users as users | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package token | ||
|
|
||
| issuer := opa.runtime().env.ISSUER | ||
|
|
||
| jwks_endpoint := jwks_endpoint if { | ||
| metadata := http.send({ | ||
| "url": concat("", [issuer, "/.well-known/openid-configuration"]), | ||
DiamondJoseph marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "method": "GET", | ||
| "force_cache": true, | ||
| "force_cache_duration_seconds": 86400, | ||
| }).body | ||
| jwks_endpoint := metadata.jwks_uri | ||
| } | ||
|
|
||
| fetch_jwks(url) := http.send({ | ||
| "url": url, | ||
| "method": "GET", | ||
| "force_cache": true, | ||
| "force_cache_duration_seconds": 86400, | ||
| }) | ||
|
|
||
| unverified := io.jwt.decode(input.token) | ||
|
|
||
| jwt_header := unverified[0] | ||
|
|
||
| jwks_url := concat("?", [jwks_endpoint, urlquery.encode_object({"kid": jwt_header.kid})]) | ||
|
|
||
| jwks := fetch_jwks(jwks_url).raw_body | ||
|
|
||
| verified := io.jwt.decode_verify(input.token, { | ||
| "cert": jwks, | ||
| "iss": issuer, | ||
| "aud": input.audience, | ||
| }) | ||
|
|
||
| claims := verified[2] if verified[0] | ||
|
|
||
| roles := claims.realm_access.roles | ||
| name := claims.preferred_username | ||
Uh oh!
There was an error while loading. Please reload this page.