scimprotocol

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2025 License: MIT Imports: 25 Imported by: 0

README

SingleStore-Lab/scim is an implementations of SCIM (system for cross domain identity management) RFC6742/RFC6743/RFC6744. It designed for the client side of SCIM, to receive provision requests from the server side of SCIM (Identity Providers). It implements the SCIM protocol with router and server interface. Example usage at /scimtest folder.

Usage

  1. Define SCIM Resources

    func init() {
    	scimtag.BuildAllSCIMCharacsCache(SCIMUser{}, SCIMGroup{})
    }
    
    type SCIMUser struct {
    	scimprotocol.SCIMResourceMarker
    	UserID   *uuid.User
    	CoreUser `scim:"urn:ietf:params:scim:schemas:core:2.0:User"` // core schema required to be the first field with a scim tag
    	Meta     SCIMMeta                                            `scim:"meta,ignoreUnmarshal,returned=always"`
    }
    
    var _ scimprotocol.Resource = SCIMUser{}
    
    type CoreUser struct {
    	Active            bool          `scim:"active,returned=keepEmpty"`
    	DisplayName       string        `scim:"displayName,caseExact"`
    	Emails            []Email       `scim:"emails"`
    	Entitlements      []Entitlement `scim:"entitlements"`
    	ExternalID        string        `scim:"externalId"`
    	Groups            []Group       `scim:"groups,mutability=readOnly"`
    	ID                string        `scim:"id,returned=always,ignoreUnmarshal"`
    	Name              Name          `scim:"name"`
    	PreferredLanguage string        `scim:"preferredLanguage"`
    	Roles             []Role        `scim:"roles"`
    	Timezone          string        `scim:"timezone"`
    	Title             string        `scim:"title"`
    	UserName          string        `scim:"userName,required"`
    	UserType          string        `scim:"userType"`
    }
    
    type Email struct {
    	Value   string `scim:"value"`
    	Type    string `scim:"type,canonicalValues=work"` // azure only allow `work`
    	Primary bool   `scim:"primary,returned=keepEmpty"`
    }
    
    type SCIMMeta struct {
    	Created      time.Time `json:"created" scim:"created,returned=always"` // SCIM characteristics need specify at all level
    	LastModified time.Time `json:"lastModified" scim:"lastModified,returned=always"`
    }
    
    • SCIM resources like User and Group must implement the Resource interface (embed SCIMResourceMarker).
    • When defining SCIM resources, the type need to be unique. Here is Why
    • Multi-Value attributes, like email, checks duplicate on whole object by default. You can customize comparation by implementing interface MultiValueElement.
    • Define scim tags for scim characteristics
      • First position of SCIM tag is name of the attribute. Tag attributes separated by ','.
      • bool or !bool in SCIM tag can represent true or false.
      • If you want empty canonical values shows in schemas, then use cannonicalValus= .
      • Please check out characteristics.go for all the supported tags.
  2. Create SCIM endpoints

    scimrouter is available to use, check example in scimtest folder.

    You can also create your own router and server with helper function below.

    • Use SingleStore-Lab/scim to create a http server:
      • Use functions in handlerhelper.go (Or do something similar) with your persistency later to create a http server
    • Use scimmarshal.go to marshal/unmarshal when the data goes through endpoints

Marshal&Unmarshal

SingleStore-Lab/scim got it's own SCIM marshal with scim tag scim.

Attribute Characteristics returned controls the marshal

  • returned=default will omit empty
  • returned=keepEmpty will return empty values
  • returned=request will must marshal when it's been requested
  • returned=never will never marshal the field even selected
  • returned=always will alway marshal the field even not selected

Attribute Characteristics required and ignoreUnmarshal controls the unmarshal

NOTE: Why we need keepEmpty while it's not in the RFC standards? Because we need ability to hide some empty attributes while keeping some necessary attributes to support multiple identity providers.

SingleStore-Lab/scim supports customization marshal, however, if you use SCIMMarshaler/SCIMUnmarshaler, then filter and patch will not works on that resource.

	type SCIMMarshaler interface {
		MarshalSCIM() ([]byte, error)
	}

	type SCIMUnmarshaler interface {
		UnmarshalSCIM([]byte) error
	}

The PrimaryDataType interface helps support customized primary data type not in the rfc, like ID to support UUID type. Example: TestMarshalObject

You could also use string for ID and convert it to your type at outside of this Library.

type PrimaryDataType interface {
	SCIMCompareValue(op string, stringValue string, azureAdd bool) (bool, error)
}

Code

  1. parser - parse input query contains filter or patch with path.
  2. filter - after parse we need evaluate a resource, like a user or a group, to check if it's passes the filter or not.
  3. patch - patch needs to walk down the path along with filter to the target value and modifies it.
  4. marshal - marshal and unmarshal SCIM object with characteristics and support features like checking schema, select attributes.
    • customized SCIM marshal by implementing SCIMMarshaler.
  5. scim tag - include SCIM characteristics and tag cache.
  6. handlerhelper - helper functions helps easily interact with database/storage layer for those handler functions when building SCIM http server.

Azure tweaks

Azure uses filter in a patch to add new element for multi-value attributes. Like if non of the element can pass filter then it will add one with the value in the patch. Use azureFilterAdd to trigger support adding elements to multi-value attributes with filters.

Note

This repo does not contains name and description in schemas because they are optional in RFC.

Improvement:

  • IMP-1. support bulk
  • IMP-2. Improve return 'requested'?
  • IMP-3. add support for number?

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Lex = lexer.MustSimple([]lexer.SimpleRule{

	{Name: "CompValue", Pattern: fmt.Sprintf(`(?:false|null|true|%s|%s)`, numberPattern, stringPattern)},
	{Name: "(", Pattern: `\(`},
	{Name: ")", Pattern: `\)`},
	{Name: "[", Pattern: `\[`},
	{Name: "]", Pattern: `\]`},
	{Name: "Not", Pattern: `not[ \t]+`},
	{Name: "URI", Pattern: uriPattrn},
	{Name: "AttrName", Pattern: `[a-zA-Z][-a-zA-Z0-9_]*`},
	{Name: "Whitespace", Pattern: `[ \t]+`},
	{Name: "Dot", Pattern: `\.`},
	{Name: "Colon", Pattern: `\:`},
})

NOTE:

  • Should avoid conflict tokenizer, and order matters.
View Source
var URIPattenRegexp = regexp.MustCompile(uriPattrn)

Functions

func CompareValueAddIfAzure

func CompareValueAddIfAzure(target reflect.Value, targetCharacs *scimtag.Characteristics, op string, value string, azureAdd bool) (bool, error)

CompareValueAddIfAzure compare the target reflect value with input op and value. if azureAdd is true, it will set the target value when op is 'eq'.

func CreateResourceHelper

func CreateResourceHelper[T Resource](
	r *http.Request,
	scimID string,
	createResourceToDB func(_ context.Context, scimID string, _ T) (T, error),
) (nvelope.Response, error)

func EvalHelper

func EvalHelper(e Expression, p *Node, objV reflect.Value, scimCharacs *scimtag.Characteristics, azureFilterAdd bool) (bool, error)

EvalHelper is a function that evaluates filter expressions against refelect.Value objects scimCharacs should be nil when object is not SCIM attribute

azureFilterAdd is hacky way to be compatible with azure's 'add' patch. Following patch should add new email that fit the filter and value. since it's hacky way so currently only support following two, no more complex op.

{
	"op": "add",
	"path": "emails[type eq \"work\"].value",
	"value": "zoey@mcglynn.name"
},
{
	"op": "add",
	"path": "emails[type eq \"work\"].primary",
	"value": true
},

the azureFilterAdd should only be turned on in patch processing when op is 'add'.

func GetFilteredResources

func GetFilteredResources[T Resource](filter *OrExpression, inputs []T) ([]T, error)

func GetListResourceHelper

func GetListResourceHelper[T Resource](
	r *http.Request,
	trace util.Trace,
	itemsPerPage int,
	scimID string,
	getAllResourceData func(ctx context.Context, scimID string) ([]T, error),
) (nvelope.Response, error)

func GetResourceHelper

func GetResourceHelper[T Resource](
	r *http.Request,
	scimID string,
	resourceID string,
	getResourceData func(ctx context.Context, scimID string, resourceID string) (T, error),
) (nvelope.Response, error)

func GetSCIMDataType

func GetSCIMDataType(t reflect.Type, scimAttrName string) (string, error)

func GetSchemasHelper

func GetSchemasHelper(
	resourceTypes []ResourceType,
	itemsPerPage int,
) (nvelope.Response, error)

func Marshal

func Marshal(obj any) ([]byte, error)

func MarshalWithSelectedAttr

func MarshalWithSelectedAttr(obj any, selectedAttr []string, excludeSelected bool) (_ []byte, err error)

func Patch

func Patch(objV reflect.Value, path *Node, op string, value []byte) error

path = nil: patch on the end of path path = &Node: patch on root

func PatchResourceHelper

func PatchResourceHelper[T Resource](
	r *http.Request,
	scimID string,
	resourceID string,
	getResourceFromDB func(ctx context.Context, scimID string, resourceID string) (T, error),
	updateResourceToDB func(ctx context.Context, scimID string, resourceID string, _ T) (T, error),
) (nvelope.Response, error)

func Unmarshal

func Unmarshal(data []byte, obj any) (err error)

Unmarshal not required to be resource, since we need it in patch to unmarshal attribute

func UpdateResourceHelper

func UpdateResourceHelper[T Resource](
	r *http.Request,
	scimID string,
	resourceID string,
	updateResourceToDB func(_ context.Context, scimID string, resourceID string, _ T) (T, error),
) (nvelope.Response, error)

Types

type AndTerm

type AndTerm struct {
	Left  Expr   `parser:"@@"` // Expr will be recursive parse either Expression or Not(Group)
	Right []Expr `parser:"(Whitespace 'and' Whitespace @@)*"`
}

func (AndTerm) Eval

func (ot AndTerm) Eval(v reflect.Value, azureAdd bool) (bool, error)

func (AndTerm) RedactedString

func (e AndTerm) RedactedString() string

func (AndTerm) String

func (e AndTerm) String() string

func (AndTerm) ToSqlizer

func (e AndTerm) ToSqlizer(sg SQLGenerator, not bool) (sq.Sqlizer, error)

type AttributeSchema

type AttributeSchema struct {
	Name            string             `json:"name"`
	Type            string             `json:"type"`
	MultiValued     bool               `json:"multiValued"`
	Required        bool               `json:"required"`
	CaseExact       *bool              `json:"caseExact,omitempty"`
	Mutability      scimtag.Mutability `json:"mutability"`
	Returned        scimtag.Returned   `json:"returned"`
	Uniqueness      *string            `json:"uniqueness,omitempty"`
	CanonicalValues *[]string          `json:"canonicalValues,omitempty"`
	ReferenceTypes  []string           `json:"referenceTypes,omitempty"`
	SubAttributes   []AttributeSchema  `json:"subAttributes,omitempty"`
}

type AuthType

type AuthType string
const AuthTypeOauthBearerToken AuthType = "oauthbearertoken"

type AuthenticationScheme

type AuthenticationScheme struct {
	Name             string   `json:"name"`
	Description      string   `json:"description"`
	SpecURI          string   `json:"specUri"`
	DocumentationURI string   `json:"documentationUri"`
	Type             AuthType `json:"type"`
	Primary          bool     `json:"primary"`
}

type Common

type Common interface {
	Eval(v reflect.Value, azureAdd bool) (bool, error)
	String() string
	ToSqlizer(sg SQLGenerator, not bool) (sq.Sqlizer, error)
}

type Config

type Config struct {
	ItemsPerPage          int
	PatchSupported        bool
	BulkSupported         bool
	BulkMaxOperations     int
	BulkMaxPayloadSize    int
	FilterSupported       bool
	FilterMaxResult       int
	ChangePassword        bool
	SortSupported         bool
	EtagSupported         bool
	AuthenticationSchemas []AuthenticationScheme
}

func (Config) MarshalSCIM

func (r Config) MarshalSCIM(reqURL string) ([]byte, error)

type EndpointSCIMID

type EndpointSCIMID struct {
	SCIMID string `nvelope:"path,name=scimID"`
}

type EndpointSCIMIDAndResourceID

type EndpointSCIMIDAndResourceID struct {
	SCIMID     string `nvelope:"path,name=scimID"`
	ResourceID string `nvelope:"path,name=resourceID"`
}

type Expr

type Expr interface {
	RedactedString() string
	Common
	// contains filtered or unexported methods
}

Expr should be implement by Expression and NotExpression (which include OrExpression)

type Expression

type Expression struct {
	Path Path `parser:"@@"`
	// check CompareOp in grammar to avoid tokenize conflict
	CompareOp string `parser:"(Whitespace (@'pr' | (@('eq'|'ne'|'co'|'sw'|'ew'|'gt'|'lt'|'ge'|'le')"`
	Value     string `parser:"  Whitespace @CompValue)))?"`
}

func (Expression) Eval

func (e Expression) Eval(v reflect.Value, azureAdd bool) (bool, error)

func (Expression) RedactedString

func (e Expression) RedactedString() string

func (Expression) String

func (e Expression) String() string

func (Expression) ToSqlizer

func (e Expression) ToSqlizer(sg SQLGenerator, not bool) (sq.Sqlizer, error)

type ListResponse

type ListResponse[T any] struct {
	Resources    []T
	StartIndex   int
	ItemsPerPage int
	TotalResults int
}

func (ListResponse[T]) MarshalSCIM

func (l ListResponse[T]) MarshalSCIM() ([]byte, error)

type MultiValueElement

type MultiValueElement interface {
	// contains filtered or unexported methods
}

type Node

type Node struct {
	Value any
	Next  *Node
}

type NotExpression

type NotExpression struct {
	Not   bool         `parser:"(@Not "`
	Group OrExpression `parser:"  '('@@')') | ('('@@')')"`
}

'not' has highest precedence so put it in deepest level

func (NotExpression) Eval

func (sg NotExpression) Eval(v reflect.Value, azureAdd bool) (bool, error)

func (NotExpression) RedactedString

func (ne NotExpression) RedactedString() string

func (NotExpression) String

func (ne NotExpression) String() string

func (NotExpression) ToSqlizer

func (ne NotExpression) ToSqlizer(sg SQLGenerator, not bool) (sq.Sqlizer, error)

type OrExpression

type OrExpression struct {
	Left  *AndTerm   `parser:"@@"`
	Right []*AndTerm `parser:"(Whitespace 'or' Whitespace @@)*"`
}

func ParseFilter

func ParseFilter(filterStr string) (*OrExpression, error)

func (OrExpression) Eval

func (le OrExpression) Eval(v reflect.Value, azureAdd bool) (bool, error)

Eval evaluates the OrExpression against the given reflect.Value v. The azureAdd parameter indicates whether to apply Azure-specific filtering logic. Detail explained in filter.go EvalHelper.

func (OrExpression) RedactedString

func (oe OrExpression) RedactedString() string

func (OrExpression) String

func (oe OrExpression) String() string

func (OrExpression) ToSqlizer

func (oe OrExpression) ToSqlizer(sg SQLGenerator, not bool) (sq.Sqlizer, error)

type PatchOperation

type PatchOperation struct {
	Op    string          `json:"op"`
	Path  string          `json:"path"`
	Value json.RawMessage `json:"value"`
}

func UnmarshalPatchRequest

func UnmarshalPatchRequest(data []byte) ([]PatchOperation, error)

type Path

type Path struct {
	URI         string        `parser:"(@URI"`
	Colon       string        `parser:"  ':')?"`
	AttrName    string        `parser:"@AttrName"`
	Filter      *OrExpression `parser:"('[' Whitespace? @@  Whitespace?']')?"`
	SubAttrName string        `parser:"('.'@AttrName)?"`
}

func ParsePath

func ParsePath(pathStr string) (*Path, error)

func (*Path) GetNodes

func (p *Path) GetNodes(coreSchemaID string) *Node

GetNodes returns a linked list from Path for recursive when filter, patch and marshal. It returns dummyhead node (empty node) followed by path nodes. If path is nil (no path), then return nil. If the node is the last one (end path), then node.Next is nil.

func (Path) String

func (p Path) String() string

type PrimaryDataType

type PrimaryDataType interface {
	SCIMCompareValue(op string, stringValue string, azureAdd bool) (bool, error)
}

type Resource

type Resource interface {
	// contains filtered or unexported methods
}

type ResourceType

type ResourceType struct {
	ID                 string           `json:"id,omitempty"`
	Name               string           `json:"name"`
	Endpoint           string           `json:"endpoint"` // "/Users" or "/Groups"
	Description        string           `json:"description,omitempty"`
	ResourceObjectType reflect.Type     `json:"-"`
	Meta               ResourceTypeMeta `json:"meta"`
}

func (ResourceType) MarshalSCIM

func (r ResourceType) MarshalSCIM() ([]byte, error)

type ResourceTypeMeta

type ResourceTypeMeta struct {
	Location     string `json:"location"`
	ResourceType string `json:"resourceType"`
}

type SCIMConnectionErr

type SCIMConnectionErr error

type SCIMMarshaler

type SCIMMarshaler interface {
	MarshalSCIM() ([]byte, error)
}

SCIMMarshaler and SCIMUnmarshaler allow customized marshal and unmarshal for SCIM Note: if you choose to have SCIMMarshaler or SCIMUnmarshaler, means you take full control of marshal. And the patch and filter may not work as expected.

type SCIMResourceMarker

type SCIMResourceMarker struct{}

type SCIMUnmarshaler

type SCIMUnmarshaler interface {
	UnmarshalSCIM([]byte) error
}

type SQLGenerator

type SQLGenerator interface {
	// sq.And, sq.Or, sq.Eq .... returns sq.Sqlizer
	Generate(Common, bool) (sq.Sqlizer, error)
}

SQLGenerator is a interface used to convert expression to sql with SICM tag

type Schema

type Schema struct {
	ID         string            `json:"id"`
	Attributes []AttributeSchema `json:"attributes"`
}

Schema's name and description is option in rfc and not fit in scim tag, so not include for now.

func GetResourceSchema

func GetResourceSchema(resourceT reflect.Type) (schemas []Schema, err error)

resource is User or Groups or... , that User contains Core User attributes group and Extensions attributes group

func (Schema) MarshalSCIM

func (s Schema) MarshalSCIM() ([]byte, error)

type SchemaExtention

type SchemaExtention struct {
	Schema   string `json:"schema"`
	Required bool   `json:"required"`
}

func GetSchemaURIFromResource

func GetSchemaURIFromResource(resourceT reflect.Type, resourceVforFilterEmpty *reflect.Value) (coreSchemaURI string, extensions []SchemaExtention, err error)

GetSchemaURIFromResource is a helper function to get schema URI and extension URIs for ResourceType from self-defined SCIM resource struct, like User, Group resourceV is only needed when you need filter out empty URI struct, like extension URI should not include in when it's empty during marshal if input type is not Resource, then return ErrNotFound error if input type is Resource but doesn't have URI, return error

type Server

type Server interface {
	// Authorization middleware for SCIM requests
	Authorization(inner func() error, r *http.Request) error

	// GetResourceTypes returns the list of supported resource types
	GetResourceTypes() []ResourceType

	// Resource handlers for individual resource operations
	GetResourceHandler(r *http.Request, resourceType ResourceType, params EndpointSCIMIDAndResourceID) (nvelope.Response, error)
	GetResourceListHandler(r *http.Request, resourceType ResourceType, params EndpointSCIMID) (nvelope.Response, error)
	PostResourceHandler(r *http.Request, resourceType ResourceType, params EndpointSCIMID) (nvelope.Response, error)
	UpdateResourceHandler(r *http.Request, resourceType ResourceType, params EndpointSCIMIDAndResourceID) (nvelope.Response, error)
	PatchResourceHandler(r *http.Request, resourceType ResourceType, params EndpointSCIMIDAndResourceID) (nvelope.Response, error)
	DeleteResourceHandler(r *http.Request, resourceType ResourceType, params EndpointSCIMIDAndResourceID) (nvelope.Response, error)

	// Bulk operations handler
	BulkHandler() (nvelope.Response, error)

	// SCIM metadata handlers
	GetResourceTypesHandler() (nvelope.Response, error)
	SchemasHandler() (nvelope.Response, error)
	GetServiceProviderConfigHandler(r *http.Request, w http.ResponseWriter) (nvelope.Response, error)
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL