Custom Access Strategies

Define Access Strategies for services that Sym does not support out of the box.

Overview

In addition to native Access Strategies and AWS Lambda, Sym provides an interface that enables implementers to define fully custom Access Strategies.

Custom Strategy Framework

The Sym Strategy Framework provides the tools to:

  • Define custom Strategy logic for escalate, deescalate, and identity fetching/matching
  • Define user-facing targets to which the Strategy can be applied
  • Deploy the Strategy as a custom Flow.

Of course, as with any Sym Strategy, implementers can also define Workflow Handlers to customize, automate, and route their Flows.

The Custom logic

The bulk of the Custom Strategy is defined in the custom_strategy.py file that lives alongside your Flow definition and implementation. The full SDK reference documentation for AccessStrategy classes can be found in our SDK docs here.

🚧

Restrictions Apply

Your Custom Strategy will be executed inside of a RestrictedPython environment, and as a result certain methods might not be available (e.g. all(), next()).

In addition, only imports from the following modules are allowed: sym.sdk, requests, json, datetime.

from sym.sdk.strategies import AccessStrategy
import requests


class CustomStrategy(AccessStrategy):
    def fetch_remote_identity(self, user):
        """This method determines how to identify this Sym user in the third party system.

        For this example, we'll use the user's email address.
        """
        return user.email

    def headers(self):
        """This is a helper method to generate the headers needed to authenticate with the
        third party system. It is not required in all Custom Strategy implementations.
        """
        secrets = self.integration.settings["secrets"]

        if not secrets:
            raise RuntimeError("API key must be set as secret 0 in Terraform")

        api_key = secrets[0].retrieve_value()
        return {"Authorization": f"Bearer {api_key}"}

    def escalate(self, target_id, event):
        """This method executes after a user's request has been approved. It should elevate
        their permissions in the third party system.
        """
        # Get the user's identity in the third party system (in this case, their email).
        requester = self.get_requester_identity(event)

        # To use the identifier from `sym_target.settings.identifier`, we need to fetch the target object.
        target_identifier = event.payload.fields["target"].settings["identifier"]

        # Using the user's ID and requested target, make an API call to a third party system to escalate their permissions.
        payload = {
            "user_id": requester,
            "resource_id": target_identifier,
        }

        requests.post("https://some-api.invalid/escalate", headers=self.headers(), json=payload)

    def deescalate(self, target_id, event):
        """This method executes after a user's request has expired, or during a 'Revoke' event.
        It should deescalate their permissions in the third party system.
        """
        # Get the user's identity in the third party system (in this case, their email).
        requester = self.get_requester_identity(event)

        # To use the identifier from `sym_target.settings.identifier`, we need to fetch the target object.
        target_identifier = event.payload.fields["target"].settings["identifier"]

        # Using the user's ID and requested target, make an API call to a third party system to deescalate their permissions.
        payload = {
            "user_id": requester,
            "resource_id": target_identifier,
        }

        requests.post("https://some-api.invalid/deescalate", headers=self.headers(), json=payload)

Declaring the Flow, Strategy, and Target definitions

Like any Sym Strategy, a Custom Strategy needs to be declared in Terraform.

📘

The get_identity reducer is not used by Custom Strategies

While you can use the same impl.py file for all your flows, including flows that use Custom Strategies, Custom Strategies never call the get_identity reducer. Instead, implement your custom identity discovery logic in the fetch_remote_identity() method of your Custom Strategy.

# An AWS Secrets Manager Secret to hold an API key necessary for making API calls to the third party system.
# Set the value with:
# aws secretsmanager put-secret-value --secret-id "sym/${local.environment_name}/api-key" --secret-string "YOUR-API-KEY"
resource "aws_secretsmanager_secret" "api_key" {
  name        = "sym/custom-strategy/api-key"
  description = "Key for accessing the third party system's API"

  tags = {
    # This SymEnv tag is required and MUST match the `environment` variable
    # passed into the `secrets_manager_access` module in your `secrets.tf` file.
    SymEnv = local.environment_name
  }
}

# This resource tells Sym how to access your API key secret in AWS Secrets Manager.
resource "sym_secret" "api_key" {
  path      = aws_secretsmanager_secret.api_key.name
  source_id = sym_secrets.this.id
}

# The custom Integration that your Strategy uses to grant and revoke access to the
# third party system as well as manage identities for that system in Sym.
resource "sym_integration" "custom" {
  type = "custom"
  name = "custom-integration"

  # The external ID is a unique identifier for your account in the third party system.
  # For example, your account ID.
  external_id = "my-account"

  settings = {
    secret_ids_json = jsonencode([sym_secret.api_key.id])
  }
}

# A custom Target represents a resource a user is requesting access to.
resource "sym_target" "custom" {
  type  = "custom"
  name  = "custom-target"
  label = "Custom Target"

  settings = {
    identifier = "some-resource"
  }
}

# The Strategy that your Flow uses to grant and revoke access to the third party system.
resource "sym_strategy" "custom" {
  type = "custom"

  name           = "custom-access"
  integration_id = sym_integration.custom.id
  targets        = [sym_target.custom.id]

  # This implementation file contains Python code that defines escalation and deescalation behavior.
  implementation = "${path.module}/custom_strategy.py"
}

resource "sym_flow" "custom" {
  name  = "custom-access"
  label = "Custom Access"

  implementation = file("${path.module}/impl.py")
  environment_id = sym_environment.this.id

  params {
    strategy_id = sym_strategy.custom.id

    prompt_field {
      name     = "reason"
      label    = "Why do you need access?"
      type     = "string"
      required = true
    }
  }
}

Full Example

You can find the complete code for this example in our Custom Access Strategy Example.