Reducers: Routing and Identity

Reducers are an SDK feature that "reduce" event input to a single value for use in a Flow.

Overview

The Sym SDK uses special Reducer functions to manage request routing, as well as special cases for user identity matching.

Reducers run at specific points in the Flow defined by the Sym backend, and will be triggered with every run of a given Flow.

ReducerRequiredFunction
get_permissionsyesDefines who is allowed to view and act on requests
get_request_notificationsnoSend notifications about new requests.
get_identity_lookupnoReturn a different email to be used by Sym when discovering a user's identity in a third party system.
get_identitynoMatch and persist user identities in cases where a requester's Slack email address does not match a third party system.

📘

Looking for get_approvers?

While we recommend using get_permissions and get_request_notifications instead, information about the deprecated get_approvers Reducer can be found here.

get_permissions

This Reducer is invoked any time a new request is made, once per request. The get_permissions Reducer accepts an Event representing a Sym Request Event, and returns a RequestPermission, an object representing the different users -- both by role and specific list -- who are allowed to act on that request.

📘

get_permissions is complementary to on_approve and on_deny

The on_approve and on_deny Hooks will fire as users interact with a request, and are great for tailoring Flow behavior and response, including potential side effects. For example, if you want requests to be approved automatically based on certain conditions like a user being on-call, the on_approve Hook is where you want to be.

get_permissions is more specifically geared toward the question of "who can do what?" Similar to the on_ Hooks, you can use this Reducer to define who can interact with Flows (e.g. "only Sym Admins can approve requests for this Flow").

A RequestPermission object covers two specific permissions, and a third that defines whether or not users can approve their own requests. Where the deprecated get_approvers is not in use, get_permissions is required.

For the two specific permissions, webapp_view and approve_deny, you can either specify a Sym User Role, or specific users by Sym user ID. Any function that returns a User object (or a list of User objects) can be used with the user_ids() function to produce such a list of user IDs. Some particularly useful examples include:

If an action in Sym is blocked due to lack of permissions (e.g., an approval is blocked because the user doesn't have the approve_deny permission for that request), the corresponding Hook (e.g., on_approve) is not invoked.

AttributeOptionsRequiredDefault
webapp_viewPermissionLevel.ADMIN
PermissionLevel.MEMBER
PermissionLevel.ALL_USERS
A list of Sym user IDs
YesN/A
approve_denyPermissionLevel.ADMIN
PermissionLevel.MEMBER
PermissionLevel.ALL_USERS
A list of Sym user IDs
YesN/A
allow_self_approvalTrue,FalseNoFalse

📘

Admins and requesters always have some permissions

The webapp_view and approve_deny permissions are always implicitly granted to all users with the ADMIN Sym User Role and the user who initiates a request. This means that admins can always view any request in the webapp, as well as approve or deny any request. Requesters can always view their own requests in the webapp and deny their own requests, but their ability to approve their own requests is gated by the allow_self_approval permission.

Note that allow_self_approval is enforced with admins too, so they cannot approve their own requests unless they use the admin override functionality in the webapp.

🚧

Hooks bypass permissions

The Sym platform assumes that any behavior defined in your Hooks is trusted, since implementations can only be modified by Sym admins. This means that, for example, a user who is not otherwise granted permission to approve their own requests can still be automatically approved if the on_request Hook returns an ApprovalTemplate.approve response.

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

@reducer
def get_permissions(event):
    return RequestPermission(
        # Only admins can view requests made by other users in the Sym web app.
        webapp_view=PermissionLevel.ADMIN, 
        # Members and admins may approve or deny Sym requests.
        approve_deny=PermissionLevel.MEMBER, 
        # Users may not approve their own requests.
        allow_self_approval=False
    )

get_request_notifications

This Reducer is invoked any time a new request is made. The get_request_notifications Reducer accepts an Event representing a Sym Request Event, and returns a list of Notification objects defining where the request should go. For more detail, see Routing Access Requests.

A Notification object contains a list of destinations and a timeout. A destination can be any of the following:

DestinationDescription
User objectSends an email to the user
slack.userSends a Slack DM to the user
slack.channelSends a Slack message to a specified channel
slack.groupSends a Slack group DM to all users listed
from sym.sdk.annotations import reducer
from sym.sdk.notifications import Notification
from sym.sdk.integrations import slack, sym


@reducer
def get_request_notifications(event):
    return [
        Notification(destinations=[slack.channel("#sym-requests")], timeout=15),
        Notification(destinations=[slack.channel("#try-again-sym-requests")], timeout=15),
        Notification(
            destinations=sym.get_or_create_users_by_emails(["[email protected]", "[email protected]"]), timeout=15
        ),
    ]

User objects can be retrieved using a variety of functions provided in the Sym SDK:

get_identity_lookup

This Reducer is invoked before get_identity and before standard identity auto-discovery performed by Sym. If your Sym user emails (i.e. Slack emails) do not match the third party service's emails, you can utilize this reducer to optionally transform your Sym user emails before performing identity discovery. If this reducer returns None, then the Sym user's original email will be used for auto-discovery.

For example, if your Sym user emails are in the format [email protected], but your user emails in the third party service are [email protected], you can implement a get_identity_lookup reducer as follows:

from sym.sdk.annotations import reducer


@reducer
def get_identity_lookup(event, service_type, external_id, user):
    """
    The get_identity_lookup reducer runs before identity discovery, and can optionally return a different
    email to use when looking up identities in external services.

    Args:
      event: A sym.sdk.event.Event object containing information about the Event
      	  that is currently being handled.
      service_type: A string indicating the service type that this Identity is for. (e.g. 'okta', 'aptible', 'aws_iam', etc.)
      external_id: A unique string identifier for this service. This values matches the
      	external_id value set in the corresponding sym_integration in your Terraform
        configuration.
      user: The sym.sdk.user.User for whom Identity shall be Discovered

    Returns:
      An email to be used for Identity Discovery.
      If None is returned, then the original Sym email will be used.
    """

    # In this example, our Aptible user emails follow a different convention than our
    # standard Sym user emails 
    if service_type == "aptible":
        # The original user.email is `[email protected]`. 
        # Transform this email into `[email protected]`.
        username_parts = user.email.split("@")[0].split(".")
        first_initial = username_parts[0][0]
        last_name = username_parts[1]

        # Aptible users have emails with the format `[email protected]`.
        # This email will be used by Sym to auto-discover the user's Aptible Identity.
        return f"{first_initial}.{last_name}@test.symops.io"
    
    # For all other services, perform normal auto-discovery with the Sym user's original email.
    return None

get_identity

Accepts a sym.sdk.event.Event, a service type, the service's external ID, and a sym.sdk.user.User object, and returns either a string or None. If it returns a value, that value will be used as the user's identity in the third party service. If it returns None, the normal identity auto-discovery is still performed.

Normally, Sym attempts to auto-discover users' identities in third-party services you integrate with based on the user's email address. If this isn't possible for whatever reason, this reducer can be used to construct the user's identity in that service instead.

from sym.sdk.annotations import reducer
from sym.sdk.integrations import okta


@reducer
def get_identity(event, service_type, external_id, user):
    """
    For a given combination of service and user, returns an identifier for
    that user in the service; or, return None to perform automated identity
    discovery in the service.
    
    Args:
      event: A sym.sdk.event.Event object containing information about Event 
      	that is currently being handled.
      service_type: A string indicating the service type that this Identity is for.
      external_id: A unique string identifier for this service. This values matches the
      	external_id value set in the corresponding sym_integration in your Terraform
        configuration.
      user: The sym.sdk.user.User object to whom the Identity belongs.
    
    Returns:
      The string value of the User's identity for the Service identified by the given
      	service_type and external_id. This value will vary based on the Service; for example,
      	AWS IAM Identities are the AWS IAM User's ARN, while Okta Identities are the unique
      	Okta User ID.
      
      If None is returned, then standard identity discovery will be performed.
    """
  
    if service_type == "aws_iam":
        # For AWS IAM, construct the user's ARN based on the external ID (for
        # AWS IAM, this is the AWS account ID) and the username portion of the
        # user's email
        return f"arn:aws:iam::{external_id}:user/{user.email.split('@')[0]}"
    elif service_type == "okta":
        # For Okta, we need to hard-code the IDs for some users because their
        # emails in Slack don't match their Okta emails.
        email_to_okta_uid = {
            "[email protected]": "00u12345678",
            "[email protected]": "00u9abcdefg",
            "[email protected]": "00uhijklmno"
        }
        
        # For all other users, return None to indicate that we want to perform
        # auto-discovery of the user's identity as normal
        return email_to_okta_uid.get(user.email, None)
      
    return None # For all other services, perform normal auto-discovery