Skip to main content

Dev Guide

Access Management service

The code for the AM is hosted an Azure DevOps Repository.

Schema

The SpiceDB schema is located at internal/adapters/spicedb/schema.zed.

Code generation

Some of the code for the service is generated with ogen. The underlying API spec is in the openapi directory.

Changing the API spec

When making changes to the API spec, please also update the API Catalogue.

Access Management client

The AM service provides a Go client. Make sure GOPRIVATE=dev.azure.com is set in your environment and run

go get dev.azure.com/schwarzit-wiking/schwarzit.one-digital-journey/_git/access-management.git

to add it to your project.

Pipelines

In order for the build to work with the ODJ pipeline, add the following configuration:

    postSourcecodeCache:
- script: |
go env -w GOPRIVATE=dev.azure.com
git config --global "url.https://$SYSTEM_ACCESSTOKEN@dev.azure.com.insteadof" 'https://dev.azure.com'
displayName: Allow Go to checkout private modules
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Example

package main

import (
"dev.azure.com/schwarzit-wiking/schwarzit.one-digital-journey/_git/access-management.git/pkg/client"
"dev.azure.com/schwarzit-wiking/schwarzit.one-digital-journey/_git/log.git"
)

func main() {
ctx := context.Background()
amClient, err := am.NewClient(conf.APIConfig.BaseURL, am.WithMyAPIBearerAuth(ctx, conf.APIConfig.ClientID, conf.APIConfig.ClientSecret))

if err != nil {
log.Exitf("Error initializing Access Management client: %v", err)
}

// Create a team
err = amClient.CreateTeam(ctx, "some-team")
if err != nil {
if !errors.Is(err, client.ErrConflict) {
log.Exitf("error creating team: %v", err)
}
log.Infof("Team already exists")
}

// Add a member
err = amClient.AddTeamMember(ctx, "some-team", "aad:some-uuid", client.MemberRoleOwner)
if err != nil {
if !errors.Is(err, client.ErrConflict) {
log.Exitf("error adding team member: %v", err)
}
log.Infof("Team member already exists with this role")
}

// Checking permissions
allowed, err := amClient.Check(ctx, "aad:some-uuid", client.TeamTarget("some-team"), "edit")

if err != nil {
log.Exitf("error checking permissions: %v", err)
}

if allowed {
log.Infof("aad:some-uuid has edit permissions on some-team")
} else {
log.Infof("aad:some-uuid has no edit permissions on some-team")
}
}

Setup for local development

A more complete example with support for a simpler local development environment looks like this:

package main

import (
"dev.azure.com/schwarzit-wiking/schwarzit.one-digital-journey/_git/access-management.git/pkg/client"
"dev.azure.com/schwarzit-wiking/schwarzit.one-digital-journey/_git/log.git"
)

func main() {
var amOptions []am.Option
// MyAPI-based OAuth for production
if conf.APIConfig.ClientID != "" && conf.APIConfig.ClientSecret != "" {
amOptions = append(amOptions, am.WithMyAPIBearerAuth(context.Background(), conf.APIConfig.ClientID, conf.APIConfig.ClientSecret))
// Basic auth for local development
} else if conf.APIConfig.Username != "" && conf.APIConfig.Password != "" {
amOptions = append(amOptions, am.WithBasicAuth(conf.APIConfig.Username, conf.APIConfig.Password))
} else {
log.Exitf("Missing authentication configuration for Access Management. Either ODJ_DEP_VARS_AM_CLIENT_ID+ODJ_DEP_NXSECRETS_AM_CLIENT_SECRET or ODJ_DEP_VARS_AM_USERNAME+ODJ_DEP_AM_NXSECRETS_PASSWORD need to be provided.")
}

amClient, err := am.NewClient(conf.APIConfig.BaseURL, amOptions...)

if err != nil {
log.Exitf("Error initializing Access Management client: %v", err)
}

// ...
}

Authorization middleware for Chi

The auth-lib repository provides a Chi middleware for token extraction and helpers for authorization checks.

package main

import (
am "dev.azure.com/schwarzit-wiking/schwarzit.one-digital-journey/_git/access-management.git/pkg/client"
odjauth "dev.azure.com/schwarzit-wiking/schwarzit.one-digital-journey/_git/auth-lib.git"
"github.com/go-chi/chi/v5"
"net/http"
)

func main() {
amClient, _ := am.NewClient("https://api.odj.cloud", am.WithBasicAuth("root", "root"))
wrapper := odjauth.NewAuthWrapper(amClient)

// Checks the "edit" permission on the team with the ID that is provided from the "id" route param
authorizeTeamEdit := wrapper.Authorize("TEAM", "id", "edit")
// Checks the global "list_teams" permission
authorizeTeamList := wrapper.AuthorizeGlobal("list_teams")

r := chi.NewRouter()
// Extracts the user identity and writes it to the request context
r.Use(odjauth.ExtractIdentity)
r.With(authorizeTeamList).Get("/teams", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.With(authorizeTeamEdit).Get("/teams/{id}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
}