scim

package module
v0.0.0-...-f923362 Latest Latest
Warning

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

Go to latest
Published: Jun 10, 2022 License: MIT Imports: 17 Imported by: 0

README

scim-logo

GoVersion GoDoc

Tag

This is an open source implementation of the SCIM v2.0 specification for use in Golang. SCIM defines a flexible schema mechanism and REST API for managing identity data. The goal is to reduce the complexity of user management operations by providing patterns for exchanging schemas using HTTP.

In this implementation it is easy to add custom schemas and extensions with the provided structures. Incoming resources will be validated by their corresponding schemas before being passed on to their callbacks.

The following features are supported:

  • GET for /Schemas, /ServiceProviderConfig and /ResourceTypes
  • CRUD (POST/GET/PUT/DELETE and PATCH) for your own resource types (i.e. /Users, /Groups, /Employees, ...)

Other optional features such as sorting, bulk, etc. are not supported in this version.

Installation

Assuming you already have a (recent) version of Go installed, you can get the code with go get:

$ go get github.com/abhishek262/scim

Usage

! errors are ignored for simplicity.

1. Create a service provider configuration.

RFC Config | Example Config

config := scim.ServiceProviderConfig{
    DocumentationURI: optional.NewString("www.example.com/scim"),
}

! no additional features/operations are supported in this version.

2. Create all supported schemas and extensions.

RFC Schema | User Schema | Group Schema | Extension Schema

schema := schema.Schema{
    ID:          "urn:ietf:params:scim:schemas:core:2.0:User",
    Name:        optional.NewString("User"),
    Description: optional.NewString("User Account"),
    Attributes:  []schema.CoreAttribute{
        schema.SimpleCoreAttribute(schema.SimpleStringParams(schema.StringParams{
            Name:       "userName",
            Required:   true,
            Uniqueness: schema.AttributeUniquenessServer(),
        })),
    },
}

extension := schema.Schema{
    ID:          "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
    Name:        optional.NewString("EnterpriseUser"),
    Description: optional.NewString("Enterprise User"),
    Attributes: []schema.CoreAttribute{
        schema.SimpleCoreAttribute(schema.SimpleStringParams(schema.StringParams{
            Name: "employeeNumber",
        })),
        schema.SimpleCoreAttribute(schema.SimpleStringParams(schema.StringParams{
            Name: "organization",
        })),
    },
}

3. Create all resource types and their callbacks.

RFC Resource Type | Example Resource Type

3.1 Callback (implementation of ResourceHandler)

Simple In Memory Example

var userResourceHandler scim.ResourceHandler
// initialize w/ own implementation

! each resource type should have its own resource handler.

3.2 Resource Type
resourceTypes := []ResourceType{
    {
        ID:          optional.NewString("User"),
        Name:        "User",
        Endpoint:    "/Users",
        Description: optional.NewString("User Account"),
        Schema:      schema,
        SchemaExtensions: []SchemaExtension{
            {Schema: extension},
        },
        Handler:     userResourceHandler,
    },
},

4. Create Server

server := Server{
    Config:        config,
    ResourceTypes: resourceTypes,
}

Addition Checks/Tests

Not everything can be checked by the SCIM server itself. Below are some things listed that we expect that the implementation covers.

! this list is currently incomplete!

We want to keep this list as short as possible. If you have ideas how we could enforce these rules in the server itself do not hesitate to open an issue or a PR.

Mutability

Immutable Attributes

PUT Handler: If one or more values are already set for the attribute, the input value(s) MUST match.

WriteOnly Attributes

ALL Handlers: Attribute values SHALL NOT be returned.
Note: These attributes usually also has a returned setting of "never".

Contributing

Contributors

We are happy to review pull requests, but please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change.

If you would like to propose a change please ensure the following:

  • All checks of GitHub Actions are passing (GolangCI-Lint: misspell, godot and whitespace)
  • All already existing tests are passing.
  • You have written tests that cover the code you are making, make sure to include edge cases.
  • There is documentation for at least all public functions you have added.
  • New public functions and structures are kept to a minimum.
  • The same practices are applied (such as the anatomy of methods, names, etc.)
  • Your changes are compliant with SCIM v2.0 (released as RFC7642, RFC7643 and RFC7644 under IETF).

Documentation

Index

Examples

Constants

View Source
const (
	// PatchOperationAdd is used to add a new attribute value to an existing resource.
	PatchOperationAdd = "add"
	// PatchOperationRemove removes the value at the target location specified by the required attribute "path".
	PatchOperationRemove = "remove"
	// PatchOperationReplace replaces the value at the target location specified by the "path".
	PatchOperationReplace = "replace"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthenticationScheme

type AuthenticationScheme struct {
	// Type is the authentication scheme. This specification defines the values "oauth", "oauth2", "oauthbearertoken",
	// "httpbasic", and "httpdigest".
	Type AuthenticationType
	// Name is the common authentication scheme name, e.g., HTTP Basic.
	Name string
	// Description of the authentication scheme.
	Description string
	// SpecURI is an HTTP-addressable URL pointing to the authentication scheme's specification.
	SpecURI optional.String
	// DocumentationURI is an HTTP-addressable URL pointing to the authentication scheme's usage documentation.
	DocumentationURI optional.String
	// Primary is a boolean value indicating the 'primary' or preferred authentication scheme.
	Primary bool
}

AuthenticationScheme specifies a supported authentication scheme property.

type AuthenticationType

type AuthenticationType string

AuthenticationType is a single keyword indicating the authentication type of the authentication scheme.

const (
	// AuthenticationTypeOauth indicates that the authentication type is OAuth.
	AuthenticationTypeOauth AuthenticationType = "oauth"
	// AuthenticationTypeOauth2 indicates that the authentication type is OAuth2.
	AuthenticationTypeOauth2 AuthenticationType = "oauth2"
	// AuthenticationTypeOauthBearerToken indicates that the authentication type is OAuth2 Bearer Token.
	AuthenticationTypeOauthBearerToken AuthenticationType = "oauthbearertoken"
	// AuthenticationTypeHTTPBasic indicated that the authentication type is Basic Access Authentication.
	AuthenticationTypeHTTPBasic AuthenticationType = "httpbasic"
	// AuthenticationTypeHTTPDigest indicated that the authentication type is Digest Access Authentication.
	AuthenticationTypeHTTPDigest AuthenticationType = "httpdigest"
)

type ListRequestParams

type ListRequestParams struct {
	// Count specifies the desired maximum number of query results per page. A negative value SHALL be interpreted as "0".
	// A value of "0" indicates that no resource results are to be returned except for "totalResults".
	Count int

	// Filter represents the parsed and tokenized filter query parameter.
	// It is an optional parameter and thus will be nil when the parameter is not present.
	Filter filter.Expression

	// StartIndex The 1-based index of the first query result. A value less than 1 SHALL be interpreted as 1.
	StartIndex int
}

ListRequestParams request parameters sent to the API via a "GetAll" route.

type Meta

type Meta struct {
	// Created is the time that the resource was added to the service provider.
	Created *time.Time
	// LastModified is the most recent time that the details of this resource were updated at the service provider.
	LastModified *time.Time
	// Version is the version / entity-tag of the resource
	Version string
}

Meta represents the metadata of a resource.

type Page

type Page struct {
	// TotalResults is the total number of results returned by the list or query operation.
	TotalResults int
	// Resources is a multi-valued list of complex objects containing the requested resources.
	Resources []Resource
}

Page represents a paginated resource query response.

type PatchOperation

type PatchOperation struct {
	// Op indicates the operation to perform and MAY be one of "add", "remove", or "replace".
	Op string
	// Path contains an attribute path describing the target of the operation. The "path" attribute is OPTIONAL for
	// "add" and "replace" and is REQUIRED for "remove" operations.
	Path *filter.Path
	// Value specifies the value to be added or replaced.
	Value interface{}
}

PatchOperation represents a single PATCH operation.

type Resource

type Resource struct {
	// ID is the unique identifier created by the callback method "Create".
	ID string
	// ExternalID is an identifier for the resource as defined by the provisioning client.
	ExternalID optional.String
	// Attributes is a list of attributes defining the resource.
	Attributes ResourceAttributes
	// Meta contains dates and the version of the resource.
	Meta Meta
}

Resource represents an entity returned by a callback method.

type ResourceAttributes

type ResourceAttributes map[string]interface{}

ResourceAttributes represents a list of attributes given to the callback method to create or replace a resource based on the given attributes.

type ResourceHandler

type ResourceHandler interface {
	// Create stores given attributes. Returns a resource with the attributes that are stored and a (new) unique identifier.
	Create(r *http.Request, attributes ResourceAttributes) (Resource, error)
	// Get returns the resource corresponding with the given identifier.
	Get(r *http.Request, id string) (Resource, error)
	// GetAll returns a paginated list of resources.
	// An empty list of resources will be represented as `null` in the JSON response if `nil` is assigned to the
	// Page.Resources. Otherwise, is an empty slice is assigned, an empty list will be represented as `[]`.
	GetAll(r *http.Request, params ListRequestParams) (Page, error)
	// Replace replaces ALL existing attributes of the resource with given identifier. Given attributes that are empty
	// are to be deleted. Returns a resource with the attributes that are stored.
	Replace(r *http.Request, id string, attributes ResourceAttributes) (Resource, error)
	// Delete removes the resource with corresponding ID.
	Delete(r *http.Request, id string) error
	// Patch update one or more attributes of a SCIM resource using a sequence of
	// operations to "add", "remove", or "replace" values.
	// If you return no Resource.Attributes, a 204 No Content status code will be returned.
	// This case is only valid in the following scenarios:
	// 1. the Add/Replace operation should return No Content only when the value already exists AND is the same.
	// 2. the Remove operation should return No Content when the value to be remove is already absent.
	// More information in Section 3.5.2 of RFC 7644: https://tools.ietf.org/html/rfc7644#section-3.5.2
	Patch(r *http.Request, id string, operations []PatchOperation) (Resource, error)
}

ResourceHandler represents a set of callback method that connect the SCIM server with a provider of a certain resource.

Example
package main

import (
	"fmt"
	"math/rand"
	"net/http"
	"strings"
	"time"

	"github.com/abhishek262/scim/errors"
	"github.com/abhishek262/scim/optional"
)

func main() {
	var r interface{} = testResourceHandler{}
	_, ok := r.(ResourceHandler)
	fmt.Println(ok)
}

type testData struct {
	resourceAttributes ResourceAttributes
	meta               map[string]string
}

// simple in-memory resource database.
type testResourceHandler struct {
	data map[string]testData
}

func (h testResourceHandler) Create(r *http.Request, attributes ResourceAttributes) (Resource, error) {
	// create unique identifier
	rand.Seed(time.Now().UnixNano())
	id := fmt.Sprintf("%04d", rand.Intn(9999))

	// store resource
	h.data[id] = testData{
		resourceAttributes: attributes,
	}

	now := time.Now()

	// return stored resource
	return Resource{
		ID:         id,
		ExternalID: h.externalID(attributes),
		Attributes: attributes,
		Meta: Meta{
			Created:      &now,
			LastModified: &now,
			Version:      fmt.Sprintf("v%s", id),
		},
	}, nil
}

func (h testResourceHandler) Delete(r *http.Request, id string) error {
	// check if resource exists
	_, ok := h.data[id]
	if !ok {
		return errors.ScimErrorResourceNotFound(id)
	}

	// delete resource
	delete(h.data, id)

	return nil
}

func (h testResourceHandler) Get(r *http.Request, id string) (Resource, error) {
	// check if resource exists
	data, ok := h.data[id]
	if !ok {
		return Resource{}, errors.ScimErrorResourceNotFound(id)
	}

	created, _ := time.ParseInLocation(time.RFC3339, fmt.Sprintf("%v", data.meta["created"]), time.UTC)
	lastModified, _ := time.Parse(time.RFC3339, fmt.Sprintf("%v", data.meta["lastModified"]))

	// return resource with given identifier
	return Resource{
		ID:         id,
		ExternalID: h.externalID(data.resourceAttributes),
		Attributes: data.resourceAttributes,
		Meta: Meta{
			Created:      &created,
			LastModified: &lastModified,
			Version:      fmt.Sprintf("%v", data.meta["version"]),
		},
	}, nil
}

func (h testResourceHandler) GetAll(r *http.Request, params ListRequestParams) (Page, error) {
	if params.Count == 0 {
		return Page{
			TotalResults: len(h.data),
		}, nil
	}

	resources := make([]Resource, 0)
	i := 1

	for k, v := range h.data {
		if i > (params.StartIndex + params.Count - 1) {
			break
		}

		if i >= params.StartIndex {
			resources = append(resources, Resource{
				ID:         k,
				ExternalID: h.externalID(v.resourceAttributes),
				Attributes: v.resourceAttributes,
			})
		}
		i++
	}

	return Page{
		TotalResults: len(h.data),
		Resources:    resources,
	}, nil
}

func (h testResourceHandler) Patch(r *http.Request, id string, operations []PatchOperation) (Resource, error) {
	if h.shouldReturnNoContent(id, operations) {
		return Resource{}, nil
	}

	for _, op := range operations {
		switch op.Op {
		case PatchOperationAdd:
			if op.Path != nil {
				h.data[id].resourceAttributes[op.Path.String()] = op.Value
			} else {
				valueMap := op.Value.(map[string]interface{})
				for k, v := range valueMap {
					if arr, ok := h.data[id].resourceAttributes[k].([]interface{}); ok {
						arr = append(arr, v)
						h.data[id].resourceAttributes[k] = arr
					} else {
						h.data[id].resourceAttributes[k] = v
					}
				}
			}
		case PatchOperationReplace:
			if op.Path != nil {
				h.data[id].resourceAttributes[op.Path.String()] = op.Value
			} else {
				valueMap := op.Value.(map[string]interface{})
				for k, v := range valueMap {
					h.data[id].resourceAttributes[k] = v
				}
			}
		case PatchOperationRemove:
			h.data[id].resourceAttributes[op.Path.String()] = nil
		}
	}

	created, _ := time.ParseInLocation(time.RFC3339, fmt.Sprintf("%v", h.data[id].meta["created"]), time.UTC)
	now := time.Now()

	// return resource with replaced attributes
	return Resource{
		ID:         id,
		ExternalID: h.externalID(h.data[id].resourceAttributes),
		Attributes: h.data[id].resourceAttributes,
		Meta: Meta{
			Created:      &created,
			LastModified: &now,
			Version:      fmt.Sprintf("%s.patch", h.data[id].meta["version"]),
		},
	}, nil
}

func (h testResourceHandler) Replace(r *http.Request, id string, attributes ResourceAttributes) (Resource, error) {
	// check if resource exists
	_, ok := h.data[id]
	if !ok {
		return Resource{}, errors.ScimErrorResourceNotFound(id)
	}

	// replace (all) attributes
	h.data[id] = testData{
		resourceAttributes: attributes,
	}

	// return resource with replaced attributes
	return Resource{
		ID:         id,
		ExternalID: h.externalID(attributes),
		Attributes: attributes,
	}, nil
}

func (h testResourceHandler) externalID(attributes ResourceAttributes) optional.String {
	if eID, ok := attributes["externalId"]; ok {
		externalID, ok := eID.(string)
		if !ok {
			return optional.String{}
		}
		return optional.NewString(externalID)
	}

	return optional.String{}
}

func (h testResourceHandler) noContentOperation(id string, op PatchOperation) bool {
	isRemoveOp := strings.EqualFold(op.Op, PatchOperationRemove)

	dataValue, ok := h.data[id]
	if !ok {
		return isRemoveOp
	}
	var path string
	if op.Path != nil {
		path = op.Path.String()
	}
	attrValue, ok := dataValue.resourceAttributes[path]
	if ok && attrValue == op.Value {
		return true
	}
	if !ok && isRemoveOp {
		return true
	}

	switch opValue := op.Value.(type) {
	case map[string]interface{}:
		for k, v := range opValue {
			if v == dataValue.resourceAttributes[k] {
				return true
			}
		}

	case []map[string]interface{}:
		for _, m := range opValue {
			for k, v := range m {
				if v == dataValue.resourceAttributes[k] {
					return true
				}
			}
		}
	}
	return false
}

func (h testResourceHandler) shouldReturnNoContent(id string, ops []PatchOperation) bool {
	for _, op := range ops {
		if h.noContentOperation(id, op) {
			continue
		}
		return false
	}
	return true
}
Output:

true

type ResourceType

type ResourceType struct {
	// ID is the resource type's server unique id. This is often the same value as the "name" attribute.
	ID optional.String
	// Name is the resource type name. This name is referenced by the "meta.resourceType" attribute in all resources.
	Name string
	// Description is the resource type's human-readable description.
	Description optional.String
	// Endpoint is the resource type's HTTP-addressable endpoint relative to the Base URL of the service provider,
	// e.g., "/Users".
	Endpoint string
	// Schema is the resource type's primary/base schema.
	Schema schema.Schema
	// SchemaExtensions is a list of the resource type's schema extensions.
	SchemaExtensions []SchemaExtension

	// Handler is the set of callback method that connect the SCIM server with a provider of the resource type.
	Handler ResourceHandler
}

ResourceType specifies the metadata about a resource type.

type SchemaExtension

type SchemaExtension struct {
	// Schema is the URI of an extended schema, e.g., "urn:edu:2.0:Staff".
	Schema schema.Schema
	// Required is a boolean value that specifies whether or not the schema extension is required for the resource
	// type. If true, a resource of this type MUST include this schema extension and also include any attributes
	// declared as required in this schema extension. If false, a resource of this type MAY omit this schema
	// extension.
	Required bool
}

SchemaExtension is one of the resource type's schema extensions.

type Server

type Server struct {
	Config        ServiceProviderConfig
	ResourceTypes []ResourceType
}

Server represents a SCIM server which implements the HTTP-based SCIM protocol that makes managing identities in multi- domain scenarios easier to support via a standardized service.

func (Server) ServeHTTP

func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP dispatches the request to the handler whose pattern most closely matches the request URL.

type ServiceProviderConfig

type ServiceProviderConfig struct {
	// DocumentationURI is an HTTP-addressable URL pointing to the service provider's human-consumable help
	// documentation.
	DocumentationURI optional.String
	// AuthenticationSchemes is a multi-valued complex type that specifies supported authentication scheme properties.
	AuthenticationSchemes []AuthenticationScheme
	// MaxResults denotes the the integer value specifying the maximum number of resources returned in a response. It defaults to 100.
	MaxResults int
	// SupportFiltering whether you SCIM implementation will support filtering.
	SupportFiltering bool
	// SupportPatch whether your SCIM implementation will support patch requests.
	SupportPatch bool
}

ServiceProviderConfig enables a service provider to discover SCIM specification features in a standardized form as well as provide additional implementation details to clients.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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