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.
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
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.
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.