Skip to content
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

Add configurable authorization logic #435

Merged
merged 6 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions api/agent/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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)
}
Expand Down
16 changes: 16 additions & 0 deletions api/agent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
36 changes: 35 additions & 1 deletion docs/conf/agent/full.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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" }
maia-iyer marked this conversation as resolved.
Show resolved Hide resolved

# 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

Expand Down
4 changes: 2 additions & 2 deletions docs/config-tornjak-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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

Expand Down
7 changes: 1 addition & 6 deletions docs/plugin_server_authentication_keycloak.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
45 changes: 0 additions & 45 deletions docs/plugin_server_authorization_adminviewer.md

This file was deleted.

70 changes: 70 additions & 0 deletions docs/plugin_server_authorization_rbac.md
Original file line number Diff line number Diff line change
@@ -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 "<x>" {desc = "<y>"}` | `<x>` is the name of a role that can be allowed access; `<y>` is a short description | no |
| `API "<x>" {allowed_roles = ["<z1>", ...]}` | `<x>` is the name of the API that will allow access to roles listed such as `<z1>` | 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 "<x>" {...}` where `x` is not a Tornjak API)
2. If an included API block has an undefined role (There exists `API "<x>" {allowed_roles = [..., "<y>", ...]}` such that for all `role "<z>" {...}`, `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.


Binary file modified docs/rsrc/tornjak-backend-plugin-diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/keycloak/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading
Loading