diff --git a/api/agent/server.go b/api/agent/server.go index a52eced0..3d647f3f 100644 --- a/api/agent/server.go +++ b/api/agent/server.go @@ -794,12 +794,12 @@ func NewAuthenticator(authenticatorPlugin *ast.ObjectItem) (authenticator.Authen // decode config to struct var config pluginAuthenticatorKeycloak if err := hcl.DecodeObject(&config, data); err != nil { - return nil, errors.Errorf("Couldn't parse Auth config: %v", err) + return nil, errors.Errorf("Couldn't parse Authenticator config: %v", err) } // Log warning if audience is nil that aud claim is not checked if config.Audience == "" { - fmt.Printf("WARNING: Auth plugin has no expected audience configured - `aud` claim will not be checked (please populate 'config > plugins > UserManagement KeycloakAuth > plugin_data > audience')") + fmt.Println("WARNING: Auth plugin has no expected audience configured - `aud` claim will not be checked (please populate 'config > plugins > UserManagement KeycloakAuth > plugin_data > audience')") } // create authenticator TODO make json an option? @@ -815,12 +815,37 @@ func NewAuthenticator(authenticatorPlugin *ast.ObjectItem) (authenticator.Authen // NewAuthorizer returns a new Authorizer func NewAuthorizer(authorizerPlugin *ast.ObjectItem) (authorization.Authorizer, error) { - key, _, _ := getPluginConfig(authorizerPlugin) + key, data, _ := getPluginConfig(authorizerPlugin) switch key { - case "AdminViewer": - // this is an empty plugin with no config - a static authorization logic example - authorizer, err := authorization.NewAdminViewerAuthorizer() + case "RBAC": + // check if data is defined + if data == nil { + return nil, errors.New("RBAC Authorizer plugin ('config > plugins > Authorizer RBAC > plugin_data') not populated") + } + fmt.Printf("Authorizer RBAC Plugin Data: %+v\n", data) + + // decode config to struct + var config pluginAuthorizerRBAC + if err := hcl.DecodeObject(&config, data); err != nil { + return nil, errors.Errorf("Couldn't parse Authorizer config: %v", err) + } + + // decode into role list and apiMapping + roleList := make(map[string]string) + apiMapping := make(map[string][]string) + for _, role := range config.RoleList { + roleList[role.Name] = role.Desc + // print warning for empty string + if role.Name == "" { + fmt.Println("WARNING: using the empty string for an API enables access to all authenticated users") + } + } + for _, api := range config.APIRoleMappings { + apiMapping[api.Name] = api.AllowedRoles + } + + authorizer, err := authorization.NewRBACAuthorizer(config.Name, roleList, apiMapping) if err != nil { return nil, errors.Errorf("Couldn't configure Authorizer: %v", err) } diff --git a/api/agent/types.go b/api/agent/types.go index 7c2850d9..b5e39911 100644 --- a/api/agent/types.go +++ b/api/agent/types.go @@ -112,3 +112,19 @@ type pluginAuthenticatorKeycloak struct { IssuerURL string `hcl:"issuer"` Audience string `hcl:"audience"` } + +type AuthRole struct { + Name string `hcl:",key"` + Desc string `hcl:"desc"` +} + +type APIRoleMapping struct { + Name string `hcl:",key"` + AllowedRoles []string `hcl:"allowed_roles"` +} + +type pluginAuthorizerRBAC struct { + Name string `hcl:"name"` + RoleList []*AuthRole `hcl:"role,block"` + APIRoleMappings []*APIRoleMapping `hcl:"API,block"` +} diff --git a/docs/conf/agent/full.conf b/docs/conf/agent/full.conf index 09675a56..e1814daa 100644 --- a/docs/conf/agent/full.conf +++ b/docs/conf/agent/full.conf @@ -41,6 +41,8 @@ plugins { ### BEGIN IAM PLUGIN CONFIGURATION ### # Note: if no UserManagement configuration included, authentication treated as noop + # This plugin will extract roles from `realm_access.roles` in the JWT and pass + # to the authorization layer as user roles. Authenticator "Keycloak" { plugin_data { # issuer - Issuer URL for OIDC @@ -56,7 +58,39 @@ plugins { } } - Authorizer "AdminViewer" {} + # This policy requires admin role for all write calls, viewer role for all read calls + # and authentication success for the "/" api + Authorizer "RBAC" { + plugin_data { + name = "Admin Viewer Policy" + role "admin" { desc = "admin person" } + role "viewer" { desc = "viewer person" } + # this special character role is reserved for allowing all authenticated persons + role "" { desc = "authenticated person" } + + # home tornjak backend api allowed with any successful authentication + API "/" { allowed_roles = [""] } + # allowed with successful authentication and either admin or viewer role + API "/api/healthcheck" { allowed_roles = ["admin", "viewer"] } + API "/api/debugserver" { allowed_roles = ["admin", "viewer"] } + API "/api/agent/list" { allowed_roles = ["admin", "viewer"] } + API "/api/entry/list" { allowed_roles = ["admin", "viewer"] } + API "/api/tornjak/serverinfo" { allowed_roles = ["admin", "viewer"] } + API "/api/tornjak/selectors/list" { allowed_roles = ["admin", "viewer"] } + API "/api/tornjak/agents/list" { allowed_roles = ["admin", "viewer"] } + API "/api/tornjak/clusters/list" { allowed_roles = ["admin", "viewer"] } + # allowed with successful authentication and admin role + API "/api/agent/ban" { allowed_roles = ["admin"] } + API "/api/agent/delete" { allowed_roles = ["admin"] } + API "/api/agent/createjointoken" { allowed_roles = ["admin"] } + API "/api/entry/create" { allowed_roles = ["admin"] } + API "/api/entry/delete" { allowed_roles = ["admin"] } + API "/api/tornjak/selectors/register" { allowed_roles = ["admin"] } + API "/api/tornjak/clusters/create" { allowed_roles = ["admin"] } + API "/api/tornjak/clusters/edit" { allowed_roles = ["admin"] } + API "/api/tornjak/clusters/delete" { allowed_roles = ["admin"] } + } + } ### END IAM PLUGIN CONFIGURATION diff --git a/docs/config-tornjak-server.md b/docs/config-tornjak-server.md index f281a46e..0ff338c3 100644 --- a/docs/config-tornjak-server.md +++ b/docs/config-tornjak-server.md @@ -63,7 +63,7 @@ For examples on enabling TLS and mTLS connections, please see [our TLS and mTLS ## About Tornjak plugins -Tornjak supports several different plugin types, each representing a different functionality. The diagram below shows how each of the plugins fit into the backend: +Tornjak supports several different plugin types, each representing a different functionality. The diagram below shows how each of the plugin types fit into the backend: ![tornjak backend plugin diagram](./rsrc/tornjak-backend-plugin-diagram.png) @@ -81,7 +81,7 @@ Tornjak supports several different plugin types, each representing a different f | ---- | ---- | ----------- | | DataStore | [sql]() | Default SQL storage for Tornjak metadata | | Authenticator | [keycloak](/docs/plugin_server_authentication_keycloak.md) | Perform OIDC Discovery and extract roles from `realmAccess.roles` field | -| Authorizer | [adminviewer](/docs/plugin_server_authorization_adminviewer.md) | Check api permission based on user role and static authorization logic | +| Authorizer | [RBAC](/docs/plugin_server_authorization_rbac.md) | Check api permission based on user role and defined authorization logic | ### Plugin configuration diff --git a/docs/plugin_server_authentication_keycloak.md b/docs/plugin_server_authentication_keycloak.md index e7e564e5..c81900db 100644 --- a/docs/plugin_server_authentication_keycloak.md +++ b/docs/plugin_server_authentication_keycloak.md @@ -27,11 +27,6 @@ It is highly recommended `audience` is populated to ensure only tokens meant for ## User Info extracted -This plugin assumes roles are available in `realm_access.roles` in the JWT and maps the following values: - -| JWT | Mapped role | -| ------------------------------ | --------------------- | -| `tornjak-viewer-realm-role` | `viewer` | -| `tornjak-admin-realm-role` | `admin` | +This plugin assumes roles are available in `realm_access.roles` in the JWT and passes this list as user.roles. These mapped values are passed to the authorization layer. diff --git a/docs/plugin_server_authorization_adminviewer.md b/docs/plugin_server_authorization_adminviewer.md deleted file mode 100644 index 31d94d37..00000000 --- a/docs/plugin_server_authorization_adminviewer.md +++ /dev/null @@ -1,45 +0,0 @@ -# Server plugin: Authorization "AdminViewer" - -Please see our documentation on the [authorization feature](./user-management.md) for more complete details. - -The configuration has no inputs. Simply creating a section as below is sufficient for enabling the authorization layer: - -A sample configuration file for syntactic referense is below: - -```hcl - Authorizer "AdminViewer" {} -``` - -NOTE: If this feature is enabled without an authentication layer, it will render essentially all calls unauthorizable. - -## Authorization logic implemented - -This plugin assumes roles `admin` and `viewer` are passed by the authentication layer. - -From this information, the following logic is applied: - -1. If user has `admin` role, can perform any call -2. If user has `viewer` role, can perform all read-only calls (See lists below) -3. If user is authenticated with no role, can perform only `/` Tornjak home call. - -List of read-only calls: -- `/` -- `/api/healthcheck` -- `/api/debugserver` -- `/api/agent/list` -- `/api/entry/list` -- `/api/tornjak/serverinfo` -- `/api/tornjak/selectors/list` -- `/api/tornjak/agents/list` -- `/api/tornjak/clusters/list` - -List of writing (admin-only) calls: -- `/api/agent/ban` -- `/api/agent/delete` -- `/api/agent/createjointoken` -- `/api/entry/create` -- `/api/entry/delete` -- `/api/tornjak/selectors/register` -- `/api/tornjak/clusters/create` -- `/api/tornjak/clusters/edit` -- `/api/tornjak/clusters/delete` diff --git a/docs/plugin_server_authorization_rbac.md b/docs/plugin_server_authorization_rbac.md new file mode 100644 index 00000000..781e7ef9 --- /dev/null +++ b/docs/plugin_server_authorization_rbac.md @@ -0,0 +1,70 @@ +# Server plugin: Authorization "RBAC" + +Please see our documentation on the [authorization feature](./user-management.md) for more complete details. + +This configuration has the following inputs: + +| Key | Description | Required | +| --- | ----------- | -------- | +| name | name of the policy for logging purposes | no | +| `role "" {desc = ""}` | `` is the name of a role that can be allowed access; `` is a short description | no | +| `API "" {allowed_roles = ["", ...]}` | `` is the name of the API that will allow access to roles listed such as `` | no | + +There can (and likely will be) multiple `role` and `API` blocks. If there are no role blocks, no API will be allowed any access. If there is a missing API block, no access will be granted for that API. + +A sample configuration file for syntactic referense is below: + +```hcl +Authorizer "RBAC" { + plugin_data { + name = "Admin Viewer Policy" + role "admin" { desc = "admin person" } + role "viewer" { desc = "viewer person" } + role "" { desc = "authenticated person" } + + API "/" { allowed_roles = [""] } + API "/api/healthcheck" { allowed_roles = ["admin", "viewer"] } + API "/api/debugserver" { allowed_roles = ["admin", "viewer"] } + API "/api/agent/list" { allowed_roles = ["admin", "viewer"] } + API "/api/entry/list" { allowed_roles = ["admin", "viewer"] } + API "/api/tornjak/serverinfo" { allowed_roles = ["admin", "viewer"] } + API "/api/tornjak/selectors/list" { allowed_roles = ["admin", "viewer"] } + API "/api/tornjak/agents/list" { allowed_roles = ["admin", "viewer"] } + API "/api/tornjak/clusters/list" { allowed_roles = ["admin", "viewer"] } + API "/api/agent/ban" { allowed_roles = ["admin"] } + API "/api/agent/delete" { allowed_roles = ["admin"] } + API "/api/agent/createjointoken" { allowed_roles = ["admin"] } + API "/api/entry/create" { allowed_roles = ["admin"] } + API "/api/entry/delete" { allowed_roles = ["admin"] } + API "/api/tornjak/selectors/register" { allowed_roles = ["admin"] } + API "/api/tornjak/clusters/create" { allowed_roles = ["admin"] } + API "/api/tornjak/clusters/edit" { allowed_roles = ["admin"] } + API "/api/tornjak/clusters/delete" { allowed_roles = ["admin"] } + } +} +``` + +NOTE: If this feature is enabled without an authentication layer, it will render all calls uncallable. + +The above specification assumes roles `admin` and `viewer` are passed by the authentication layer. In this example, the following apply: + +1. If user has `admin` role, can perform any call +2. If user has `viewer` role, can perform all read-only calls (See lists below) +3. If user is authenticated with no role, can perform only `/` Tornjak home call. + +## Valid inputs + +There are a couple failure cases in which the plugin will fail to initialize and the Tornjak backend will not run: + +1. If an included API block has an undefined API (`API "" {...}` where `x` is not a Tornjak API) +2. If an included API block has an undefined role (There exists `API "" {allowed_roles = [..., "", ...]}` such that for all `role "" {...}`, `y != z`) + +## The empty string role "" + +If there is a role listed with name `""`, this enables some APIs to allow all users where the authentication layer does not return error. In the above example, only the `/` API has this behavior. + +## Additional behavior specification + +If there is a role that is not included as an `allowed_role` in any API block, a user will not be granted access to any API based on that role. + + diff --git a/docs/rsrc/tornjak-backend-plugin-diagram.png b/docs/rsrc/tornjak-backend-plugin-diagram.png index cdc15cae..aa69be80 100644 Binary files a/docs/rsrc/tornjak-backend-plugin-diagram.png and b/docs/rsrc/tornjak-backend-plugin-diagram.png differ diff --git a/examples/keycloak/README.md b/examples/keycloak/README.md index 0543532e..c0dfe2be 100644 --- a/examples/keycloak/README.md +++ b/examples/keycloak/README.md @@ -21,4 +21,4 @@ and open the *Administration Console* The credentials in this example have username and password both `admin`. You may configure this in `statefulset.yaml` -The Tornjak Realm has two users: `admin` and `viewer`. +The Tornjak Realm has two users with usernames: `admin` and `viewer`, and passwords `admin` and `viewer` respectively. diff --git a/examples/keycloak/config.yaml b/examples/keycloak/config.yaml index 31f13861..417eeaa6 100644 --- a/examples/keycloak/config.yaml +++ b/examples/keycloak/config.yaml @@ -51,24 +51,6 @@ data: "failureFactor": 30, "roles": { "realm": [ - { - "id": "f7447da4-8316-41b3-a424-cc39f850e216", - "name": "tornjak-viewer-realm-role", - "description": "", - "composite": false, - "clientRole": false, - "containerId": "f328c058-824c-4f64-bf73-6b45b0fbc914", - "attributes": {} - }, - { - "id": "ef18c2b2-cd99-465d-9d06-d717365b070c", - "name": "tornjak-admin-realm-role", - "description": "", - "composite": false, - "clientRole": false, - "containerId": "f328c058-824c-4f64-bf73-6b45b0fbc914", - "attributes": {} - }, { "id": "9e2d7e27-e931-45a8-a44f-05175500005e", "name": "uma_authorization", @@ -99,6 +81,24 @@ data: "containerId": "f328c058-824c-4f64-bf73-6b45b0fbc914", "attributes": {} }, + { + "id": "2593f3ba-6607-4fce-84b9-411b427330be", + "name": "viewer", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "f328c058-824c-4f64-bf73-6b45b0fbc914", + "attributes": {} + }, + { + "id": "be442260-4450-448c-be76-c11b3d0fd483", + "name": "admin", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "f328c058-824c-4f64-bf73-6b45b0fbc914", + "attributes": {} + }, { "id": "e31fd018-82ff-48fb-bd02-5a440fb3e5e4", "name": "offline_access", @@ -340,12 +340,7 @@ data: { "id": "65a0126a-1a3e-4af0-ac2c-6c7926d4cc91", "name": "viewer", - "composite": true, - "composites": { - "realm": [ - "tornjak-viewer-realm-role" - ] - }, + "composite": false, "clientRole": true, "containerId": "5879eb39-2121-4fa6-93cf-6e91392e0508", "attributes": {} @@ -353,12 +348,7 @@ data: { "id": "78df9ca6-7976-4dd1-b2c5-2e09c1d8bc45", "name": "admin", - "composite": true, - "composites": { - "realm": [ - "tornjak-admin-realm-role" - ] - }, + "composite": false, "clientRole": true, "containerId": "5879eb39-2121-4fa6-93cf-6e91392e0508", "attributes": {} @@ -452,7 +442,7 @@ data: "path": "/admin", "attributes": {}, "realmRoles": [ - "tornjak-admin-realm-role" + "admin" ], "clientRoles": {}, "subGroups": [] @@ -463,7 +453,7 @@ data: "path": "/viewer", "attributes": {}, "realmRoles": [ - "tornjak-viewer-realm-role" + "viewer" ], "clientRoles": {}, "subGroups": [] @@ -627,7 +617,8 @@ data: "config": { "id.token.claim": "false", "access.token.claim": "true", - "included.custom.audience": "tornjak-backend" + "included.custom.audience": "tornjak-backend", + "userinfo.token.claim": "false" } } ], @@ -1482,14 +1473,14 @@ data: "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", - "oidc-full-name-mapper", + "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", - "saml-user-attribute-mapper" + "saml-user-property-mapper" ] } }, @@ -1502,13 +1493,13 @@ data: "config": { "allowed-protocol-mapper-types": [ "oidc-sha256-pairwise-sub-mapper", - "saml-user-attribute-mapper", - "oidc-full-name-mapper", "saml-role-list-mapper", - "saml-user-property-mapper", + "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", - "oidc-address-mapper" + "oidc-address-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper" ] } }, @@ -1637,7 +1628,7 @@ data: "supportedLocales": [], "authenticationFlows": [ { - "id": "4088582a-11d7-4f3f-a8e8-55d32a1c76b3", + "id": "a0d2ae19-1a29-4bf4-ae40-c4876fc9c691", "alias": "Account verification options", "description": "Method with which to verity the existing account", "providerId": "basic-flow", @@ -1663,7 +1654,7 @@ data: ] }, { - "id": "9ea5725d-b25b-4e91-969f-b8f3ac55a9ea", + "id": "1cadc6d6-e038-4728-b2d7-606d994ad926", "alias": "Authentication Options", "description": "Authentication options.", "providerId": "basic-flow", @@ -1697,7 +1688,7 @@ data: ] }, { - "id": "60526891-c368-4a1d-a575-c0b58ba36cbe", + "id": "2944a1f5-c2d0-4878-a403-5d9b84cf9cf5", "alias": "Browser - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1723,7 +1714,7 @@ data: ] }, { - "id": "6a763267-a45d-453a-be79-f26e063dab09", + "id": "70342ff7-5c14-44df-ba0f-f1f7a3bf5d6e", "alias": "Direct Grant - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1749,7 +1740,7 @@ data: ] }, { - "id": "6dc06302-aeda-49cb-98c5-15d8d2ed9024", + "id": "a0a4f27b-0f47-4341-8e48-96e98152063c", "alias": "First broker login - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1775,7 +1766,7 @@ data: ] }, { - "id": "f9b3f24c-4039-4165-9822-be6665ea4647", + "id": "d6af0bbb-e085-4ef8-a0fa-775bc6a20391", "alias": "Handle Existing Account", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId": "basic-flow", @@ -1801,7 +1792,7 @@ data: ] }, { - "id": "085ea431-fead-4581-92fb-408cbe566a09", + "id": "17f8b8d5-ea2e-48e8-96d3-8f860072dd7b", "alias": "Reset - Conditional OTP", "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId": "basic-flow", @@ -1827,7 +1818,7 @@ data: ] }, { - "id": "c468f235-6276-40cb-8cbf-1649890c4d6e", + "id": "9ccee1f8-ca00-41ee-8f33-846a881a17d9", "alias": "User creation or linking", "description": "Flow for the existing/non-existing user alternatives", "providerId": "basic-flow", @@ -1854,7 +1845,7 @@ data: ] }, { - "id": "d591d312-f782-4bd1-8ad1-aa940a3d9f37", + "id": "66b2e00f-520b-416f-9c22-68e737447cc2", "alias": "Verify Existing Account by Re-authentication", "description": "Reauthentication of existing account", "providerId": "basic-flow", @@ -1880,7 +1871,7 @@ data: ] }, { - "id": "e3229dcb-b8cc-4d85-a493-665bf70c1bb9", + "id": "86d97d48-a1b0-4e5e-a523-95d72254707e", "alias": "browser", "description": "browser based authentication", "providerId": "basic-flow", @@ -1922,7 +1913,7 @@ data: ] }, { - "id": "192229a4-987b-4fd4-ab9b-8af694cbcbff", + "id": "dc57eaf8-1355-44ac-9320-d7b33075920f", "alias": "clients", "description": "Base authentication for clients", "providerId": "client-flow", @@ -1964,7 +1955,7 @@ data: ] }, { - "id": "95031e63-79ba-4592-9b34-adc3fe48c49b", + "id": "9bd62cd6-001b-4b89-80b0-0f738d1b6ece", "alias": "direct grant", "description": "OpenID Connect Resource Owner Grant", "providerId": "basic-flow", @@ -1998,7 +1989,7 @@ data: ] }, { - "id": "bb6c332b-3b65-4a07-a9b2-96483504bccd", + "id": "d7d1fa18-14e8-4a24-a869-5a2491708c8d", "alias": "docker auth", "description": "Used by Docker clients to authenticate against the IDP", "providerId": "basic-flow", @@ -2016,7 +2007,7 @@ data: ] }, { - "id": "5d3ccb15-7b2a-4396-a71a-381c523ce344", + "id": "ece951a2-3897-420b-b017-515e6b50fb7d", "alias": "first broker login", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId": "basic-flow", @@ -2043,7 +2034,7 @@ data: ] }, { - "id": "fcfc4df2-a50b-4094-8b91-81ef05a6d446", + "id": "1cbe0eca-7666-4525-99c8-b55e8b2c613e", "alias": "forms", "description": "Username, password, otp and other auth forms.", "providerId": "basic-flow", @@ -2069,7 +2060,7 @@ data: ] }, { - "id": "e5eeb267-7d6e-4232-b687-da1dbbbe6fbd", + "id": "6a48989a-c1ba-4146-8234-61c923dfed93", "alias": "http challenge", "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId": "basic-flow", @@ -2095,7 +2086,7 @@ data: ] }, { - "id": "b01af0c8-30e7-463c-81d0-fccbfb6cbe78", + "id": "68f379a9-00b4-4981-977f-3e1714f1b925", "alias": "registration", "description": "registration flow", "providerId": "basic-flow", @@ -2114,7 +2105,7 @@ data: ] }, { - "id": "e405584c-04cb-41ae-a1e6-09742bd06175", + "id": "45835836-efe7-41c5-98ef-ea4d7ff6ea95", "alias": "registration form", "description": "registration form", "providerId": "form-flow", @@ -2156,7 +2147,7 @@ data: ] }, { - "id": "92174ab4-8f40-4d0c-b47f-578545e65d72", + "id": "4c8fdfeb-b20e-4d34-8f53-5f9a9fafcba2", "alias": "reset credentials", "description": "Reset credentials for a user if they forgot their password or something", "providerId": "basic-flow", @@ -2198,7 +2189,7 @@ data: ] }, { - "id": "22fdad5c-a195-423a-a95c-67af0ddf7874", + "id": "ca564ca1-2253-44f3-bad0-15efeb995f6b", "alias": "saml ecp", "description": "SAML ECP Profile Authentication Flow", "providerId": "basic-flow", @@ -2218,14 +2209,14 @@ data: ], "authenticatorConfig": [ { - "id": "bee29af0-0b4b-402d-a514-ebf57aa8701d", + "id": "5cabfdc6-2cc7-453f-bf83-83d29c1ffba2", "alias": "create unique user config", "config": { "require.password.update.after.registration": "false" } }, { - "id": "22d06d43-b441-44ba-b3d5-fe96e3afd5bc", + "id": "de70db4a-e3ab-4b8a-84af-02501f5974b4", "alias": "review profile config", "config": { "update.profile.on.first.login": "missing" diff --git a/pkg/agent/authentication/authenticator/keycloak.go b/pkg/agent/authentication/authenticator/keycloak.go index 501e3895..0a7571fa 100644 --- a/pkg/agent/authentication/authenticator/keycloak.go +++ b/pkg/agent/authentication/authenticator/keycloak.go @@ -31,11 +31,6 @@ type KeycloakAuthenticator struct { audience string } -var StaticRoleMappings = map[string]string{ - "tornjak-viewer-realm-role": "viewer", - "tornjak-admin-realm-role": "admin", -} - func getJWKeyFunc(httpjwks bool, jwksInfo string) (*keyfunc.JWKS, error) { if httpjwks { opts := keyfunc.Options{ // TODO add options to config file @@ -101,17 +96,6 @@ func getToken(r *http.Request, redirectURL string) (string, error) { } -func (a *KeycloakAuthenticator) TranslateToTornjakRoles(roles []string) ([]string){ - var translatedRoles []string - for _, role := range roles { - tornjakRole, ok := StaticRoleMappings[role] - if ok { - translatedRoles = append(translatedRoles, tornjakRole) - } - } - return translatedRoles -} - func wrapAuthenticationError(err error) (*user.UserInfo) { return &user.UserInfo{ AuthenticationError: err, @@ -137,10 +121,7 @@ func (a *KeycloakAuthenticator) AuthenticateRequest(r *http.Request)(*user.UserI return wrapAuthenticationError(errors.New("Token invalid")) } - // translate roles to tornjak roles - tornjakRoles := a.TranslateToTornjakRoles(claims.RealmAccess.Roles) - return &user.UserInfo{ - Roles: tornjakRoles, + Roles: claims.RealmAccess.Roles, } } diff --git a/pkg/agent/authorization/adminviewer.go b/pkg/agent/authorization/adminviewer.go deleted file mode 100644 index 5b96991d..00000000 --- a/pkg/agent/authorization/adminviewer.go +++ /dev/null @@ -1,77 +0,0 @@ -package authorization - -import ( - "net/http" - "github.com/pkg/errors" - - "github.com/spiffe/tornjak/pkg/agent/authentication/user" -) - -type ReadWriteAuthorizer struct { - api_permissions map[string][]string -} - -func getAuthMap() (map[string][]string) { - // api call matches to list of strings, representing disjunction of requirements - api_permissions := map[string][]string{ - // no auth token needed for Tornjak Home - "/": []string{}, - - // viewer - "/api/healthcheck": []string{"admin", "viewer"}, - "/api/debugserver": []string{"admin", "viewer"}, - "/api/agent/list": []string{"admin", "viewer"}, - "/api/entry/list": []string{"admin", "viewer"}, - "/api/tornjak/serverinfo": []string{"admin", "viewer"}, - "/api/tornjak/selectors/list": []string{"admin", "viewer"}, - "/api/tornjak/agents/list": []string{"admin", "viewer"}, - "/api/tornjak/clusters/list": []string{"admin", "viewer"}, - // admin - "/api/agent/ban": []string{"admin"}, - "/api/agent/delete": []string{"admin"}, - "/api/agent/createjointoken": []string{"admin"}, - "/api/entry/create": []string{"admin"}, - "/api/entry/delete": []string{"admin"}, - "/api/tornjak/selectors/register": []string{"admin"}, - "/api/tornjak/clusters/create": []string{"admin"}, - "/api/tornjak/clusters/edit": []string{"admin"}, - "/api/tornjak/clusters/delete": []string{"admin"}, - } - return api_permissions -} - - -func NewAdminViewerAuthorizer() (*ReadWriteAuthorizer, error) { - api_permissions := getAuthMap() - return &ReadWriteAuthorizer{ - api_permissions: api_permissions, - }, nil -} - -func (a *ReadWriteAuthorizer) AuthorizeRequest(r *http.Request, u *user.UserInfo) error { - // if not authenticated fail and return error - if u.AuthenticationError != nil { - return errors.Errorf("Authentication error: %v", u.AuthenticationError) - } - - roles := u.Roles - apiPath := r.URL.Path - - allowedRoles := a.api_permissions[apiPath] - - // if no role required, return nil - if len(allowedRoles) == 0 { - return nil - } - - // check if any roles in sufficientRoles - for _, role := range roles { - for _, allowedRole := range allowedRoles { - if role == allowedRole { - return nil - } - } - } - - return errors.New("Unauthorized request") -} diff --git a/pkg/agent/authorization/rbac.go b/pkg/agent/authorization/rbac.go new file mode 100644 index 00000000..0b1e1fe4 --- /dev/null +++ b/pkg/agent/authorization/rbac.go @@ -0,0 +1,97 @@ +package authorization + +import ( + "net/http" + "github.com/pkg/errors" + + "github.com/spiffe/tornjak/pkg/agent/authentication/user" +) + +type RBACAuthorizer struct { + name string + roleList map[string]string + apiMapping map[string][]string +} + +// TODO put this in a common constants file +var staticAPIList = map[string]struct{}{ + "/": {}, + "/api/healthcheck": {}, + "/api/debugserver": {}, + "/api/agent/list": {}, + "/api/entry/list": {}, + "/api/tornjak/serverinfo": {}, + "/api/tornjak/selectors/list": {}, + "/api/tornjak/agents/list": {}, + "/api/tornjak/clusters/list": {}, + "/api/agent/ban": {}, + "/api/agent/delete": {}, + "/api/agent/createjointoken": {}, + "/api/entry/create": {}, + "/api/entry/delete": {}, + "/api/tornjak/selectors/register": {}, + "/api/tornjak/clusters/create": {}, + "/api/tornjak/clusters/edit": {}, + "/api/tornjak/clusters/delete": {}, +} + +func validateInitParameters(roleList map[string]string, apiMapping map[string][]string) error { + for api, allowList := range apiMapping { + // check that API exists + if _, ok := staticAPIList[api]; !ok { + return errors.Errorf("API %s does not exist", api) + } + + // check that each role exists in roleList + for _, allowedRole := range allowList { + if _, ok := roleList[allowedRole]; !ok { + return errors.Errorf("API %s lists undefined role %s", api, allowedRole) + } + } + } + return nil +} + +func NewRBACAuthorizer(policyName string, roleList map[string]string, apiMapping map[string][]string) (*RBACAuthorizer, error) { + err := validateInitParameters(roleList, apiMapping) + if err != nil { + return nil, errors.Errorf("Could not parse policy %s: invalid mapping: %v", policyName, err) + } + return &RBACAuthorizer{ + name: policyName, + roleList: roleList, + apiMapping: apiMapping, + }, nil +} + +func (a *RBACAuthorizer) AuthorizeRequest(r *http.Request, u *user.UserInfo) error { + // if not authenticated fail and return error + if u.AuthenticationError != nil { + return errors.Errorf("Authentication error: %v", u.AuthenticationError) + } + + userRoles := u.Roles + apiPath := r.URL.Path + + allowedRoles := a.apiMapping[apiPath] + + // if no role listed for api, reject + if len(allowedRoles) == 0 { + return errors.New("Unauthorized request") + } + + // check each allowed role + for _, allowedRole := range allowedRoles { + if allowedRole == "" { // all authenticated allowed + return nil + } + for _, role := range userRoles { + // user has role + if role == allowedRole { + return nil + } + } + } + + return errors.New("Unauthorized request") +}