Documentation
¶
Overview ¶
Package scim is the service-provider half of SCIM 2.0: an HTTP handler for Users and Groups (RFC 7644) over a Backend the caller writes, with the core schemas (RFC 7643), the filter language, PATCH, paging and the discovery endpoints.
h := &scim.Server{Backend: scim.NewMemory(), BaseURL: "https://app.example.org/scim/v2"}
mux.Handle("/scim/v2/", http.StripPrefix("/scim/v2", scim.BearerAuth(token, h)))
An identity provider (Entra ID, Okta, Keycloak, authn-bridge…) is the client: it creates, updates and deletes people and groups here, and the application reads them from its Backend.
What this package refuses, because each is input from outside:
- a filter longer than MaxFilterBytes, nested deeper than 32, or with an operator the attribute cannot take (400 invalidFilter);
- a body larger than MaxBodyBytes, a PATCH with more than MaxOperations operations (413), a page larger than MaxResults (clamped);
- writes to read-only attributes (id, meta, a user's groups: 400 mutability) and resources without their core schema;
- a PATCH that does not apply in full: PATCH is atomic (RFC 7644 §3.5.2), and a concurrent change is detected through the resource version, not overwritten.
Errors from the Backend that are not this package's sentinels become a 500 whose body says nothing about them.
Index ¶
- Constants
- Variables
- func BearerAuth(token string, next http.Handler) http.Handler
- type Backend
- type Bool
- type Email
- type Error
- type Filter
- type Group
- type InvalidValue
- type Memory
- func (m *Memory) CreateGroup(_ context.Context, g Group) error
- func (m *Memory) CreateUser(_ context.Context, u User) error
- func (m *Memory) DeleteGroup(_ context.Context, id string) error
- func (m *Memory) DeleteUser(_ context.Context, id string) error
- func (m *Memory) GetGroup(_ context.Context, id string) (Group, error)
- func (m *Memory) GetUser(_ context.Context, id string) (User, error)
- func (m *Memory) ListGroups(_ context.Context, _ *Filter) ([]Group, error)
- func (m *Memory) ListUsers(_ context.Context, _ *Filter) ([]User, error)
- func (m *Memory) ReplaceGroup(_ context.Context, g Group, ifVersion string) error
- func (m *Memory) ReplaceUser(_ context.Context, u User, ifVersion string) error
- type Meta
- type Name
- type PatchOp
- type PatchRequest
- type Ref
- type SearchRequest
- type Server
- type User
Constants ¶
const ( UserSchema = "urn:ietf:params:scim:schemas:core:2.0:User" GroupSchema = "urn:ietf:params:scim:schemas:core:2.0:Group" ListResponse = "urn:ietf:params:scim:api:messages:2.0:ListResponse" ErrorSchema = "urn:ietf:params:scim:api:messages:2.0:Error" PatchOpSchema = "urn:ietf:params:scim:api:messages:2.0:PatchOp" SPConfigSchema = "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig" ResourceTypeSc = "urn:ietf:params:scim:schemas:core:2.0:ResourceType" SchemaSchema = "urn:ietf:params:scim:schemas:core:2.0:Schema" // MediaType is the SCIM content type (RFC 7644 §8.1). MediaType = "application/scim+json" )
Schema and message URNs (RFC 7643 §8.7, RFC 7644 §3).
const MaxFilterBytes = 4096
MaxFilterBytes bounds the length of a filter.
Variables ¶
var ( // ErrNotFound: no resource with that id (404). ErrNotFound = errors.New("scim: not found") // ErrConflict: a unique attribute (userName, a group's displayName if // the backend wants it unique) is taken (409 uniqueness). ErrConflict = errors.New("scim: uniqueness conflict") // ErrPrecondition: the resource changed since it was read (412). ErrPrecondition = errors.New("scim: resource changed") )
Sentinel errors a Backend returns.
Functions ¶
func BearerAuth ¶
BearerAuth admits requests carrying "Authorization: Bearer <token>", compared in constant time, and answers others 401 with a SCIM error. An empty token admits nobody. The token should be long and random (RFC 7644 §7.4); TLS is the caller's to provide (§7.1).
Types ¶
type Backend ¶
type Backend interface {
CreateUser(ctx context.Context, u User) error
// GetUser returns a user with Groups filled in, or ErrNotFound.
GetUser(ctx context.Context, id string) (User, error)
// ReplaceUser stores u if the stored user's Meta.Version is ifVersion.
ReplaceUser(ctx context.Context, u User, ifVersion string) error
DeleteUser(ctx context.Context, id string) error
ListUsers(ctx context.Context, f *Filter) ([]User, error)
CreateGroup(ctx context.Context, g Group) error
GetGroup(ctx context.Context, id string) (Group, error)
ReplaceGroup(ctx context.Context, g Group, ifVersion string) error
DeleteGroup(ctx context.Context, id string) error
ListGroups(ctx context.Context, f *Filter) ([]Group, error)
}
Backend stores Users and Groups. The Server assigns ids, timestamps and versions, validates input and applies PATCH; the Backend keeps what it is given and enforces what only it can see: uniqueness (ErrConflict), that a member exists (InvalidValue), and that a replaced resource is still the version that was read (ErrPrecondition).
List methods receive the filter, or nil. A Backend may use it to narrow the result (Filter.Equality names the cheap cases), or ignore it: the Server applies the filter exactly to whatever comes back.
type Bool ¶
type Bool bool
Bool is a boolean that also reads the strings "true" and "false" in any case, as some identity providers send them (Entra ID sends "False").
func (*Bool) UnmarshalJSON ¶
UnmarshalJSON reads true, false, "true" or "false".
type Email ¶
type Email struct {
Value string `json:"value"`
Type string `json:"type,omitempty"`
Display string `json:"display,omitempty"`
Primary bool `json:"primary,omitempty"`
}
Email is one of a user's addresses (RFC 7643 §4.1.2).
type Error ¶
Error is a SCIM error response (RFC 7644 §3.12).
func (*Error) MarshalJSON ¶
MarshalJSON writes the Error message, status as a string.
type Filter ¶
type Filter struct {
// contains filtered or unexported fields
}
Filter is a parsed filter.
func (*Filter) Equality ¶
Equality reports whether the filter is a single `attr eq "value"` on a top-level attribute, so that a Backend can answer it from an index (userName, externalId, displayName). attr is lower-cased.
type Group ¶
type Group struct {
Schemas []string `json:"schemas"`
ID string `json:"id"`
ExternalID string `json:"externalId,omitempty"`
DisplayName string `json:"displayName"`
Members []Ref `json:"members,omitempty"`
Meta Meta `json:"meta"`
}
Group is a core Group (RFC 7643 §4.2).
type InvalidValue ¶
type InvalidValue string
InvalidValue is a Backend's refusal of a value, such as a member that does not exist (400 invalidValue). Its message is shown to the client.
func (InvalidValue) Error ¶
func (e InvalidValue) Error() string
type Memory ¶
type Memory struct {
// contains filtered or unexported fields
}
Memory is a Backend in memory: tests, and the reference for writing another. userName is unique without regard to case (RFC 7643 §4.1.1), group display names are not unique, members must exist, and deleting a user or group removes it from every group.
func (*Memory) ListGroups ¶
func (*Memory) ReplaceGroup ¶
type Meta ¶
type Meta struct {
ResourceType string `json:"resourceType"`
Created time.Time `json:"created"`
LastModified time.Time `json:"lastModified"`
Location string `json:"location,omitempty"`
// Version is a weak ETag, W/"…", computed by the Server.
Version string `json:"version,omitempty"`
}
Meta is the provider-assigned metadata of a resource (RFC 7643 §3.1).
type Name ¶
type Name struct {
Formatted string `json:"formatted,omitempty"`
FamilyName string `json:"familyName,omitempty"`
GivenName string `json:"givenName,omitempty"`
MiddleName string `json:"middleName,omitempty"`
HonorificPrefix string `json:"honorificPrefix,omitempty"`
HonorificSuffix string `json:"honorificSuffix,omitempty"`
}
Name is a user's name (RFC 7643 §4.1.1).
type PatchOp ¶
type PatchOp struct {
Op string `json:"op"`
Path string `json:"path,omitempty"`
Value json.RawMessage `json:"value,omitempty"`
}
PatchOp is one operation.
type PatchRequest ¶
type PatchRequest struct {
Schemas []string `json:"schemas"`
Operations []PatchOp `json:"Operations"`
}
PatchRequest is a PATCH body.
type Ref ¶
type Ref struct {
Value string `json:"value"`
Ref string `json:"$ref,omitempty"`
Display string `json:"display,omitempty"`
// Type is "User" or "Group" for a member, "direct" or "indirect" for a
// user's group.
Type string `json:"type,omitempty"`
}
Ref points at another resource: a group a user belongs to, or a member of a group.
type SearchRequest ¶
type SearchRequest struct {
Schemas []string `json:"schemas"`
Filter string `json:"filter,omitempty"`
StartIndex *int `json:"startIndex,omitempty"`
Count *int `json:"count,omitempty"`
Attributes []string `json:"attributes,omitempty"`
ExcludedAttributes []string `json:"excludedAttributes,omitempty"`
}
SearchRequest is the body of POST /.search (RFC 7644 §3.4.3).
type Server ¶
type Server struct {
Backend Backend
// BaseURL is the absolute URL of the SCIM root as clients see it, used
// for meta.location and Location; empty gives relative locations.
BaseURL string
// MaxResults bounds a page (default 100); a larger count is clamped.
MaxResults int
// MaxBodyBytes bounds a request body (default 1 MiB).
MaxBodyBytes int64
// MaxOperations bounds the operations of one PATCH (default 100).
MaxOperations int
// MaxMembers bounds the members of one group (default 10000).
MaxMembers int
// Now and NewID replace the clock and the id generator (tests).
Now func() time.Time
NewID func() string
// ErrorLog receives the Backend errors that become a 500.
ErrorLog func(err error)
}
Server is the SCIM HTTP handler. Mount it at the SCIM root (strip the prefix): it serves /Users, /Groups, their /.search, /ServiceProviderConfig, /ResourceTypes and /Schemas. It does no authentication: wrap it, for instance with BearerAuth.
type User ¶
type User struct {
Schemas []string `json:"schemas"`
ID string `json:"id"`
ExternalID string `json:"externalId,omitempty"`
UserName string `json:"userName"`
Name *Name `json:"name,omitempty"`
DisplayName string `json:"displayName,omitempty"`
// Active is nil when the client never said; Enabled treats that as true.
Active *Bool `json:"active,omitempty"`
Emails []Email `json:"emails,omitempty"`
// Groups is read-only: the Backend fills it from group memberships.
Groups []Ref `json:"groups,omitempty"`
Meta Meta `json:"meta"`
}
User is a core User (RFC 7643 §4.1). Attributes this package does not model are ignored on input.