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)
})
}