Custom Transforms in SGNL Protected Systems

Introduction

Custom Transforms in SGNL allow you to modify the structure and content of responses from the SGNL Access API and Search API, enabling you to tailor the output to meet the specific requirements of your Protected Systems. While SGNL has predefined transforms for platforms like Okta and Entra ID, Custom Transforms provide complete flexibility to format responses for any integration scenario.

With Custom Transforms, you can:

  • Reformat SGNL API responses to match the expected schema of your Protected System
  • Add, remove, or modify fields in the response payload
  • Transform data types and formats
  • Create conditional logic to handle different response scenarios

Prerequisites

  • A SGNL Client
  • A SGNL User Account with Protected System Admin privileges
  • At least one Protected System configured in SGNL

Configuring Custom Transforms

  1. Log in to your SGNL Client with an Admin Account
  2. Navigate to the Protected Systems section and select the Protected System you want to configure
  3. Select the “Transforms” tab within the Protected System configuration
  4. Choose either “Access Transform” or “Search Transform” depending on which API response you want to modify
  5. Select “Custom” from the transform type options
  6. Enter your custom transform template using the Go template syntax
  7. Save your configuration

Using Custom Transforms

You can find information about the SGNL Access API here. One key difference however, when using transforms – you will need to append transform=true as a query parameter to your request.

So if you’re requesting an Access API response without transforms from the access api https://wholesalechips.sgnlapis.cloud/access/v2/evaluations, to see a transformed response, you should send the request to https://wholesalechips.sgnlapis.cloud/access/v2/evaluations?transform=true.

Example Custom Transform

Below is a simple example of a custom transform that modifies an Access API response to return the Principal ID and an array of decisions:

{
  "PrincipalId": "{{.principalId}}",
  "Decisions": [
    {{- range $i, $item := .decisions}}{{if $i}},{{end}}
    {
      "assetId": "{{$item.assetId}}",
      "allowOrDeny": "{{$item.decision}}"
    }
    {{- end}}
  ]
}

This transform converts the standard SGNL Access API response into a simpler format that includes just the Principal ID and a list of decision objects containing the asset ID and the allow/deny decision.

Supported Template Functions

SGNL’s Custom Transforms support a wide range of template functions that you can use to manipulate data. Below are the categories of functions available:

String Functions

FunctionUsage ExampleDescription
uppercase{{uppercase .Name}}Converts string to uppercase
lowercase{{lowercase .Name}}Converts string to lowercase
trim{{trim .Value}}Trims whitespace
substring{{substring .Text 0 5}}Gets substring
replace{{replace .Text “a” “b”}}Replaces substrings
split{{split .Text “,”}}Splits string into slice
join{{join .Slice “,”}}Joins slice into string
contains{{contains .Text “foo”}}Checks substring presence
hasPrefix{{hasPrefix .Text “pre”}}Checks prefix
hasSuffix{{hasSuffix .Text “end”}}Checks suffix
padLeft{{padLeft .Text 10 " “}}Pads string on the left
padRight{{padRight .Text 10 " “}}Pads string on the right

Numeric Functions

FunctionUsage ExampleDescription
abs{{abs .Value}}Absolute value
floor{{floor .Value}}Floor value
ceil{{ceil .Value}}Ceiling value
round{{round .Value}}Rounded value
formatNumber{{formatNumber .Value}}Formats number
add{{add 1 2}}Addition
sub{{sub 5 3}}Subtraction
mul{{mul 2 3}}Multiplication
div{{div 10 2}}Division

Array Functions

FunctionUsage ExampleDescription
sort{{sort .Numbers}}Sorts array
reverse{{reverse .Numbers}}Reverses array
distinct{{distinct .Numbers}}Removes duplicates
count{{count .Numbers}}Gets length
first{{first .Numbers}}First element
last{{last .Numbers}}Last element
filter{{filter .Numbers fn}}Filters array
map{{map .Numbers fn}}Maps function over array
append{{append .Numbers 4}}Appends value
slice{{slice .Numbers 1 3}}Slices array
createSlice{{createSlice 1 2 3}}Creates array
flatten{{flatten .Nested}}Flattens nested arrays
zip{{zip .Arr1 .Arr2}}Zips arrays
groupBy{{groupBy .Arr “Field”}}Groups by field
orderBy{{orderBy .Arr “Field”}}Orders by field

Object Functions

FunctionUsage ExampleDescription
merge{{merge .Obj1 .Obj2}}Merges objects
pick{{pick .Obj “Field”}}Picks fields
omit{{omit .Obj “Field”}}Omits fields

Path Operations

FunctionUsage ExampleDescription
path{{path .Obj “a.b.c”}}Gets nested value
at{{at .Arr 2}}Gets array element
keys{{keys .Obj}}Gets object keys
values{{values .Obj}}Gets object values
entries{{entries .Obj}}Gets object entries
get{{get .Obj “key”}}Gets value by key
set{{set .Obj “key” “val”}}Sets value by key

Conditional Helpers

FunctionUsage ExampleDescription
default{{default .Value “fallback”}}Uses fallback if value is empty

Type Conversion

FunctionUsage ExampleDescription
toJSON{{toJSON .Obj}}Converts to JSON string
fromJSON{{fromJSON .JSONString}}Parses JSON string

Advanced Examples

Complex Transformation with Conditional Logic

{
  "principalIdentifier": "{{.principalId}}",
  "access": {
    {{- if gt (count .decisions) 0}}
    "status": "{{if eq (first .decisions).decision "Allow"}}granted{{else}}denied{{end}}",
    "resources": [
      {{- range $i, $item := .decisions}}{{if $i}},{{end}}
      {
        "resourceId": "{{$item.assetId}}",
        "permitted": {{if eq $item.decision "Allow"}}true{{else}}false{{end}},
        "timestamp": "{{.issuedAt}}"
      }
      {{- end}}
    ]
    {{- else}}
    "status": "no_decisions",
    "resources": []
    {{- end}}
  },
  "metadata": {
    "evaluationTime": "{{.evaluationDuration}}ms",
    "transformedAt": "{{.issuedAt}}"
  }
}

Formatting Search API Results

{
  "query": "{{.query}}",
  "results": [
    {{- range $i, $item := .results}}{{if $i}},{{end}}
    {
      "id": "{{$item.id}}",
      "type": "{{$item.type}}",
      "attributes": {{toJSON (pick $item.attributes "name" "email" "department")}}
    }
    {{- end}}
  ],
  "summary": {
    "totalCount": {{count .results}},
    "filteredCount": {{count (filter .results (lambda "item" "eq item.type \"user\""))}}
  }
}

Best Practices

When creating Custom Transforms, consider the following best practices:

  1. Test thoroughly: Validate your transforms with different API responses to ensure they handle all possible scenarios.

  2. Handle edge cases: Include conditional logic to handle empty arrays, null values, or unexpected data structures.

  3. Keep it simple: Create transforms that are as simple as possible while meeting your requirements. Complex transforms can be difficult to maintain.

  4. Document your transforms: Add comments to your transform templates to explain complex logic or non-obvious transformations.

  5. Use template functions: Leverage the wide variety of template functions to simplify your transforms and avoid reinventing the wheel.

  6. Consider performance: Very complex transforms may impact response times. Optimize your transforms for performance when necessary.

Templates for JSON Generation

SGNL’s Custom Transforms are well-suited for generating structured JSON outputs. Below is a detailed guide on using Go templates effectively in your SGNL transforms.

Basic Template Features

When working with templates for JSON generation in SGNL, you’ll commonly use these key features:

  1. Variable Substitution

    • Basic: {{.principalId}}, {{.evaluationDuration}}
    • Nested objects: {{.decisions[0].assetId}}
  2. Conditional Statements

    • Simple if: {{- if .description}} ... {{- end}}
    • If-else: {{- if eq .decision "Allow"}} ... {{- else}} ... {{- end}}
  3. Loops

    • Range over array: {{- range $index, $decision := .decisions}} ... {{- end}}
    • Using the index: {{- if $index}},{{end}} (for comma management in JSON arrays)
  4. Whitespace Control

    • {{- and -}} control whitespace before and after template actions
    • Important for generating clean, valid JSON
  5. Comments

    • {{/* This is a comment */}} - not rendered in output

Comprehensive Example

Here’s a more comprehensive example that demonstrates the power of SGNL templates for generating complex JSON structures:

{{- /* Example transform template for SGNL Access API */ -}}
{
  "company": "SGNL",
  "principal": {
    "id": "{{.principalId}}",
    "type": "{{default .principalType "user"}}"
  },
  "accessDecisions": {
    "timestamp": "{{{{.issuedAt}}}}",
    "evaluationMs": {{.evaluationDuration}},
    {{- if gt (count .decisions) 0}}
    "status": "{{if eq (first .decisions).decision "Allow"}}granted{{else}}denied{{end}}",
    "assets": [
      {{- range $index, $decision := .decisions}}
      {{- if $index}},{{end}}
      {
        "id": "{{$decision.assetId}}",
        {{- if $decision.action}}
        "action": "{{$decision.action}}",
        {{- end}}
        "permitted": {{if eq $decision.decision "Allow"}}true{{else}}false{{end}},
        "metadata": {
          "policyIds": [
            {{- range $pIndex, $policy := $decision.policies}}
            {{- if $pIndex}},{{end}}
            "{{$policy.policyId}}"
            {{- end}}
          ],
          "timestamp": "{{{{.issuedAt}}}}"
        }
      }
      {{- end}}
    ]
    {{- else}}
    "status": "no_decisions",
    "assets": []
    {{- end}}
  }
}

This example shows a transform that:

  • Formats an SGNL Access API response into a custom structure
  • Uses conditional logic to check if decisions exist and determine access status
  • Loops through decisions and nested policy information
  • Uses the default function for fallback values
  • Formats timestamps and provides metadata

Important Tips for JSON Templates

  1. Handling Commas in Arrays: Use the index in range loops to avoid invalid trailing commas:

    {{- range $index, $item := .items}}
    {{- if $index}},{{end}}
    { "item": "{{$item}}" }
    {{- end}}
    
  2. Escaping Special Characters: Be mindful of escaping quotes and special characters in JSON strings:

    "description": "{{replace .description "\"" "\\\""}}"
    
  3. Null Values: Handle potential null values with conditional statements or the default function:

    "department": "{{default .department "Unassigned"}}"
    
  4. Boolean Values: Ensure proper JSON boolean formatting:

    "active": {{if .active}}true{{else}}false{{end}}
    
  5. Nested Objects: For complex nested structures, consider breaking them into logical sections with whitespace control:

    "user": {
      {{- if .userName}}
      "name": "{{.userName}}",
      {{- end}}
      "roles": [
        {{- range $index, $role := .userRoles}}
        {{- if $index}},{{end}}
        "{{$role}}"
        {{- end}}
      ]
    }
    

Further Resources