8. Developer quick start#

Important

This section covers a highly technical topic. It addresses app developers who want to integrate an app with the Guardian. Familiarity with using the command line, working with an HTTP API, and writing code is necessary to understand this section.

This section assumes familiarity with the following Univention documentation:

This section provides a walk-through of the steps necessary to integrate an app with the Guardian. It refers to the example outlined in What is the Guardian?:

Example Organization is an app developer who creates Cake Express, which allows people to order cakes for company events. Administrators can install the app from Univention App Center. They want to integrate Cake Express with the Guardian.

Note

The example scripts in this section assume that:

  • You installed the app on the same UCS system as the Management API.

  • You installed the app on the same UCS system as the Keycloak server.

If either of these two things isn’t true, you need to find a way for the UCS app infrastructure maintainer to communicate their locations to the script at run time.

8.1. Management API#

This section describes how to get started with the Management API to integrate your app with the Guardian.

8.1.1. Getting a Keycloak token#

The first thing that Example Organization needs to do is to write a join script for its app. The app needs to interact with the Management API. To do this, the join script must obtain a token from Keycloak to authenticate all calls to the API.

Listing 8.1 shows the variables you need to obtain a token from Keycloak.

Listing 8.1 Variables for obtaining a token from Keycloak#
binduser=Administrator
bindpwd=password

CLIENT_ID=guardian-scripts

GUARDIAN_KEYCLOAK_URL=$(ucr get guardian-management-api/oauth/keycloak-uri)
SYSTEM_KEYCLOAK_URL=$(ucr get keycloak/server/sso/fqdn)
KEYCLOAK_BASE_URL=${GUARDIAN_KEYCLOAK_URL:-$SYSTEM_KEYCLOAK_URL}

KEYCLOAK_URL="$KEYCLOAK_BASE_URL/realms/ucs/protocol/openid-connect/token"
if [[ ! $KEYCLOAK_URL == http ]]; then
    KEYCLOAK_URL="https://$KEYCLOAK_URL"
fi

Note

In a typical join script, the arguments --binduser, --bindpwd, and --bindpwdfile are available, which specify an administrator user, and either a password or a file for parsing the password.

The example assumes that the join script has already parsed these parameters into the variables binduser and bindpwd.

You can retrieve the token with the following command:

$ token=$(curl -d "client_id=$CLIENT_ID" \
     -d "username=$binduser" \
     -d "password=$bindpwd" \
     -d "grant_type=password" \
     $KEYCLOAK_URL | sed 's/.*"access_token":"\([[:alnum:]\.-_-]*\)".*/\1/')

Important

All commands in subsequent sections reference token. You may need to refresh the token several times, if you are entering commands manually.

8.1.2. Registering an app#

Example Organization now needs to tell the Guardian about its app Cake Express. To do this, it needs to take the token from the previous section and make a request to the Management API.

$ MANAGEMENT_SERVER="$(hostname).$(ucr get domainname)/guardian/management"

$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"cake-express", "display_name":"Cake Express"}' \
    $MANAGEMENT_SERVER/apps/register

Example Organization is now ready to start setting up the Guardian to work with Cake Express.

Important

All names in Guardian are lower-case ASCII alphanumeric with either underscores or hyphens. The encoding for display names is only limited by the character support for the PostgreSQL database that Guardian uses.

8.1.3. Registering namespaces#

A namespace is a categorization to store everything that an app wants to use in Guardian, like roles and permissions.

Every app gets a default namespace to use. Example Organization wants to manage three different facets of Cake Express:

cakes

Category for everything related to what’s actually sold.

orders

Category for administration of orders.

users

Category for managing other users of Cake Express.

Later, Example Organization creates some roles in each of these namespaces for tasks in Cake Express. Listing 8.2 to Listing 8.4 show how Example Organization creates these namespaces for the app in the app’s join script:

Listing 8.2 Create namespace cakes#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"cakes", "display_name":"Cakes"}' \
    $MANAGEMENT_SERVER/namespaces/cake-express
Listing 8.3 Create namespace orders#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"orders", "display_name":"Orders"}' \
    $MANAGEMENT_SERVER/namespaces/cake-express
Listing 8.4 Create namespace users#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"users", "display_name":"Users"}' \
    $MANAGEMENT_SERVER/namespaces/cake-express

8.1.4. Registering roles#

Example Organization wants to create three different roles for users of Cake Express:

cake-express:cakes:cake-orderer

Someone who can order cakes from Cake Express.

cake-express:orders:finance-manager

Someone who manages the expenses for the orders.

cake-express:users:user-manager

Someone who manages other users within Cake Express.

Example Organization also wants to create a role for some of their cakes:

cake-express:cakes:birthday-cake

A cake just for employee birthdays.

Each role consists of the following parts, separated by a colon (:):

  • app: for example cake-express

  • namespace: for example cakes

  • role name: for example cake-orderer

Listing 8.5 to Listing 8.8 show how Example Organization creates these roles for the app in the app’s join script:

Listing 8.5 Create role …:cake-orderer#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"cake-orderer", "display_name":"Cake Orderer"}' \
    $MANAGEMENT_SERVER/roles/cake-express/cakes
Listing 8.6 Create role …:finance-manager#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"finance-manager", "display_name":"Finance Manager"}' \
    $MANAGEMENT_SERVER/roles/cake-express/orders
Listing 8.7 Create role …:user-manager#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"user-manager", "display_name":"User Manager"}' \
    $MANAGEMENT_SERVER/roles/cake-express/users
Listing 8.8 Create role …:birthday-cake#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"birthday-cake", "display_name":"Birthday Cake"}' \
    $MANAGEMENT_SERVER/roles/cake-express/cakes

8.1.5. Registering permissions#

Example Organization wants to provide some permissions that define what users of Cake Express want to do:

cake-express:cakes:order-cake

Users with this permission can order cakes.

cake-express:orders:cancel-order

Users can cancel a cake order.

cake-express:users:manage-notifications

Users can manage cake notifications.

Listing 8.9 to Listing 8.11 show how Example Organization creates these permissions for the app in the app’s join script:

Listing 8.9 Create permission …:order-cake#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"order-cake", "display_name":"order cake"}' \
    $MANAGEMENT_SERVER/permissions/cake-express/cakes
Listing 8.10 Create permission …:cancel-order#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"cancel-order", "display_name":"cancel order"}' \
    $MANAGEMENT_SERVER/permissions/cake-express/orders
Listing 8.11 Create permission …:manage-notifications#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{"name":"manage-notifications", "display_name":"manage notifications"}' \
    $MANAGEMENT_SERVER/permissions/cake-express/users

8.1.6. Registering capabilities#

Finally, Example Organization wants to define some default capabilities for its application. The guardian app administrator who installs Cake Express can change these later. These default capabilities make it easier for Cake Express to work out of the box.

The app would like to create the following capabilities:

  1. Users with the cake-orderer role can order cakes.

  2. Users with the finance-manager role, or the person who ordered the cake, have the permission to cancel the cake order.

  3. Users with the user-manager role have the permission to manage cake notifications. Users can also manage their own notifications for cakes sent to them, except for notifications related to birthday cakes.

8.1.6.1. Create Cake order#

Listing 8.12 shows how Example Organization creates the capability for ordering cake:

Listing 8.12 Create capability for ordering cake#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "name": "cake-orderer-can-order-cake",
          "display_name": "Cake Orderers can order cake",
          "role": {
            "app_name": "cake-express",
            "namespace_name": "cakes",
            "name": "cake-orderer"
          },
          "conditions": [],
          "relation": "AND",
          "permissions": [
            {
              "app_name": "cake-express",
              "namespace_name": "cakes",
              "name": "order-cake"
            }
           ]
        }' \
    $MANAGEMENT_SERVER/capabilities/cake-express/cakes

8.1.6.2. Cancel cake order#

Listing 8.13 and Listing 8.14 show how Example Organization creates the capability for canceling an order. The action requires two POST requests to create the capability:

Listing 8.13 Create capability to cancel a cake order for the finance manager role#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "name": "finance-manager-can-cancel-order",
          "display_name": "Finance Manager can cancel orders",
          "role": {
            "app_name": "cake-express",
            "namespace_name": "orders",
            "name": "finance-manager"
          },
          "conditions": [],
          "relation": "AND",
          "permissions": [
            {
              "app_name": "cake-express",
              "namespace_name": "orders",
              "name": "cancel-order"
            }
          ]
        }' \
    $MANAGEMENT_SERVER/capabilities/cake-express/orders
Listing 8.14 Create capability to cancel a cake order for the user themselves#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "name": "self-can-cancel-order",
          "display_name": "Users can cancel their own order",
          "role": {
            "app_name": "cake-express",
            "namespace_name": "cakes",
            "name": "cake-orderer"
          },
          "conditions": [
            {
              "app_name": "guardian",
              "namespace_name": "builtin",
              "name": "target_field_equals_actor_field",
              "parameters": [
                {
                  "name": "actor_field",
                  "value": "id"
                },
                {
                  "name": "target_field",
                  "value": "orderer_id"
                }
              ]
            }
          ],
          "relation": "AND",
          "permissions": [
            {
              "app_name": "cake-express",
              "namespace_name": "orders",
              "name": "cancel-order"
            }
          ]
        }' \
    $MANAGEMENT_SERVER/capabilities/cake-express/orders

8.1.6.3. Manage notifications#

Listing 8.15 and Listing 8.16 show how Example Organization creates the capability for managing notifications. The action requires two POST requests to create the capability:

Listing 8.15 Create capability to manage notifications for the user manager role#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "name": "user-manager-can-manage-notifications",
          "display_name": "User Managers can manage cake notifications",
          "role": {
            "app_name": "cake-express",
            "namespace_name": "users",
            "name": "user-manager"
          },
          "conditions": [],
          "relation": "AND",
          "permissions": [
            {
              "app_name": "cake-express",
              "namespace_name": "users",
              "name": "manage-notifications"
            }
           ]
        }' \
    $MANAGEMENT_SERVER/capabilities/cake-express/users
Listing 8.16 Create capability to manage notifications for the user themselves#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "name": "self-can-manage-notifications",
          "display_name": "Users can manage their own notifications, except for birthday cakes",
          "role": {
            "app_name": "cake-express",
            "namespace_name": "cakes",
            "name": "cake-orderer"
          },
          "conditions": [
            {
              "app_name": "guardian",
              "namespace_name": "builtin",
              "name": "target_field_equals_actor_field",
              "parameters": [
                {
                  "name": "actor_field",
                  "value": "id"
                },
                {
                  "name": "target_field",
                  "value": "recipient_id"
                }
              ]
            },
            {
              "app_name": "guardian",
              "namespace_name": "builtin",
              "name": "target_does_not_have_role",
              "parameters": [
                {
                  "name": "role",
                  "value": "cake-express:cakes:birthday-cake"
                }
              ]
            }
          ],
          "relation": "AND",
          "permissions": [
            {
              "app_name": "cake-express",
              "namespace_name": "users",
              "name": "manage-notifications"
            }
          ]
        }' \
    $MANAGEMENT_SERVER/capabilities/cake-express/users

Example Organization is now done with the join script and is ready to start using Guardian with their application.

8.1.7. Registering custom conditions#

The Guardian comes with several built-in conditions, documented in the section Conditions Reference.

However, some apps need to write their own custom conditions, and the Management API provides an endpoint to facilitate this. The endpoint requires knowledge of Rego.

Suppose that Example Organization tracks whether or not a user likes cakes or not, and wants to provide the guardian app administrator with a condition that allows them to opt users out of receiving a cake, without knowing how Cake Express stores their cake preferences.

Listing 8.17 shows the Rego code for the custom condition.

Listing 8.17 Create custom condition with Rego#
package guardian.conditions

import future.keywords.if
import future.keywords.in

condition("cake-express:users:recipient-likes-cakes", _, condition_data) if {
    condition_data.target.old.attributes.recipient["likes_cakes"]
} else = false

You can test this code in the Rego Playground provided by the Open Policy Agent. Listing 8.18 shows the code for the Playground. Paste it into the Editor section. Click the Evaluate button in the Rego Playground. The OUTPUT section shows the result true.

Listing 8.18 Test custom condition with Rego Playground#
package guardian.conditions

import future.keywords.if
import future.keywords.in

condition("cake-express:users:recipient-likes-cakes", _, condition_data) if {
    condition_data.target.old.attributes.recipient["likes_cakes"]
} else = false

result := condition(
            "cake-express:users:recipient-likes-cakes",
            {},
            {"target":
               {"old":
                  {"attributes":
                     {"recipient": {"likes_cakes": true}}
                  }
               }
            }
          )

The code requires base64 encoding before sending it to the API. To create the base64 encoding, apply the following steps:

  1. Save your Rego code in a file.

  2. Encode it with the command base64 $FILENAME.

  3. Copy the base64 encoded Rego code and paste it as value to the attribute code as shown in Listing 8.19.

Listing 8.19 shows how Example Organization creates a custom condition:

Listing 8.19 Create custom condition for the app with base64 encoding#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "name": "recipient-likes-cakes",
          "display_name": "recipient likes cakes",
          "documentation": "True if the user recieving a cake likes cakes",
          "parameters": [],
          "code": "cGFja2FnZSBndWFyZGlhbi5jb25kaXRpb25zCgppbXBvcnQgZnV0dXJlLmtleXdvcmRzLmlmCmltcG9ydCBmdXR1cmUua2V5d29yZHMuaW4KCmNvbmRpdGlvbigiY2FrZS1leHByZXNzOnVzZXJzOnJlY2lwaWVudC1saWtlcy1jYWtlcyIsIF8sIGNvbmRpdGlvbl9kYXRhKSBpZiB7CiAgICBjb25kaXRpb25fZGF0YS50YXJnZXQub2xkLmF0dHJpYnV0ZXMucmVjaXBpZW50WyJsaWtlc19jYWtlcyJdCn0gZWxzZSA9IGZhbHNl"
        }' \
    $MANAGEMENT_SERVER/conditions/cake-express/users

Listing 8.20 shows how Example Organization then updates the existing capability for ordering cakes:

Listing 8.20 Update existing capability with custom condition#
$ curl -X PUT \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "display_name": "Cake Orderers can order cake",
          "role": {
            "app_name": "cake-express",
            "namespace_name": "cakes",
            "name": "cake-orderer"
          },
          "conditions": [
            {
              "app_name": "cake-express",
              "namespace_name": "users",
              "name": "recipient-likes-cakes",
              "parameters": []
            }
          ],
          "relation": "AND",
          "permissions": [
            {
              "app_name": "cake-express",
              "namespace_name": "cakes",
              "name": "order-cake"
            }
           ]
        }' \
    $MANAGEMENT_SERVER/capabilities/cake-express/cakes/cake-orderer-can-order-cake

8.2. Authorization API#

Before going through this section, you must follow the instructions from the previous section Management API.

Note

Code in this section isn’t part of the join script. This means that it doesn’t have access to the guardian-scripts client and the Administrator password. As part of the join script for your app, create your own Keycloak client to use it with your app, that allows service accounts and requires a client secret.

All examples in this section use a hypothetical Keycloak client that Cake Express already has. To create your own Keycloak client, you can use univention-keycloak oidc/rp create.

8.2.1. Listing all general permissions#

Cake Express has three tabs in the web interface:

  • Order a Cake

  • Manage Existing Orders

  • Settings

Cake Express uses its own internal rules:

  • The Settings tab is always available.

  • Order a Cake is only available to users who can order cakes and have the cake-express:cakes:order-cake permission.

  • Manage Existing Orders is only available to users who can manage all orders and have the cake-express:orders:manage-order permission. Users who can’t manage all orders have to use the Order a Cake tab to see their own orders.

Ariel is a user with id ariel. She has the cake-express:cakes:cake-orderer role. Tristan has ordered her an anniversary cake, because she has been with the Happy Employees company for 10 years. It’s also Ariel’s birthday in two weeks, so Carla has also ordered her a birthday cake.

Ariel logs into Cake Express, and Cake Express needs to know which tabs to show Ariel. So Cake Express asks the Authorization API for all capabilities related to the cakes and orders namespaces:

Listing 8.21 Retrieve all capabilities related to cakes and orders namespaces#
AUTHORIZATION_SERVER="$(hostname).$(ucr get domainname)/guardian/authorization"

$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "namespaces": [
            {
              "app_name": "cake-express",
              "name": "cakes"
            },
            {
              "app_name": "cake-express",
              "name": "orders"
            }
          ],
          "actor": {
            "id": "ariel",
            "roles": [
              {
                "app_name": "cake-express",
                "namespace_name": "cakes",
                "name": "cake-orderer"
              }
            ],
            "attributes": {}
          },
          "targets": [],
          "include_general_permissions": true,
          "extra_request_data": {}
        }' \
    $AUTHORIZATION_SERVER/permissions

Note

Usually the Authorization API expects one or more targets to evaluate permissions. However, you can ask for general_permissions, which means the Authorization API also evaluates all capabilities without a target.

In the Cake Express example of the web interface tabs, there aren’t specific objects like cakes to verify. You just want to know general permissions, so you set include_general_permissions to true.

The Authorization API says that Ariel has one general permission: cake-express:cakes:order-cakes. This means that Cake Express shows her the Order a Cake tab, but not the Manage Existing Orders tab. Cake Express always shows the Settings tab.

8.2.2. Listing all target permissions#

Now Ariel wants to manage her cake notifications, so she clicks on the Settings tab and goes to the Cake Notifications section.

From the previous call to the API, Cake Express already knows that Ariel doesn’t have the cake-express:users:manage-notifications general permission for any cake. But Ariel might be able to manage notifications for cakes she is associated with. So Cake Express gathers a list of all cakes where Ariel is the recipient, and asks the Authorization API for target permissions for those cakes:

Listing 8.22 List all cakes associated with Ariel#
$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "namespaces": [
            {
              "app_name": "cake-express",
              "name": "users"
            }
          ],
          "actor": {
            "id": "ariel",
            "roles": [
              {
                "app_name": "cake-express",
                "namespace_name": "cakes",
                "name": "cake-orderer"
              }
            ],
            "attributes": {
              "id": "ariel"
            }
          },
          "targets": [
            {
              "old_target": {
                "id": "anniversary-cake-from-tristan",
                "roles": [],
                "attributes": {
                  "id": "anniversary-cake-from-tristan",
                  "orderer_id": "tristan",
                  "recipient_id": "ariel",
                  "notifications": true
                }
              }
            },
            {
              "old_target": {
                "id": "birthday-cake-from-carla",
                "roles": [
                  {
                    "app_name": "cake-express",
                    "namespace_name": "cakes",
                    "name": "birthday-cake"
                  }
                ],
                "attributes": {
                  "id": "birthday-cake-from-carla",
                  "orderer_id": "carla",
                  "recipient_id": "ariel",
                  "notifications": true
                }
              }
            }
          ],
          "include_general_permissions": false,
          "extra_request_data": {}
        }' \
    $AUTHORIZATION_SERVER/permissions

Note

Targets for the Authorization API can verify the old_target, which is the original state of the target, and the new_target, which is the updated state of the target.

In the case of showing Ariel which cakes she can manage, the cakes haven’t changed, so the request only needs to supply the old_target.

The Authorization API shows that Ariel has cake-express:users:manage-notifications permissions for the anniversary cake from Tristan, but no permissions for the birthday cake from Carla. So Cake Express only shows Ariel the anniversary cake from Tristan.

8.2.3. Checking specific permissions#

When Ariel turns notifications off for the anniversary cake, Cake Express makes a confirmation verification to make sure she can manage notifications on the cake:

$ curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $token" \
    -d '{
          "namespaces": [
            {
              "app_name": "cake-express",
              "name": "users"
            }
          ],
          "actor": {
            "id": "ariel",
            "roles": [
              {
                "app_name": "cake-express",
                "namespace_name": "cakes",
                "name": "cake-orderer"
              }
            ],
            "attributes": {
              "id": "ariel"
            }
          },
          "targets": [
            {
              "old_target": {
                "id": "anniversary-cake-from-tristan",
                "roles": [],
                "attributes": {
                  "id": "anniversary-cake-from-tristan",
                  "orderer_id": "tristan",
                  "recipient_id": "ariel",
                  "notifications": true
                }
              },
              "new_target": {
                "id": "anniversary-cake-from-tristan",
                "roles": [],
                "attributes": {
                  "id": "anniversary-cake-from-tristan",
                  "orderer_id": "tristan",
                  "recipient_id": "ariel",
                  "notifications": false
                }
              }
            }
          ],
          "targeted_permissions_to_check": [
              {
                "app_name": "cake-express",
                "namespace_name": "users",
                "name": "manage-notifications"
              }
            ],
            "general_permissions_to_check": [
              {
                "app_name": "cake-express",
                "namespace_name": "users",
                "name": "manage-notifications"
              }
            ],
          "extra_request_data": {}
        }' \
    $AUTHORIZATION_SERVER/permissions/check

The Authorization API says that Ariel doesn’t have general permissions to manage notifications, but she does have permissions for all targets. So Cake Express saves her updated notification settings, and Ariel doesn’t receive notifications about her anniversary cake.