Holmes開発者ブログ

契約マネジメントシステム「ホームズクラウド」の開発者ブログです

spring-boot-starter-scim2を使ってSCIM APIを作る

こんにちは、id:c-terashimaです

tl;dr

SCIMとはプロビジョニングやデプロビジョニング用のアカウント・グループ情報をRESTful APIで操作するプロトコルです
基幹システムで管理しているアカウント情報を別システムに流し込むのに利用され、ホームズクラウドでも6月にリリース予定です

spring-boot-starter-scim2の利用方法をまとめていこうと思います

参考

spring-boot-starter-scim2とは

SpringBoot上で SCIM2 SDK を利用するためのOSSになります
導入することで以下のメリットがあると考え導入しました

  • Request/ResponseのEntity
  • ServiceProvider、ResourceTypesなどのエンドポイントの自動生成
  • フィルター文字列の解析

github.com

環境

開発環境は以下の通りです

  • Java 8
  • Gradle 5.4.1
  • Kotlin 1.3.71
  • SpringBoot 2.2.6 RELEASE

build.gradle

以下のようにdependencyを追加します

dependencies {
    implementation 'com.bettercloud:spring-boot-starter-scim2:1.0.0'
    implementation 'com.bettercloud:scim2-sdk-common:1.0.0'
    implementation "com.unboundid.product.scim2:scim2-sdk-common:2.3.3"
    implementation "com.unboundid.product.scim2:scim2-sdk-server:2.3.3"
}

com.bettercloudでは足りない機能をcom.unboundidで補う必要があるため、追加します

エンドポイント

spring-boot-starter-scim2 は以下のエンドポイントを自動で出力してくれます
ServiceProviderConfigapplication.yml に出力する情報を記載するだけでOKで、残りの2つは@ScimResourceをControllerクラスに付与するだけです

@ScimResource(description = "Access User Resources", name = "User", schema = UserResource::class)
  • /ServiceProviderConfig
  • /ResourceTypes
  • /Schemas
scim2:
  service-provider-config:
    documentationUri: http://www.simplecloud.info
    patch:
      supported: true
    bulk:
      supported: true
      maxOperations: 1000
      maxPayloadSize: 10000
    filter:
      supported: true
      maxResults: 100
    change-password:
      supported: false
    sort:
      supported: true
    etag:
      supported: false
    authenticationSchemes:
      - name: SCIM
        description: SCIM
        specUri: http://localhost:8080
        documentationUri: http://localhost:8080
        type: oauthbearertoken
        primary: true

各項目についてはGithubに説明がありますので、そちらをご覧いただければと思います

github.com

/ServiceProviderConfigのResponse JSON

{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
  ],
  "patch": {
    "supported": true
  },
  "bulk": {
    "supported": false,
    "maxOperations": 1000,
    "maxPayloadSize": 10000
  },
  "filter": {
    "supported": true,
    "maxResults": 100
  },
  "changePassword": {
    "supported": false
  },
  "sort": {
    "supported": false
  },
  "etag": {
    "supported": false
  },
  "meta": {
    "resourceType": "ServiceProviderConfig",
    "location": "http://localhost:8080/ServiceProviderConfig"
  }
}

/ResourceTypesのResponse JSON

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:ListResponse"
  ],
  "id": null,
  "externalId": null,
  "meta": null,
  "totalResults": 2,
  "Resources": [
    {
      "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
      ],
      "id": "User",
      "name": "User",
      "description": "Access User Resources",
      "endpoint": "/Users",
      "meta": {
        "resourceType": "ResourceType",
        "location": "http://localhost:8080/ResourceTypes/User"
      }
    },
    {
      "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
      ],
      "id": "Group",
      "name": "Group",
      "description": "Access Group Resources",
      "endpoint": "/Groups",
      "meta": {
        "resourceType": "ResourceType",
        "location": "http://localhost:8080/ResourceTypes/Group"
      }
    }
  ],
  "startIndex": 1,
  "itemsPerPage": 2
}

/SchemasのResponse JSON

出力量が多いので折りたたんでおります

JSONを見る

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:ListResponse"
  ],
  "id": null,
  "externalId": null,
  "meta": null,
  "totalResults": 1,
  "Resources": [
    {
      "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:Schema"
      ],
      "id": "urn:ietf:params:scim:schemas:core:2.0:User",
      "name": "User",
      "description": "User Account",
      "attributes": [
        {
          "name": "active",
          "type": "boolean",
          "multiValued": false,
          "description": "A Boolean value indicating the User's administrative status.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "addresses",
          "type": "complex",
          "subAttributes": [
            {
              "name": "country",
              "type": "string",
              "multiValued": false,
              "description": "The country name component.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "formatted",
              "type": "string",
              "multiValued": false,
              "description": "The full mailing address, formatted for display or use with a mailing label. This attribute MAY contain newlines.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "locality",
              "type": "string",
              "multiValued": false,
              "description": "The city or locality component.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "postalCode",
              "type": "string",
              "multiValued": false,
              "description": "The zipcode or postal code component.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "primary",
              "type": "boolean",
              "multiValued": false,
              "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred address. The primary attribute value 'true' MUST appear no more than once.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "region",
              "type": "string",
              "multiValued": false,
              "description": "The state or region component.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "streetAddress",
              "type": "string",
              "multiValued": false,
              "description": "The full street address component, which may include house number, street name, PO BOX, and multi-line extended street address information. This attribute MAY contain newlines.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function; e.g., 'work' or 'home'.",
              "required": false,
              "canonicalValues": [
                "other",
                "work",
                "home"
              ],
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": true,
          "description": "Physical mailing addresses for this User.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "displayName",
          "type": "string",
          "multiValued": false,
          "description": "The name of the User, suitable for display to end-users. The name SHOULD be the full name of the User being described if known.",
          "required": false,
          "caseExact": false,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "emails",
          "type": "complex",
          "subAttributes": [
            {
              "name": "display",
              "type": "string",
              "multiValued": false,
              "description": "A human readable name, primarily used for display purposes.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "primary",
              "type": "boolean",
              "multiValued": false,
              "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred mailing address or primary e-mail address. The primary attribute value 'true' MUST appear no more than once.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function; e.g., 'work' or 'home'.",
              "required": false,
              "canonicalValues": [
                "other",
                "work",
                "home"
              ],
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "value",
              "type": "string",
              "multiValued": false,
              "description": "E-mail addresses for the user. The value\nSHOULD be canonicalized by the Service Provider, e.g.\nbjensen@example.com instead of bjensen@EXAMPLE.COM. Canonical Type\nvalues of work, home, and other.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": true,
          "description": "E-mail addresses for the user. The value SHOULD be canonicalized by the Service Provider, e.g., bjensen@example.com instead of bjensen@EXAMPLE.COM. Canonical Type values of work, home, and other.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "entitlements",
          "type": "complex",
          "subAttributes": [
            {
              "name": "display",
              "type": "string",
              "multiValued": false,
              "description": "A human readable name, primarily used for display purposes.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "primary",
              "type": "boolean",
              "multiValued": false,
              "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "value",
              "type": "string",
              "multiValued": false,
              "description": "The value of an entitlement.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": true,
          "description": "A list of entitlements for the User that represent a thing the User has.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "groups",
          "type": "complex",
          "subAttributes": [
            {
              "name": "display",
              "type": "string",
              "multiValued": false,
              "description": "A human readable name, primarily used for display purposes.",
              "required": false,
              "caseExact": false,
              "mutability": "readOnly",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "$ref",
              "type": "reference",
              "multiValued": false,
              "description": "The URI of the corresponding Group resource to which the user belongs",
              "required": false,
              "caseExact": true,
              "mutability": "readOnly",
              "returned": "default",
              "uniqueness": "none",
              "referenceTypes": [
                "Group",
                "User"
              ]
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function; e.g., 'direct' or 'indirect'.",
              "required": false,
              "canonicalValues": [
                "indirect",
                "direct"
              ],
              "caseExact": false,
              "mutability": "readOnly",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "value",
              "type": "string",
              "multiValued": false,
              "description": "The identifier of the User's group.",
              "required": false,
              "caseExact": false,
              "mutability": "readOnly",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": true,
          "description": "A list of groups that the user belongs to, either thorough direct membership, nested groups, or dynamically calculated.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "ims",
          "type": "complex",
          "subAttributes": [
            {
              "name": "display",
              "type": "string",
              "multiValued": false,
              "description": "A human readable name, primarily used for display purposes.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "primary",
              "type": "boolean",
              "multiValued": false,
              "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred messenger or primary messenger. The primary attribute value 'true' MUST appear no more than once.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function; e.g., 'aim', 'gtalk', 'mobile' etc.",
              "required": false,
              "canonicalValues": [
                "qq",
                "skype",
                "gtalk",
                "aim",
                "icq",
                "yahoo",
                "msn",
                "xmpp"
              ],
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "value",
              "type": "string",
              "multiValued": false,
              "description": "Instant messaging address for the User.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": true,
          "description": "Instant messaging addresses for the User.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "locale",
          "type": "string",
          "multiValued": false,
          "description": "Used to indicate the User's default location for purposes of localizing items such as currency, date time format, numerical representations, etc.",
          "required": false,
          "caseExact": false,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "name",
          "type": "complex",
          "subAttributes": [
            {
              "name": "familyName",
              "type": "string",
              "multiValued": false,
              "description": "The family name of the User, or Last Name in most Western languages (for example, Jensen given the full name Ms. Barbara J Jensen, III.).",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "formatted",
              "type": "string",
              "multiValued": false,
              "description": "The full name, including all middle names, titles, and suffixes as appropriate, formatted for display (for example, Ms. Barbara J Jensen, III.).",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "givenName",
              "type": "string",
              "multiValued": false,
              "description": "The given name of the User, or First Name in most Western languages (for example, Barbara given the full name Ms. Barbara J Jensen, III.).",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "honorificPrefix",
              "type": "string",
              "multiValued": false,
              "description": "The honorific prefix(es) of the User, or Title in most Western languages (for example, Ms. given the full name Ms. Barbara J Jensen, III.).",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "honorificSuffix",
              "type": "string",
              "multiValued": false,
              "description": "The honorific suffix(es) of the User, or Suffix in most Western languages (for example, III. given the full name Ms. Barbara J Jensen, III.)",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "middleName",
              "type": "string",
              "multiValued": false,
              "description": "The middle name(s) of the User (for example, Robert given the full name Ms. Barbara J Jensen, III.).",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": false,
          "description": "The components of the user's real name.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "nickName",
          "type": "string",
          "multiValued": false,
          "description": "The casual way to address the user in real life, e.g.'Bob' or 'Bobby' instead of 'Robert'. This attribute SHOULD NOT be used to represent a User's username (e.g., bjensen or mpepperidge)",
          "required": false,
          "caseExact": false,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "password",
          "type": "string",
          "multiValued": false,
          "description": "The User's clear text password. This attribute is intended to be used as a means to specify an initial password when creating a new User or to reset an existing User's password.",
          "required": false,
          "caseExact": false,
          "mutability": "writeOnly",
          "returned": "never",
          "uniqueness": "none"
        },
        {
          "name": "phoneNumbers",
          "type": "complex",
          "subAttributes": [
            {
              "name": "display",
              "type": "string",
              "multiValued": false,
              "description": "A human readable name, primarily used for display purposes.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "primary",
              "type": "boolean",
              "multiValued": false,
              "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred phone number or primary phone number. The primary attribute value 'true' MUST appear no more than once.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function; e.g., 'work' or 'home' or 'mobile' etc.",
              "required": false,
              "canonicalValues": [
                "other",
                "pager",
                "work",
                "mobile",
                "fax",
                "home"
              ],
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "value",
              "type": "string",
              "multiValued": false,
              "description": "Phone number of the User",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": true,
          "description": "Phone numbers for the User.  The value SHOULD be canonicalized by the Service Provider according to format in RFC3966 e.g., 'tel:+1-201-555-0123'.  Canonical Type values of work, home, mobile, fax, pager and other.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "photos",
          "type": "complex",
          "subAttributes": [
            {
              "name": "display",
              "type": "string",
              "multiValued": false,
              "description": "A human readable name, primarily used for display purposes.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "primary",
              "type": "boolean",
              "multiValued": false,
              "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred messenger or primary messenger. The primary attribute value 'true' MUST appear no more than once.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function; e.g., 'photo' or 'thumbnail'.",
              "required": false,
              "canonicalValues": [
                "thumbnail",
                "photo"
              ],
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "value",
              "type": "reference",
              "multiValued": false,
              "description": "URI of a photo of the User.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none",
              "referenceTypes": [
                "external"
              ]
            }
          ],
          "multiValued": true,
          "description": "URIs of photos of the User.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "preferredLanguage",
          "type": "string",
          "multiValued": false,
          "description": "Indicates the User's preferred written or spoken language.  Generally used for selecting a localized User interface. e.g., 'en_US' specifies the language English and country US.",
          "required": false,
          "caseExact": false,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "profileUrl",
          "type": "reference",
          "multiValued": false,
          "description": "A fully qualified URL to a page representing the User's online profile",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none",
          "referenceTypes": [
            "external"
          ]
        },
        {
          "name": "roles",
          "type": "complex",
          "subAttributes": [
            {
              "name": "display",
              "type": "string",
              "multiValued": false,
              "description": "A human readable name, primarily used for display purposes.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "primary",
              "type": "boolean",
              "multiValued": false,
              "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "value",
              "type": "string",
              "multiValued": false,
              "description": "The value of a role.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": true,
          "description": "A list of roles for the User that collectively represent who the User is; e.g., 'Student', 'Faculty'.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "timezone",
          "type": "string",
          "multiValued": false,
          "description": "The User's time zone in the 'Olson' timezone database format; e.g.,'America/Los_Angeles'",
          "required": false,
          "caseExact": false,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "title",
          "type": "string",
          "multiValued": false,
          "description": "The user's title, such as \"Vice President\".",
          "required": false,
          "caseExact": false,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "userName",
          "type": "string",
          "multiValued": false,
          "description": "Unique identifier for the User typically used by the user to directly authenticate to the service provider.",
          "required": true,
          "caseExact": false,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "server"
        },
        {
          "name": "userType",
          "type": "string",
          "multiValued": false,
          "description": "Used to identify the organization to user relationship. Typical values used might be 'Contractor', 'Employee', 'Intern', 'Temp', 'External', and 'Unknown' but any value may be used.",
          "required": false,
          "caseExact": false,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        },
        {
          "name": "x509Certificates",
          "type": "complex",
          "subAttributes": [
            {
              "name": "display",
              "type": "string",
              "multiValued": false,
              "description": "A human readable name, primarily used for display purposes.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "primary",
              "type": "boolean",
              "multiValued": false,
              "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.",
              "required": false,
              "caseExact": true,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "type",
              "type": "string",
              "multiValued": false,
              "description": "A label indicating the attribute's function.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            },
            {
              "name": "value",
              "type": "binary",
              "multiValued": false,
              "description": "The value of a X509 certificate.",
              "required": false,
              "caseExact": false,
              "mutability": "readWrite",
              "returned": "default",
              "uniqueness": "none"
            }
          ],
          "multiValued": true,
          "description": "A list of certificates issued to the User.",
          "required": false,
          "caseExact": true,
          "mutability": "readWrite",
          "returned": "default",
          "uniqueness": "none"
        }
      ],
      "meta": {
        "resourceType": "Schema",
        "location": "http://localhost:8080/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"
      }
    }
  ],
  "startIndex": 1,
  "itemsPerPage": 1
}

UsersとGroups

ユーザとグループのCRUDは実装する必要があります

Get

単体複数のリソースを取得する2つのエンドポイントを作る必要があります
複数のリソースを取得するエンドポイントはFilter指定が可能で、以下のようなクエリパラータでアクセスされます

/Users?filter=userName eq "user name"

eqequalの略で他に指定される条件は以下のようになっています

Operator Description
eq equal
co contains
sw starts with
pr present value
gt greater than
ge greater than or equal
lt less than
le less than or equal
and logical And
or logical Or

これらの条件を独自に実装し絞り込むのはなかなか大変ではありますが、条件解析もフレームワークが用意してくれています
パラメータで受け取った絞り込み文字列をFilterクラスを通して絞り込みを行います

@GetMapping
fun search(request: HttpServletRequest, @ModelAttribute searchRequest: SearchRequest
           , @RequestParam(value = ApiConstants.QUERY_PARAMETER_FILTER, required = false) filterString: String?
): ResponseEntity<ListResponse<GenericScimResource>> {
    // 対象全リソース取得
    val resources = getResources()

    // 絞り込み
    val filter: Filter? =
        if(filterString != null) Filter.fromString(filterString) else null
    val result = if(filter != null) {
        resources.filter { it ->
            FilterEvaluator.evaluate(filter, it.objectNode)
        }
    } else resources

    val listResponse = ListResponse(result.size, result, 1, result.size)

    return ResponseEntity.ok(listResponse)
}

POST

こちらはユーザやグループを登録するのですが、UserResourceに予めValidation設定が記載されているので、SchemaCheckerを利用して入力チェックを行うことができます

@PostMapping
fun create(request: HttpServletRequest, @RequestBody data: UserResource): ResponseEntity<GenericScimResource> {
    parameterValidation(data)

    val response = createResource(data)
    return ResponseEntity.created(createLocation(response.id)).body(response)
}

private fun parameterValidation(data: UserResource) {
    val coreSchema = getSchema()
    val schemaExtensions = getSchemaExtensions()

    val builder =
            ResourceTypeDefinition.Builder("test", "/test")
                    .setCoreSchema(coreSchema)
                    .addOptionalSchemaExtension(schemaExtensions)

    val resourceTypeDefinition: ResourceTypeDefinition = builder.build()
    val checker = SchemaChecker(resourceTypeDefinition)
    val resource = checker.removeReadOnlyAttributes(JsonUtils.valueToNode(data))
    val results = checker.checkCreate(resource)
    if (results.syntaxIssues.isNotEmpty()) {
        throw BadRequestException.invalidSyntax(results.syntaxIssues.joinToString())
    }
}

PATCH

リソースを一部更新するのに利用されます
以下はグループの名前変更メンバー追加を行っており、複数の更新をサポートする必要があります

{
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [
        {
            "op": "replace",
            "path": "displayName",
            "value": "1879db59-3bdf-4490-ad68-ab880a269474updatedDisplayName"
        },
        {
            "op": "add",
            "path": "members",
            "value": [{
                "$ref": null,
                "value": "f648f8d5ea4e4cd38e9c"
            }
    ]
}

opには以下の3つが利用可能でEnumで定義されています

  • add
  • replace
  • remove
@PatchMapping("/{id}")
fun update(request: HttpServletRequest, @PathVariable("id") id: String
          , @RequestBody data: PatchRequest): ResponseEntity<Void> {

    updateResource(data, id)
    return ResponseEntity.noContent().build()
}

総評

exmapleも少なくGitHubユニットテストを解析しながらの作業でしたが、手間のかかるRequest/Responseクラスの作成や標準エンドポイントの自動出力など大変助かることが多かったかと思います
BulkUsersGroupsも標準規約があるのでフレームワークで用意してくれていてもいいのかなと感じました
時間があればPull Requestで改善を試みたいと思います

エンジニア・デザイナーを募集しておりますので、ご興味がある方はこちらからご連絡ください。

lab.holmescloud.com

lab.holmescloud.com