Service extensions

Service extensions

Explore

Documentation

Integrating identity systems requires extreme configurability. Service Extensions are short Golang programs that hook into extension points within orchestrator to modify or extend features. They give administrators the ability to customize the behavior of the orchestrator to suit the particular needs of their integration.

Service Extension Points

All major components in the Orchestrator expose Service Extension. Please reference the component specific docs for more details on the exact extension points.

Local Development Environment

Service extensions can be written in the Maverics UI or with any text editor. If you prefer to use a local development environment to take advantage of common IDE features such as auto-complete, linting, code compilation, etc., refer to the instructions in the service extension library repository.

Go Library Documentation

The Orchestrator exposes a library to aid in the development of extensions and to hook into underlying functionality in the Orchestrator. For example, you may want to log in to an IDP, query an attribute provider, or pull secrets from a secret store. To understand what functionality is available and how to use it, please see the library documentation.

Configuration options

Service extension code can be defined directly in the configuration file using a code block, or in a separate file using file.

Function name

funcName is a unique identifier for the service extension function name. Function names in Go must not contain spaces, and the first letter should be capitalized so that it can be exported (see idioms).

Code

code is the Golang code of the service extension specified directly in the config file.

File

file is the filesystem path to a Go service extension file.

Metadata

metadata is an arbitrary set of key-value pairs that can be made available to a given extension. The values can be referenced from within the Go code, making service extensions more flexible and the configuration more obvious.

ℹ️
Metadata is not shared between different service extensions.

Allowed Protected Packages

allowedProtectedPackages is a string array value that allows the specification of protected packages in service extensions.

Currently, the following packages are protected and are not enabled by default:

  • os
  • os/exec
⚠️
Allowing protected packages in service extensions can be a security risk because it exposes OS level functionality. Use with caution.

Examples

Defining a Service Extension in Config

To specify a service extension using code, use the |+ block scalar YAML construct.

apps:
  - name: exampleOIDCApp
    type: oidc
    # ...
    authentication:
      isAuthenticatedSE:
        funcName: IsAuthenticated
        code: |+
          package main

          import (
            "net/http"

            "github.com/strata-io/service-extension/orchestrator"
            "github.com/strata-io/service-extension/session"
          )

          func IsAuthenticated(api orchestrator.Orchestrator, _ http.ResponseWriter, req *http.Request) bool {
            return false
          }          

Defining Service Extensions in Separate Files

Managing service extensions is often easier when they are written and saved as separate .go files. Use file to specify the filesystem location.

apps:
  - name: exampleOIDCApp
    type: oidc
    # ...
    authentication:
      authenticateSE:
        funcName: IsAuthenticated
        file: /etc/maverics/extensions/auth.go

The file /etc/maverics/extensions/auth.go contains the service extension code:

package main

import (
    "net/http"

    "github.com/strata-io/service-extension/orchestrator"
)

func IsAuthenticated(orchestrator.Orchestrator, http.ResponseWriter, *http.Request) bool {
    return false
}

Service Extension using Metadata

The following example sets a boolean attribute debug: false in metadata:

authentication:
  isAuthenticatedSE:
    funcName: IsAuthenticated
    file: /etc/maverics/extensions/auth.go
    metadata:
      debug: true
  authenticateSE:
    funcName: Authenticate
    file: /etc/maverics/extensions/auth.go
    metadata:
      debug: false

This can then be referenced in /etc/maverics/extensions/auth.go:

package main

import (
    "net/http"

    "github.com/strata-io/service-extension/orchestrator"
)

func IsAuthenticated(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) bool {
    var (
        metadata = api.Metadata()
        logger = api.Logger()
    )
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return false
	}

    debugEnabled := metadata["debug"] == true // Will be set to 'true'

	isOktaAuth, err := session.GetString("okta.authenticated")
	if err != nil {
		logger.Error("se", "unable to retrieve session value 'okta.authenticated'", "error", err.Error())
		return false
    }
	if isOktaAuth == "true" {
		if debugEnabled {
			logger.Debug("se", "user is not authenticated")
		}
		return true
	}
    if debugEnabled {
        logger.Debug("se", "user is authenticated")
    }
    return false
}

func Authenticate(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) {
    var (
        metadata = api.Metadata()
        logger = api.Logger()
    )

    debugEnabled := metadata["debug"] == true  // Will be set to 'false'

    okta, err := api.IdentityProvider("okta")
    if err != nil {
        http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        logger.Error("se", "missing okta IDP", "error", err.Error())
        return
    }

    if debugEnabled {
        logger.Debug("se", "logging in via okta")
    }

    okta.Login(rw, req)
}

Service Extension using allowedProtectedPackages

The following example enables the use of the os and os/exec packages in a service extension:

apis:
  - name: exampleAPI
    serveSE:
      funcName: Serve
      file: /etc/maverics/extensions/serve.go
      allowedProtectedPackages:
        - "os"
        - "os/exec"

/etc/maverics/extensions/serve.go:

package main

import (
    "os"
    "os/exec"

    "github.com/strata-io/service-extension/orchestrator"
)

func Serve(api orchestrator.Orchestrator) error {
    logger := api.Logger()

    // Use the "os" package
    home := os.Getenv("HOME")
	logger.Debug("se", "home directory", "home", home)

    // Use the "os/exec" package
    cmd := exec.Command("ls", "-l")
    cmd.Stdout = os.Stdout
    cmd.Run()
}

HTTP Client Reuse

The orchestrator provides an interface to create and reuse an HTTP client in service extensions. HTTP client reuse is important as the client will pool the underlying TCP connections to enable connection reuse. Reusing a client also helps ensures the system is not overloaded by the opening of too many connections.

ℹ️
The example below shows how a custom HTTP client can be created and reused. In many cases, using the DefaultClient will provide the same benefits and require less coding.
package main

import(
	"net/http"
    
	"github.com/strata-io/service-extension/orchestrator"
)

func LoadAttrs(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) error {
	apiHttp := api.HTTP()
	
	// Attempt to retrieve an already stored client.
	client, err := apiHttp.GetClient("apiClient")
	if err != nil {
		// Create a new client if one has not already been stored.
		client = &http.Client{Timeout: time.Second * 5}
		err := apiHttp.SetClient("apiClient", client)
		if err != nil {
			return fmt.Errorf("failed to create HTTP client: %w", err)
		}
	}

	// Use the client to make HTTP requests.
	resp, err := client.Get("https://api.example.com/users/1")
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Process response ...
	
	return nil
}

Third party packages support

The orchestrator supports a number of third-party packages that could be imported in service extensions. Please see the third-party packages documentation for a list of libraries that can be used.

The supported packages can be imported in Service Extensions by specifying the import path at the top of the service extension code.

package main

import (
    ldap "github.com/go-ldap/ldap/v3"
)

Idioms

Exported Functions

When defining a Service Extension, the function name should be capitalized in order to indicate that the function is exported and will be the entry point called by the orchestrator runtime. Capitalizing extension names helps to differentiate between the public Service Extension API and internal utilities.

Effective Go

The Go language defines a broad set of idioms for writing Effective Go. Service Extensions should generally follow these idioms in order to align with best practices.