Skip to content

cerberauth/scimply

Repository files navigation

scimply

A complete SCIM 2.0 (and 1.1) server library for Go — RFC-compliant, zero-dependency core, and multiple backends.

Join Discord Build Latest version Coverage Go Report Card GoDoc Stars License


A complete SCIM 2.0 (and 1.1) server library for Go.

Features

  • Full SCIM 2.0 (RFC 7642, RFC 7643, RFC 7644) implementation
  • SCIM 1.1 support via a conversion layer
  • Zero-dependency coreschema/, resource/, protocol/, store/, server/ use only the Go standard library
  • Filter parser — recursive-descent ABNF parser for the full RFC 7644 filter grammar
  • PATCH engine — complete RFC 7644 §3.5.2 semantics with atomicity
  • Multiple backends — in-memory, PostgreSQL, MySQL/MariaDB, MongoDB, SCIM proxy
  • Compliance test suite — reusable against any SCIM server
  • Structured audit logging

Quick Start

package main

import (
    "log"
    "net/http"

    "github.com/cerberauth/scimply/schema"
    "github.com/cerberauth/scimply/server"
    "github.com/cerberauth/scimply/store"
)

func main() {
    reg := schema.NewRegistry()
    reg.RegisterDefaults() // User, Group, EnterpriseUser

    srv, err := server.New(
        server.WithStore(store.NewMemoryStore()),
        server.WithSchemaRegistry(reg),
        server.WithBasePath("/scim/v2"),
        server.WithBearerTokenAuth(func(token string) (bool, error) {
            return token == "my-secret-token", nil
        }),
    )
    if err != nil {
        log.Fatal(err)
    }

    log.Println("SCIM 2.0 server listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", srv))
}

API Endpoints

Method Path Description
GET /scim/v2/ServiceProviderConfig Server capabilities
GET /scim/v2/ResourceTypes Supported resource types
GET /scim/v2/Schemas Schema definitions
GET /scim/v2/Users List users (supports filter, sort, pagination)
POST /scim/v2/Users Create user
GET /scim/v2/Users/{id} Get user
PUT /scim/v2/Users/{id} Replace user
PATCH /scim/v2/Users/{id} Patch user
DELETE /scim/v2/Users/{id} Delete user
POST /scim/v2/Users/.search Query via POST body
POST /scim/v2/Bulk Bulk operations
(same for Groups)

Backends

In-Memory (development and testing)

store.NewMemoryStore()

PostgreSQL

import "github.com/cerberauth/scimply/connector/postgres"

pgStore, err := postgres.New(
    postgres.WithDSN("postgres://user:pass@localhost:5432/scimdb"),
    postgres.WithAutoMigrate(true),
    postgres.WithTablePrefix("scim_"),
    postgres.WithSchemaRegistry(reg),
)
if err != nil { log.Fatal(err) }
defer pgStore.Close(context.Background())
if err := pgStore.Init(context.Background()); err != nil { log.Fatal(err) }

MySQL / MariaDB

import "github.com/cerberauth/scimply/connector/mysql"

myStore, err := mysql.New(
    mysql.WithDSN("user:pass@tcp(localhost:3306)/scimdb?parseTime=true"),
    mysql.WithAutoMigrate(true),
)

MongoDB

import "github.com/cerberauth/scimply/connector/mongodb"

mgStore, err := mongodb.New(
    mongodb.WithURI("mongodb://localhost:27017"),
    mongodb.WithDatabase("scimdb"),
    mongodb.WithAutoMigrate(true),
)

Table & Field Mapping (existing schemas)

All SQL connectors support mapping each SCIM resource type to a specific table and each SCIM attribute to a specific column. Columns can live in different tables — declare JOINs with JoinDef. MongoDB supports mapping to a specific collection and arbitrary BSON field paths, with optional cross-collection $lookup.

When FieldMappings is set for a resource type the connector enters column mode: it reads and writes individual columns instead of a JSON blob, and AutoMigrate is skipped for that type.

import (
    sqlconn "github.com/cerberauth/scimply/connector/sql"
    "github.com/cerberauth/scimply/connector/postgres"
)

pgStore, err := postgres.New(
    postgres.WithDSN(dsn),

    // Map "User" to existing tables — no data column required.
    postgres.WithResourceConfig("User", postgres.ResourceTableConfig{
        Table: "accounts",
        FieldMappings: map[string]sqlconn.ColumnRef{
            "id":                {Column: "id"},
            "userName":          {Column: "email"},
            "active":            {Column: "is_active"},
            "meta.created":      {Column: "created_at"},
            "meta.lastModified": {Column: "updated_at"},
            // Attributes in a joined table:
            "name.givenName":  {Table: "profiles", Column: "first_name"},
            "name.familyName": {Table: "profiles", Column: "last_name"},
        },
        Joins: []postgres.JoinDef{{
            Table:      "profiles",
            Condition:  "profiles.account_id = accounts.id",
            JoinType:   "LEFT",
            ForeignKey: "account_id", // FK column used for writes
            DeleteJoin: true,
        }},
    }),

    // Convenience helpers build up the same config incrementally:
    //   postgres.WithTableName("Group", "teams")
    //   postgres.WithFieldMapping("Group", "displayName", "name")
    //   postgres.WithTableFieldMapping("Group", "members.value", "team_members", "user_id")
    //   postgres.WithJoin("Group", postgres.JoinDef{...})

    postgres.WithSchemaRegistry(reg),
)

// Access the raw connection pool for queries beyond the declarative config.
pool := pgStore.Pool() // *pgxpool.Pool

For MongoDB:

import "github.com/cerberauth/scimply/connector/mongodb"

mgStore, err := mongodb.New(
    mongodb.WithURI(uri),
    mongodb.WithDatabase("myapp"),
    // Map "User" to the "users" collection with custom field paths.
    mongodb.WithResourceConfig("User", mongodb.ResourceCollectionConfig{
        Collection: "users",
        FieldMappings: map[string]string{
            "id":       "_id",
            "userName": "email",
            "active":   "account.active",
        },
        // Cross-collection lookup for 1:1 profile documents:
        Lookups: []mongodb.LookupConfig{{
            From: "profiles", LocalField: "_id", ForeignField: "userId", As: "profile",
            FieldMappings: map[string]string{
                "name.givenName":  "profile.firstName",
                "name.familyName": "profile.lastName",
            },
        }},
    }),
)

db := mgStore.Database() // *mongo.Database — escape hatch for complex queries

SCIM-to-SCIM Proxy

import scimclient "github.com/cerberauth/scimply/connector/scim"

upstream, err := scimclient.New(
    scimclient.WithBaseURL("https://api.example.com/scim/v2"),
    scimclient.WithBearerToken("upstream-token"),
)

Filter Support

Full RFC 7644 filter syntax:

GET /scim/v2/Users?filter=userName eq "bjensen"
GET /scim/v2/Users?filter=emails[type eq "work" and value co "@example.com"]
GET /scim/v2/Users?filter=active eq true and userType eq "Employee"
GET /scim/v2/Users?filter=not (userName eq "bjensen")

PATCH Operations

Full RFC 7644 §3.5.2 PATCH semantics:

PATCH /scim/v2/Users/123
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    {"op": "replace", "path": "active", "value": false},
    {"op": "add", "path": "emails", "value": [{"type": "home", "value": "home@example.com"}]},
    {"op": "replace", "path": "emails[type eq \"work\"].value", "value": "new@example.com"},
    {"op": "remove", "path": "members[value eq \"user-456\"]"}
  ]
}

SCIM 1.1

The library supports SCIM 1.1 via a conversion layer in the v1/ package. Resources are converted to/from the internal SCIM 2.0 representation at the boundary.

Key 1.1 differences handled:

  • Schema URIs: urn:scim:schemas:core:1.0urn:ietf:params:scim:schemas:core:2.0:User
  • Service provider endpoint: /ServiceProviderConfigs (plural)
  • Error format: {"Errors": [{"code": "404", "description": "..."}]}
  • Simplified filter operators (no ne, ew, not, [])

Compliance Test Suite

Run the standard SCIM 2.0 compliance suite against any server:

import "github.com/cerberauth/scimply/compliance"

func TestMyServer(t *testing.T) {
    srv := startMyServer(t)
    compliance.RunSuite(t, compliance.SuiteConfig{
        BaseURL:    srv.URL + "/scim/v2",
        AuthHeader: "Bearer test-token",
    })
}

License

This repository is licensed under the MIT License @ CerberAuth. You are free to use, modify, and distribute the contents of this repository for educational and testing purposes.

About

A complete SCIM 2.0 (and 1.1) server library for Go — RFC-compliant, zero-dependency core, and multiple backends.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages