7. Developer quick start#
Note
This is a highly technical topic, and is primarily geared towards app developers who want to integrate an app with the Guardian. Familiarity with using the command line, working with an API, and writing code is necessary to understand this chapter.
You should also be familiar with:
This section provides a walk-through of the steps necessary to integrate an app with the Guardian.
ACME Corporation is an app developer who creates Cake Express, which allows people to order cakes for company events, and which can be installed from the Univention App Center. They want to integrate Cake Express with the Guardian.
Note
The example scripts assume that:
The app is installed on the same server as the Management API, and
The app is installed on the same server as the Keycloak server.
If either of these two things is not true, you will need to find a way for the UCS app infrastructure maintainer to communicate their locations to the script at run time.
7.1. Management API#
7.1.1. Getting a Keycloak token#
The first thing that ACME Corporation needs to do is to write a join script for their app. This app will need to interact with the Management API, and to do this the join script must get a token from Keycloak to authenticate all calls to the API.
Here are the variables you need to get a token:
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 --binduser
, --bindpwd
, and --bindpwdfile
are available,
which specify an administrator user,
and either a password or a file for parsing the password.
The example above assumes that the join script has already parsed these
parameters into binduser
and bindpwd
variables.
You can retrieve the token with:
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/')
The token
is referenced in all commands for subsequent sections.
You may need to refresh the token several times,
if you are entering commands manually.
7.1.2. Registering an app#
ACME Corporation now needs to let the Guardian know 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
Note
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.
ACME Corporation is now ready to start setting up the Guardian to work with Cake Express.
7.1.3. Registering namespaces#
A namespace is just a handy categorization to store everything that an app wants to use in Guardian, like roles and permissions.
Every app gets a default
namespace to use.
But ACME Corporation wants to manage three different facets of Cake Express:
cakes
: Category for everything related to what is actually being sold.orders
: Category for administration of orders.users
: Category for managing other users of Cake Express.
Later, ACME Corporation will create some roles in each of these namespaces for doing tasks in Cake Express.
Here is how ACME Corporation creates these namespaces:
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-d '{"name":"cakes", "display_name":"Cakes"}' \
$MANAGEMENT_SERVER/namespaces/cake-express
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-d '{"name":"orders", "display_name":"Orders"}' \
$MANAGEMENT_SERVER/namespaces/cake-express
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-d '{"name":"users", "display_name":"Users"}' \
$MANAGEMENT_SERVER/namespaces/cake-express
7.1.4. Registering roles#
ACME Corporation 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.
ACME Corporation also wants to create a role for some of their cakes:
cake-express:cakes:birthday-cake
: A cake just for employee birthdays.
Each role above consists of the following parts, separated by a :
:
Here is how ACME Corporation creates these roles:
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
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
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
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
7.1.5. Registering permissions#
ACME Corporation wants to provide some permissions that define what users of Cake Express want to do:
cake-express:cakes:order-cake
: Users with this permission are allowed to order cakes.cake-express:orders:cancel-order
: Users can cancel a cake order.cake-express:users:manage-notifications
: Users can manage cake notifications.
Here is how ACME Corporation creates these permissions:
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
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
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
7.1.6. Registering capabilities#
Finally, ACME Corporation wants to define some default capabilities for their applications. The guardian app admin that installs Cake Express can change these later, but these default capabilities make it easier for Cake Express to work out of the box.
They want to create:
Users with the
cake-orderer
role are allowed to order cakes.Users with the
finance-manager
role, or the person who ordered the cake, have the permission to cancel the cake order.Users with the
user-manager
role have the permission to manage cake notifications. Users can also manage their own notifications for cakes that are sent to them, except for notifications related to birthday cakes.
Here is how ACME Corporation creates the 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
Here is how ACME Corporation creates the capability for canceling an order.
This requires two POST
requests in order to create it:
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
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
Here is how ACME Corporation creates the capability for managing notifications.
This also requires two POST
requests in order to create it:
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
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
ACME Corporation is now done with the join script and is ready to start using Guardian with their application.
7.1.7. Registering custom conditions#
The Guardian comes with several built-in conditions, which are documented in the chapter on 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 ACME Corporation tracks whether or not a user likes cake, and wants to provide a simple condition to guardian app admins that allows them to opt users out of receiving a cake, without having to know how Cake Express stores its cake preferences.
The Rego code for this condition is as follows:
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:
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}}}}})
Click the Evaluate button on the Rego Playground to receive a
true
result.
The code must be base64
encoded before sending to the API.
Here is how ACME Corporation creates a custom condition:
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
ACME Corporation then updates the existing capability for ordering cakes:
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