Defining Who Can Approve Requests

Overview

Out of the box, the Sym SDK provides a single Reducer which defines the basic permissions for who can do what with a given request:

from sym.sdk.annotations import reducer
from sym.sdk.request_permission import PermissionLevel, RequestPermission

@reducer
def get_permissions(event):
    return RequestPermission(
        webapp_view=PermissionLevel.ADMIN,
        approve_deny=PermissionLevel.ADMIN, 
        # Users can approve their own requests. This is great for testing!
        allow_self_approval=True 
    )

Beyond this, multiple ways to determine approval behavior exist, ranging from customizing this Reducer to full automation based on custom SDK logic.

Concepts

Define permissions declaratively using the get_permissions Reducer

The simplest use case of the get_permissions Reducer is to grant a permission to one of the built-in Sym User Roles using the PermissionLevel enum. A permission can be restricted to admins only using PermissionLevel.ADMIN, restricted to admins and members by using PermissionLevel.MEMBER, or granted to all users in your Sym organization, including guests, by using PermissionLevel.ALL_USERS.

The Reducer can also be extended to handle more complex scenarios by calling upon the Sym SDK's various Integrations to define your permissions.

For example, if you want to allow literally all of your users to use your webapp while only granting the ability to approve or deny requests to a list of trusted users which your org maintains in an Okta group, you can do that:

from sym.sdk.annotations import reducer
from sym.sdk.exceptions import OktaError
from sym.sdk.integrations import okta
from sym.sdk.request_permission import RequestPermission, PermissionLevel
from sym.sdk.user import user_ids


@reducer
def get_permissions(event):
    try:
        approve_deny_permission = user_ids(okta.users_in_group(group_id="00g123456abc"))
    except OktaError:
        approve_deny_permission = PermissionLevel.ADMIN

    return RequestPermission(
        webapp_view=PermissionLevel.ALL_USERS,
        approve_deny=approve_deny_permission,
        allow_self_approval=False,
    )

This example also takes advantage of the Sym SDK's exceptions to gracefully degrade in case the Okta API is unavailable.

The get_permissions Reducer is invoked once for each request, at the time the request is made, and its output is persisted for the lifetime of the request. This makes it well-suited for declarative, static permissions, such as when you want to restrict the ability to approve a request to a known set of users that won't change in the scope of an individual request's lifetime.

For more details on what you can do with the get_permissions Reducer, including some important notes on implicit permission grants that always apply, please see its documentation.

Gate actions dynamically using Hooks

For more complex scenarios that require more flexibility than a declarative approach, you can use Hooks (more specifically, on_ Hooks). Unlike the get_permissions Reducer, which is evaluated once per request when the request is made then persisted for the lifetime of the request, Hooks are evaluated just in time for their respective Event—for example, the on_approve Hook is not called until someone actually attempts to approve a request, and it will be called each time someone attempts to approve a request. By contrast with the get_permissions Reducer, this makes Hooks useful for making dynamic decisions based on the state of the world in the moment. They're likewise useful for more than just gating actions, but also for powerful, smart automations—see Automating and Fast-Tracking Approvals for more on that.

📘

get_permissions is evaluated before Hooks

The get_permissions Reducer takes precedence over Hooks. If the get_permissions Reducer denies someone the ability to take an action, that action's Hook will not fire. For example, if a user is not granted the approve_deny permission and they click the Deny button, the on_deny Hook will not be invoked and the user will be told they don't have permission to do that.

On the other hand, if approve_deny is granted to a user and they click the Deny button, the on_deny Hook will be invoked and it will have a chance to prevent the user from denying the request.

You can block user actions with ApprovalTemplate.ignore()

In addition to the transition methods used to Automate and Fast-Track Approvals, the Sym SDK provides a special method that can be used to simply block user interaction without causing a transition, and send a message back to the acting user.

from sym.sdk.annotations import hook
from sym.sdk.templates import ApprovalTemplate

@hook
def on_approve(event):
    return ApprovalTemplate.ignore(message="No one can ever approve this request!")

The on_approve can be used to check + block approvers

Assuming a request has been made successfully and appears in a Slack channel or message, the next step along the happy path is for someone to "approve" the request.

Approving a request will trigger any on_approve hook to fire, prior to executing the approval itself, and any subsequent escalation.

In the below example, we dynamically change which Okta groups are allowed to approve a request based on whether there is an ongoing PagerDuty incident at the time of the approval attempt.

from sym.sdk.annotations import hook
from sym.sdk.integrations import okta, pagerduty, PagerDutyStatus
from sym.sdk.templates import ApprovalTemplate


@hook
def on_approve(event):
    okta_managers_group_id = "00g123456"
    okta_engineers_group_id = "00g7890ab"

    if pagerduty.has_incident(service_ids=["P9Q1Z1D"], statuses=[PagerDutyStatus.ACKNOWLEDGED]):
        if not okta.is_user_in_group(event.user, group_id=okta_managers_group_id) and not okta.is_user_in_group(
            event.user, group_id=okta_engineers_group_id
        ):
            return ApprovalTemplate.ignore(
                message="Ongoing incident—You must be a manager or engineer to approve this request."
            )
    else:
        if not okta.is_user_in_group(event.user, group_id=okta_managers_group_id):
            return ApprovalTemplate.ignore(
                message=(
                    "You must be a manager to approve this request. If there is an ongoing incident, please acknowledge"
                    " it before attempting to approve."
                )
            )

Note that for this example to work, both Okta groups of users would need to be granted approve_deny permissions by the get_permissions Reducer, either explicitly or by using one of the PermissionLevel enum elements. The allow_self_approval permission can also be used to control if members of these groups are allowed to approve their own requests.

on_deny is a separate event, but works exactly the same way

Similar to on_approve, Sym provides an on_deny hook that you can use as a checkpoint to ensure only approved people or groups are able to reject requests. This is a less common application, as denying a request tends to have no direct consequence other than creating a need to re-request; but it works exactly as you'd expect:

from sym.sdk.annotations import hook
from sym.sdk.templates import ApprovalTemplate
from sym.sdk.integrations import okta

@hook
def on_deny(event):
    if not okta.is_user_in_group(event.user, group_id=event.flow.vars["okta_managers_group"]):
        return ApprovalTemplate.ignore(message="Only managers may deny access requests.")