Skip to content

Commit

Permalink
Add configurable authorization logic (#435)
Browse files Browse the repository at this point in the history
  • Loading branch information
maia-iyer authored May 28, 2024
2 parents dbf7bf5 + cd9d73d commit d184312
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 220 deletions.
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" }

# 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

0 comments on commit d184312

Please sign in to comment.