mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 20:57:24 +00:00
feat: add scim v2 service provider configuration endpoints (#9258)
# Which Problems Are Solved * Adds support for the service provider configuration SCIM v2 endpoints # How the Problems Are Solved * Adds support for the service provider configuration SCIM v2 endpoints * `GET /scim/v2/{orgId}/ServiceProviderConfig` * `GET /scim/v2/{orgId}/ResourceTypes` * `GET /scim/v2/{orgId}/ResourceTypes/{name}` * `GET /scim/v2/{orgId}/Schemas` * `GET /scim/v2/{orgId}/Schemas/{id}` # Additional Context Part of #8140 Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
parent
b6841251b1
commit
e15094cdea
@ -580,6 +580,13 @@ SAML:
|
|||||||
# EmailAddress: hi@zitadel.com # ZITADEL_SAML_PROVIDERCONFIG_CONTACTPERSON_EMAILADDRESS
|
# EmailAddress: hi@zitadel.com # ZITADEL_SAML_PROVIDERCONFIG_CONTACTPERSON_EMAILADDRESS
|
||||||
|
|
||||||
SCIM:
|
SCIM:
|
||||||
|
DocumentationUrl: https://zitadel.com/docs/guides/manage/user/scim2
|
||||||
|
AuthenticationSchemes:
|
||||||
|
- Name: Zitadel authentication token
|
||||||
|
Description: Authentication scheme using the OAuth Bearer Token Standard
|
||||||
|
SpecUri: https://www.rfc-editor.org/info/rfc6750
|
||||||
|
DocumentationUri: https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users
|
||||||
|
Type: oauthbearertoken
|
||||||
# default values whether an email/phone is considered verified when a users email/phone is created or updated
|
# default values whether an email/phone is considered verified when a users email/phone is created or updated
|
||||||
EmailVerified: true # ZITADEL_SCIM_EMAILVERIFIED
|
EmailVerified: true # ZITADEL_SCIM_EMAILVERIFIED
|
||||||
PhoneVerified: true # ZITADEL_SCIM_PHONEVERIFIED
|
PhoneVerified: true # ZITADEL_SCIM_PHONEVERIFIED
|
||||||
|
@ -1,12 +1,23 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
EmailVerified bool
|
DocumentationUrl string
|
||||||
PhoneVerified bool
|
AuthenticationSchemes []*ServiceProviderConfigAuthenticationScheme
|
||||||
MaxRequestBodySize int64
|
EmailVerified bool
|
||||||
Bulk BulkConfig
|
PhoneVerified bool
|
||||||
|
MaxRequestBodySize int64
|
||||||
|
Bulk BulkConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkConfig struct {
|
type BulkConfig struct {
|
||||||
MaxOperationsCount int
|
MaxOperationsCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServiceProviderConfigAuthenticationScheme struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
SpecUri string `json:"specUri"`
|
||||||
|
DocumentationUri string `json:"documentationUri"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Primary bool `json:"primary"`
|
||||||
|
}
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package integration_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed testdata/service_provider_config_expected.json
|
||||||
|
expectedProviderConfigJson []byte
|
||||||
|
|
||||||
|
//go:embed testdata/service_provider_config_expected_schemas.json
|
||||||
|
expectedSchemasJson []byte
|
||||||
|
|
||||||
|
//go:embed testdata/service_provider_config_expected_resource_types.json
|
||||||
|
expectedResourceTypesJson []byte
|
||||||
|
|
||||||
|
//go:embed testdata/service_provider_config_expected_resource_type_user.json
|
||||||
|
expectedResourceTypeUserJson []byte
|
||||||
|
|
||||||
|
//go:embed testdata/service_provider_config_expected_user_schema.json
|
||||||
|
expectedUserSchemaJson []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServiceProviderConfig(t *testing.T) {
|
||||||
|
resp, err := Instance.Client.SCIM.GetServiceProviderConfig(CTX, Instance.DefaultOrg.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assertJsonEqual(t, expectedProviderConfigJson, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceTypes(t *testing.T) {
|
||||||
|
resp, err := Instance.Client.SCIM.GetResourceTypes(CTX, Instance.DefaultOrg.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assertJsonEqual(t, expectedResourceTypesJson, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
resourceName string
|
||||||
|
want []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
resourceName: "User",
|
||||||
|
want: expectedResourceTypeUserJson,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not found",
|
||||||
|
resourceName: "foobar",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
resp, err := Instance.Client.SCIM.GetResourceType(CTX, Instance.DefaultOrg.Id, tt.resourceName)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assertJsonEqual(t, tt.want, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchemas(t *testing.T) {
|
||||||
|
resp, err := Instance.Client.SCIM.GetSchemas(CTX, Instance.DefaultOrg.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assertJsonEqual(t, expectedSchemasJson, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchema(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
id string
|
||||||
|
want []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
id: "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
|
want: expectedUserSchemaJson,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not found",
|
||||||
|
id: "foobar",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
resp, err := Instance.Client.SCIM.GetSchema(CTX, Instance.DefaultOrg.Id, tt.id)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assertJsonEqual(t, tt.want, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertJsonEqual(t *testing.T, expected, actual []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// replace dynamic data json
|
||||||
|
expectedJson := strings.Replace(string(expected), "{domain}", Instance.Domain, 1)
|
||||||
|
expectedJson = strings.Replace(expectedJson, "{orgId}", Instance.DefaultOrg.Id, 1)
|
||||||
|
assert.Equal(t, normalizeJson(t, []byte(expectedJson)), normalizeJson(t, actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeJson(t *testing.T, content []byte) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
raw := new(json.RawMessage)
|
||||||
|
err := json.Unmarshal(content, raw)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
content, err = json.MarshalIndent(raw, "", " ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return string(content) // use string for easier assertion diffs
|
||||||
|
}
|
41
internal/api/scim/integration_test/testdata/service_provider_config_expected.json
vendored
Normal file
41
internal/api/scim/integration_test/testdata/service_provider_config_expected.json
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"schemas": [
|
||||||
|
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"resourceType": "ServiceProviderConfig",
|
||||||
|
"location": "http://{domain}:8080/scim/v2/{orgId}/ServiceProviderConfig"
|
||||||
|
},
|
||||||
|
"documentationUri": "https://zitadel.com/docs/guides/manage/user/scim2",
|
||||||
|
"patch": {
|
||||||
|
"supported": true
|
||||||
|
},
|
||||||
|
"bulk": {
|
||||||
|
"supported": true,
|
||||||
|
"maxOperations": 100,
|
||||||
|
"maxPayloadSize": 1000000
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"supported": true,
|
||||||
|
"maxResults": 100
|
||||||
|
},
|
||||||
|
"changePassword": {
|
||||||
|
"supported": true
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"supported": true
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"supported": false
|
||||||
|
},
|
||||||
|
"authenticationSchemes": [
|
||||||
|
{
|
||||||
|
"name": "Zitadel authentication token",
|
||||||
|
"description": "Authentication scheme using the OAuth Bearer Token Standard",
|
||||||
|
"specUri": "https://www.rfc-editor.org/info/rfc6750",
|
||||||
|
"documentationUri": "https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users",
|
||||||
|
"type": "oauthbearertoken",
|
||||||
|
"primary": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"schemas": [
|
||||||
|
"urn:ietf:params:scim:schemas:core:2.0:ResourceType"
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"resourceType": "User",
|
||||||
|
"location": "http://{domain}:8080/scim/v2/{orgId}/ResourceTypes/User"
|
||||||
|
},
|
||||||
|
"id": "User",
|
||||||
|
"name": "User",
|
||||||
|
"endpoint": "Users",
|
||||||
|
"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
|
"description": "User Account"
|
||||||
|
}
|
24
internal/api/scim/integration_test/testdata/service_provider_config_expected_resource_types.json
vendored
Normal file
24
internal/api/scim/integration_test/testdata/service_provider_config_expected_resource_types.json
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"schemas": [
|
||||||
|
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
||||||
|
],
|
||||||
|
"itemsPerPage": 100,
|
||||||
|
"totalResults": 1,
|
||||||
|
"startIndex": 1,
|
||||||
|
"Resources": [
|
||||||
|
{
|
||||||
|
"schemas": [
|
||||||
|
"urn:ietf:params:scim:schemas:core:2.0:ResourceType"
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"resourceType": "User",
|
||||||
|
"location": "http://{domain}:8080/scim/v2/{orgId}/ResourceTypes/User"
|
||||||
|
},
|
||||||
|
"id": "User",
|
||||||
|
"name": "User",
|
||||||
|
"endpoint": "Users",
|
||||||
|
"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
|
"description": "User Account"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
601
internal/api/scim/integration_test/testdata/service_provider_config_expected_schemas.json
vendored
Normal file
601
internal/api/scim/integration_test/testdata/service_provider_config_expected_schemas.json
vendored
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
{
|
||||||
|
"schemas": [
|
||||||
|
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
||||||
|
],
|
||||||
|
"itemsPerPage": 100,
|
||||||
|
"totalResults": 1,
|
||||||
|
"startIndex": 1,
|
||||||
|
"Resources": [
|
||||||
|
{
|
||||||
|
"schemas": [
|
||||||
|
"urn:ietf:params:scim:schemas:core:2.0:Schema"
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"resourceType": "Schema",
|
||||||
|
"location": "http://{domain}:8080/scim/v2/{orgId}/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"
|
||||||
|
},
|
||||||
|
"id": "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
|
"name": "User",
|
||||||
|
"description": "User Account",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "externalId",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": false,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "formatted",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "familyName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "givenName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "middleName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "honorificPrefix",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "honorificSuffix",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "displayName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nickName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "profileUrl",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "preferredLanguage",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "locale",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timezone",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "active",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "emails",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phoneNumbers",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "writeOnly",
|
||||||
|
"returned": "never",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ims",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "addresses",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "streetAddress",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "locality",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "region",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "postalCode",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "country",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "formatted",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "photos",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entitlements",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "roles",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
591
internal/api/scim/integration_test/testdata/service_provider_config_expected_user_schema.json
vendored
Normal file
591
internal/api/scim/integration_test/testdata/service_provider_config_expected_user_schema.json
vendored
Normal file
@ -0,0 +1,591 @@
|
|||||||
|
{
|
||||||
|
"schemas": [
|
||||||
|
"urn:ietf:params:scim:schemas:core:2.0:Schema"
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"resourceType": "Schema",
|
||||||
|
"location": "http://{domain}:8080/scim/v2/{orgId}/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"
|
||||||
|
},
|
||||||
|
"id": "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
|
"name": "User",
|
||||||
|
"description": "User Account",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "externalId",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": false,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "formatted",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "familyName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "givenName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "middleName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "honorificPrefix",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "honorificSuffix",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "displayName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nickName",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "profileUrl",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "preferredLanguage",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "locale",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timezone",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "active",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "emails",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": true,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phoneNumbers",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "writeOnly",
|
||||||
|
"returned": "never",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ims",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "addresses",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "streetAddress",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "locality",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "region",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "postalCode",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "country",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "formatted",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "photos",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entitlements",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "roles",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "complex",
|
||||||
|
"subAttributes": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "string",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary",
|
||||||
|
"description": "For details see RFC7643",
|
||||||
|
"type": "boolean",
|
||||||
|
"multiValued": false,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiValued": true,
|
||||||
|
"required": false,
|
||||||
|
"caseExact": true,
|
||||||
|
"mutability": "readWrite",
|
||||||
|
"returned": "always",
|
||||||
|
"uniqueness": "none"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -60,7 +60,7 @@ func NewBulkHandler(
|
|||||||
) *BulkHandler {
|
) *BulkHandler {
|
||||||
handlersByPluralResourceName := make(map[schemas.ScimResourceTypePlural]RawResourceHandlerAdapter, len(handlers))
|
handlersByPluralResourceName := make(map[schemas.ScimResourceTypePlural]RawResourceHandlerAdapter, len(handlers))
|
||||||
for _, handler := range handlers {
|
for _, handler := range handlers {
|
||||||
handlersByPluralResourceName[handler.ResourceNamePlural()] = handler
|
handlersByPluralResourceName[handler.Schema().PluralName] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
return &BulkHandler{
|
return &BulkHandler{
|
||||||
@ -135,7 +135,7 @@ func (h *BulkHandler) processOperation(ctx context.Context, op *BulkRequestOpera
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resourceNamePlural != "" && resourceID != "" {
|
if resourceNamePlural != "" && resourceID != "" {
|
||||||
opResp.Location = buildLocation(ctx, resourceNamePlural, resourceID)
|
opResp.Location = schemas.BuildLocationForResource(ctx, resourceNamePlural, resourceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
opResp.Status = strconv.Itoa(statusCode)
|
opResp.Status = strconv.Itoa(statusCode)
|
||||||
|
@ -2,21 +2,17 @@ package resources
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/zitadel/zitadel/internal/api/http"
|
|
||||||
"github.com/zitadel/zitadel/internal/api/scim/resources/patch"
|
"github.com/zitadel/zitadel/internal/api/scim/resources/patch"
|
||||||
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceHandler[T ResourceHolder] interface {
|
type ResourceHandler[T ResourceHolder] interface {
|
||||||
SchemaType() schemas.ScimSchemaType
|
Schema() *schemas.ResourceSchema
|
||||||
ResourceNameSingular() schemas.ScimResourceTypeSingular
|
|
||||||
ResourceNamePlural() schemas.ScimResourceTypePlural
|
|
||||||
NewResource() T
|
NewResource() T
|
||||||
|
|
||||||
Create(ctx context.Context, resource T) (T, error)
|
Create(ctx context.Context, resource T) (T, error)
|
||||||
@ -27,48 +23,31 @@ type ResourceHandler[T ResourceHolder] interface {
|
|||||||
List(ctx context.Context, request *ListRequest) (*ListResponse[T], error)
|
List(ctx context.Context, request *ListRequest) (*ListResponse[T], error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Resource struct {
|
|
||||||
ID string `json:"-"`
|
|
||||||
Schemas []schemas.ScimSchemaType `json:"schemas"`
|
|
||||||
Meta *ResourceMeta `json:"meta"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceMeta struct {
|
|
||||||
ResourceType schemas.ScimResourceTypeSingular `json:"resourceType"`
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
LastModified time.Time `json:"lastModified"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Location string `json:"location"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceHolder interface {
|
type ResourceHolder interface {
|
||||||
SchemasHolder
|
SchemasHolder
|
||||||
GetResource() *Resource
|
GetResource() *schemas.Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
type SchemasHolder interface {
|
type SchemasHolder interface {
|
||||||
GetSchemas() []schemas.ScimSchemaType
|
GetSchemas() []schemas.ScimSchemaType
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildResource[T ResourceHolder](ctx context.Context, handler ResourceHandler[T], details *domain.ObjectDetails) *Resource {
|
func buildResource[T ResourceHolder](ctx context.Context, handler ResourceHandler[T], details *domain.ObjectDetails) *schemas.Resource {
|
||||||
created := details.CreationDate.UTC()
|
created := details.CreationDate.UTC()
|
||||||
if created.IsZero() {
|
if created.IsZero() {
|
||||||
created = details.EventDate.UTC()
|
created = details.EventDate.UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Resource{
|
schema := handler.Schema()
|
||||||
|
return &schemas.Resource{
|
||||||
ID: details.ID,
|
ID: details.ID,
|
||||||
Schemas: []schemas.ScimSchemaType{handler.SchemaType()},
|
Schemas: []schemas.ScimSchemaType{schema.ID},
|
||||||
Meta: &ResourceMeta{
|
Meta: &schemas.ResourceMeta{
|
||||||
ResourceType: handler.ResourceNameSingular(),
|
ResourceType: schema.Name,
|
||||||
Created: created,
|
Created: &created,
|
||||||
LastModified: details.EventDate.UTC(),
|
LastModified: gu.Ptr(details.EventDate.UTC()),
|
||||||
Version: strconv.FormatUint(details.Sequence, 10),
|
Version: strconv.FormatUint(details.Sequence, 10),
|
||||||
Location: buildLocation(ctx, handler.ResourceNamePlural(), details.ID),
|
Location: schemas.BuildLocationForResource(ctx, schema.PluralName, details.ID),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildLocation(ctx context.Context, resourceName schemas.ScimResourceTypePlural, id string) string {
|
|
||||||
return http.DomainContext(ctx).Origin() + path.Join(schemas.HandlerPrefix, authz.GetCtxData(ctx).OrgID, string(resourceName), id)
|
|
||||||
}
|
|
||||||
|
@ -19,7 +19,7 @@ import (
|
|||||||
|
|
||||||
// RawResourceHandlerAdapter adapts the ResourceHandler[T] without any generics
|
// RawResourceHandlerAdapter adapts the ResourceHandler[T] without any generics
|
||||||
type RawResourceHandlerAdapter interface {
|
type RawResourceHandlerAdapter interface {
|
||||||
ResourceNamePlural() schemas.ScimResourceTypePlural
|
Schema() *schemas.ResourceSchema
|
||||||
|
|
||||||
Create(ctx context.Context, data io.ReadCloser) (ResourceHolder, error)
|
Create(ctx context.Context, data io.ReadCloser) (ResourceHolder, error)
|
||||||
Replace(ctx context.Context, resourceID string, data io.ReadCloser) (ResourceHolder, error)
|
Replace(ctx context.Context, resourceID string, data io.ReadCloser) (ResourceHolder, error)
|
||||||
@ -37,8 +37,8 @@ func NewResourceHandlerAdapter[T ResourceHolder](handler ResourceHandler[T]) *Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (adapter *ResourceHandlerAdapter[T]) ResourceNamePlural() schemas.ScimResourceTypePlural {
|
func (adapter *ResourceHandlerAdapter[T]) Schema() *schemas.ResourceSchema {
|
||||||
return adapter.handler.ResourceNamePlural()
|
return adapter.handler.Schema()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (adapter *ResourceHandlerAdapter[T]) CreateFromHttp(r *http.Request) (ResourceHolder, error) {
|
func (adapter *ResourceHandlerAdapter[T]) CreateFromHttp(r *http.Request) (ResourceHolder, error) {
|
||||||
@ -112,7 +112,7 @@ func (adapter *ResourceHandlerAdapter[T]) GetFromHttp(r *http.Request) (T, error
|
|||||||
|
|
||||||
func (adapter *ResourceHandlerAdapter[T]) readEntity(data io.ReadCloser) (T, error) {
|
func (adapter *ResourceHandlerAdapter[T]) readEntity(data io.ReadCloser) (T, error) {
|
||||||
entity := adapter.handler.NewResource()
|
entity := adapter.handler.NewResource()
|
||||||
return entity, readSchema(data, entity, adapter.handler.SchemaType())
|
return entity, readSchema(data, entity, adapter.handler.Schema().ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readSchema(data io.ReadCloser, entity SchemasHolder, schema schemas.ScimSchemaType) error {
|
func readSchema(data io.ReadCloser, entity SchemasHolder, schema schemas.ScimSchemaType) error {
|
||||||
|
@ -28,7 +28,7 @@ type ListRequest struct {
|
|||||||
SortOrder ListRequestSortOrder `json:"sortOrder" schema:"sortOrder"`
|
SortOrder ListRequestSortOrder `json:"sortOrder" schema:"sortOrder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListResponse[T ResourceHolder] struct {
|
type ListResponse[T any] struct {
|
||||||
Schemas []schemas.ScimSchemaType `json:"schemas"`
|
Schemas []schemas.ScimSchemaType `json:"schemas"`
|
||||||
ItemsPerPage uint64 `json:"itemsPerPage"`
|
ItemsPerPage uint64 `json:"itemsPerPage"`
|
||||||
TotalResults uint64 `json:"totalResults"`
|
TotalResults uint64 `json:"totalResults"`
|
||||||
@ -43,7 +43,7 @@ const (
|
|||||||
ListRequestSortOrderDsc ListRequestSortOrder = "descending"
|
ListRequestSortOrderDsc ListRequestSortOrder = "descending"
|
||||||
|
|
||||||
defaultListCount = 100
|
defaultListCount = 100
|
||||||
maxListCount = 100
|
MaxListCount = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
var parser = zhttp.NewParser()
|
var parser = zhttp.NewParser()
|
||||||
@ -65,7 +65,7 @@ func (o ListRequestSortOrder) IsAscending() bool {
|
|||||||
return o == ListRequestSortOrderAsc
|
return o == ListRequestSortOrderAsc
|
||||||
}
|
}
|
||||||
|
|
||||||
func newListResponse[T ResourceHolder](totalResultCount uint64, q query.SearchRequest, resources []T) *ListResponse[T] {
|
func NewListResponse[T any](totalResultCount uint64, q query.SearchRequest, resources []T) *ListResponse[T] {
|
||||||
return &ListResponse[T]{
|
return &ListResponse[T]{
|
||||||
Schemas: []schemas.ScimSchemaType{schemas.IdListResponse},
|
Schemas: []schemas.ScimSchemaType{schemas.IdListResponse},
|
||||||
ItemsPerPage: q.Limit,
|
ItemsPerPage: q.Limit,
|
||||||
@ -137,8 +137,8 @@ func (r *ListRequest) validate() error {
|
|||||||
// according to the spec values < 0 are treated as 0
|
// according to the spec values < 0 are treated as 0
|
||||||
if r.Count < 0 {
|
if r.Count < 0 {
|
||||||
r.Count = 0
|
r.Count = 0
|
||||||
} else if r.Count > maxListCount {
|
} else if r.Count > MaxListCount {
|
||||||
return zerrors.ThrowInvalidArgumentf(nil, "SCIM-ucr", "Limit count exceeded, set a count <= %v", maxListCount)
|
return zerrors.ThrowInvalidArgumentf(nil, "SCIM-ucr", "Limit count exceeded, set a count <= %v", MaxListCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.SortOrder.isDefined() {
|
if !r.SortOrder.isDefined() {
|
||||||
|
@ -23,30 +23,31 @@ type UsersHandler struct {
|
|||||||
userCodeAlg crypto.EncryptionAlgorithm
|
userCodeAlg crypto.EncryptionAlgorithm
|
||||||
config *scim_config.Config
|
config *scim_config.Config
|
||||||
filterEvaluator *filter.Evaluator
|
filterEvaluator *filter.Evaluator
|
||||||
|
schema *scim_schemas.ResourceSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScimUser struct {
|
type ScimUser struct {
|
||||||
*Resource
|
*scim_schemas.Resource `scim:"ignoreInSchema"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id" scim:"ignoreInSchema"`
|
||||||
ExternalID string `json:"externalId,omitempty"`
|
ExternalID string `json:"externalId,omitempty"`
|
||||||
UserName string `json:"userName,omitempty"`
|
UserName string `json:"userName,omitempty" scim:"required,unique,caseInsensitive"`
|
||||||
Name *ScimUserName `json:"name,omitempty"`
|
Name *ScimUserName `json:"name,omitempty" scim:"required"`
|
||||||
DisplayName string `json:"displayName,omitempty"`
|
DisplayName string `json:"displayName,omitempty"`
|
||||||
NickName string `json:"nickName,omitempty"`
|
NickName string `json:"nickName,omitempty"`
|
||||||
ProfileUrl *scim_schemas.HttpURL `json:"profileUrl,omitempty"`
|
ProfileUrl *scim_schemas.HttpURL `json:"profileUrl,omitempty"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
|
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
|
||||||
Locale string `json:"locale,omitempty"`
|
Locale string `json:"locale,omitempty"`
|
||||||
Timezone string `json:"timezone,omitempty"`
|
Timezone string `json:"timezone,omitempty"`
|
||||||
Active *bool `json:"active,omitempty"`
|
Active *bool `json:"active,omitempty"`
|
||||||
Emails []*ScimEmail `json:"emails,omitempty"`
|
Emails []*ScimEmail `json:"emails,omitempty" scim:"required"`
|
||||||
PhoneNumbers []*ScimPhoneNumber `json:"phoneNumbers,omitempty"`
|
PhoneNumbers []*ScimPhoneNumber `json:"phoneNumbers,omitempty"`
|
||||||
Password *scim_schemas.WriteOnlyString `json:"password,omitempty"`
|
Password *scim_schemas.WriteOnlyString `json:"password,omitempty"`
|
||||||
Ims []*ScimIms `json:"ims,omitempty"`
|
Ims []*ScimIms `json:"ims,omitempty"`
|
||||||
Addresses []*ScimAddress `json:"addresses,omitempty"`
|
Addresses []*ScimAddress `json:"addresses,omitempty"`
|
||||||
Photos []*ScimPhoto `json:"photos,omitempty"`
|
Photos []*ScimPhoto `json:"photos,omitempty"`
|
||||||
Entitlements []*ScimEntitlement `json:"entitlements,omitempty"`
|
Entitlements []*ScimEntitlement `json:"entitlements,omitempty"`
|
||||||
Roles []*ScimRole `json:"roles,omitempty"`
|
Roles []*ScimRole `json:"roles,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScimEntitlement struct {
|
type ScimEntitlement struct {
|
||||||
@ -87,7 +88,7 @@ type ScimIms struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ScimEmail struct {
|
type ScimEmail struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value" scim:"required"`
|
||||||
Primary bool `json:"primary"`
|
Primary bool `json:"primary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,8 +99,8 @@ type ScimPhoneNumber struct {
|
|||||||
|
|
||||||
type ScimUserName struct {
|
type ScimUserName struct {
|
||||||
Formatted string `json:"formatted,omitempty"`
|
Formatted string `json:"formatted,omitempty"`
|
||||||
FamilyName string `json:"familyName,omitempty"`
|
FamilyName string `json:"familyName,omitempty" scim:"required"`
|
||||||
GivenName string `json:"givenName,omitempty"`
|
GivenName string `json:"givenName,omitempty" scim:"required"`
|
||||||
MiddleName string `json:"middleName,omitempty"`
|
MiddleName string `json:"middleName,omitempty"`
|
||||||
HonorificPrefix string `json:"honorificPrefix,omitempty"`
|
HonorificPrefix string `json:"honorificPrefix,omitempty"`
|
||||||
HonorificSuffix string `json:"honorificSuffix,omitempty"`
|
HonorificSuffix string `json:"honorificSuffix,omitempty"`
|
||||||
@ -110,20 +111,26 @@ func NewUsersHandler(
|
|||||||
query *query.Queries,
|
query *query.Queries,
|
||||||
userCodeAlg crypto.EncryptionAlgorithm,
|
userCodeAlg crypto.EncryptionAlgorithm,
|
||||||
config *scim_config.Config) ResourceHandler[*ScimUser] {
|
config *scim_config.Config) ResourceHandler[*ScimUser] {
|
||||||
return &UsersHandler{command, query, userCodeAlg, config, filter.NewEvaluator(scim_schemas.IdUser)}
|
return &UsersHandler{
|
||||||
|
command,
|
||||||
|
query,
|
||||||
|
userCodeAlg,
|
||||||
|
config,
|
||||||
|
filter.NewEvaluator(scim_schemas.IdUser),
|
||||||
|
scim_schemas.BuildSchema(scim_schemas.SchemaBuilderArgs{
|
||||||
|
ID: scim_schemas.IdUser,
|
||||||
|
Name: scim_schemas.UserResourceType,
|
||||||
|
EndpointName: scim_schemas.UsersResourceType,
|
||||||
|
Description: "User Account",
|
||||||
|
Resource: new(ScimUser),
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) ResourceNameSingular() scim_schemas.ScimResourceTypeSingular {
|
func (u *ScimUser) GetResource() *scim_schemas.Resource {
|
||||||
return scim_schemas.UserResourceType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UsersHandler) ResourceNamePlural() scim_schemas.ScimResourceTypePlural {
|
|
||||||
return scim_schemas.UsersResourceType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ScimUser) GetResource() *Resource {
|
|
||||||
return u.Resource
|
return u.Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ScimUser) GetSchemas() []scim_schemas.ScimSchemaType {
|
func (u *ScimUser) GetSchemas() []scim_schemas.ScimSchemaType {
|
||||||
if u.Resource == nil {
|
if u.Resource == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -132,12 +139,12 @@ func (u *ScimUser) GetSchemas() []scim_schemas.ScimSchemaType {
|
|||||||
return u.Resource.Schemas
|
return u.Resource.Schemas
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) NewResource() *ScimUser {
|
func (h *UsersHandler) Schema() *scim_schemas.ResourceSchema {
|
||||||
return new(ScimUser)
|
return h.schema
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) SchemaType() scim_schemas.ScimSchemaType {
|
func (h *UsersHandler) NewResource() *ScimUser {
|
||||||
return scim_schemas.IdUser
|
return new(ScimUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) Create(ctx context.Context, user *ScimUser) (*ScimUser, error) {
|
func (h *UsersHandler) Create(ctx context.Context, user *ScimUser) (*ScimUser, error) {
|
||||||
@ -226,7 +233,7 @@ func (h *UsersHandler) List(ctx context.Context, request *ListRequest) (*ListRes
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newListResponse(count, q.SearchRequest, make([]*ScimUser, 0)), nil
|
return NewListResponse(count, q.SearchRequest, make([]*ScimUser, 0)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := h.query.SearchUsers(ctx, q, nil)
|
users, err := h.query.SearchUsers(ctx, q, nil)
|
||||||
@ -240,7 +247,7 @@ func (h *UsersHandler) List(ctx context.Context, request *ListRequest) (*ListRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
scimUsers := h.mapToScimUsers(ctx, users.Users, metadata)
|
scimUsers := h.mapToScimUsers(ctx, users.Users, metadata)
|
||||||
return newListResponse(users.SearchResponse.Count, q.SearchRequest, scimUsers), nil
|
return NewListResponse(users.SearchResponse.Count, q.SearchRequest, scimUsers), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
||||||
|
@ -382,29 +382,29 @@ func (h *UsersHandler) mapAndValidateMetadata(ctx context.Context, user *ScimUse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) buildResourceForQuery(ctx context.Context, user *query.User) *Resource {
|
func (h *UsersHandler) buildResourceForQuery(ctx context.Context, user *query.User) *schemas.Resource {
|
||||||
return &Resource{
|
return &schemas.Resource{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Schemas: []schemas.ScimSchemaType{schemas.IdUser},
|
Schemas: []schemas.ScimSchemaType{schemas.IdUser},
|
||||||
Meta: &ResourceMeta{
|
Meta: &schemas.ResourceMeta{
|
||||||
ResourceType: schemas.UserResourceType,
|
ResourceType: schemas.UserResourceType,
|
||||||
Created: user.CreationDate.UTC(),
|
Created: gu.Ptr(user.CreationDate.UTC()),
|
||||||
LastModified: user.ChangeDate.UTC(),
|
LastModified: gu.Ptr(user.ChangeDate.UTC()),
|
||||||
Version: strconv.FormatUint(user.Sequence, 10),
|
Version: strconv.FormatUint(user.Sequence, 10),
|
||||||
Location: buildLocation(ctx, h.ResourceNamePlural(), user.ID),
|
Location: schemas.BuildLocationForResource(ctx, h.schema.PluralName, user.ID),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) buildResourceForWriteModel(ctx context.Context, user *command.UserV2WriteModel) *Resource {
|
func (h *UsersHandler) buildResourceForWriteModel(ctx context.Context, user *command.UserV2WriteModel) *schemas.Resource {
|
||||||
return &Resource{
|
return &schemas.Resource{
|
||||||
Schemas: []schemas.ScimSchemaType{schemas.IdUser},
|
Schemas: []schemas.ScimSchemaType{schemas.IdUser},
|
||||||
Meta: &ResourceMeta{
|
Meta: &schemas.ResourceMeta{
|
||||||
ResourceType: schemas.UserResourceType,
|
ResourceType: schemas.UserResourceType,
|
||||||
Created: user.CreationDate.UTC(),
|
Created: gu.Ptr(user.CreationDate.UTC()),
|
||||||
LastModified: user.ChangeDate.UTC(),
|
LastModified: gu.Ptr(user.ChangeDate.UTC()),
|
||||||
Version: strconv.FormatUint(user.ProcessedSequence, 10),
|
Version: strconv.FormatUint(user.ProcessedSequence, 10),
|
||||||
Location: buildLocation(ctx, h.ResourceNamePlural(), user.AggregateID),
|
Location: schemas.BuildLocationForResource(ctx, h.schema.PluralName, user.AggregateID),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ func (h *UsersHandler) buildListQuery(ctx context.Context, request *ListRequest)
|
|||||||
return q, nil
|
return q, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
filterQuery, err := request.Filter.BuildQuery(ctx, h.SchemaType(), fieldPathColumnMapping)
|
filterQuery, err := request.Filter.BuildQuery(ctx, h.schema.ID, fieldPathColumnMapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
183
internal/api/scim/schemas/schema_builder.go
Normal file
183
internal/api/scim/schemas/schema_builder.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package schemas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SchemaBuilderArgs struct {
|
||||||
|
ID ScimSchemaType
|
||||||
|
Name ScimResourceTypeSingular
|
||||||
|
EndpointName ScimResourceTypePlural
|
||||||
|
Description string
|
||||||
|
Resource any
|
||||||
|
}
|
||||||
|
|
||||||
|
type fieldSchemaInfo struct {
|
||||||
|
Ignore bool
|
||||||
|
Required bool
|
||||||
|
CaseExact bool
|
||||||
|
Unique bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
timeType = reflect.TypeOf(time.Time{})
|
||||||
|
languageTagType = reflect.TypeOf(language.Tag{})
|
||||||
|
httpURLType = reflect.TypeOf(HttpURL{})
|
||||||
|
writeOnlyStringType = reflect.TypeOf(WriteOnlyString(""))
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildSchema(args SchemaBuilderArgs) *ResourceSchema {
|
||||||
|
return &ResourceSchema{
|
||||||
|
Resource: &Resource{
|
||||||
|
Schemas: []ScimSchemaType{IdSchema},
|
||||||
|
ID: string(args.ID),
|
||||||
|
Meta: &ResourceMeta{
|
||||||
|
ResourceType: SchemaResourceType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID: args.ID,
|
||||||
|
Name: args.Name,
|
||||||
|
PluralName: args.EndpointName,
|
||||||
|
Description: args.Description,
|
||||||
|
Attributes: buildSchemaAttributes(reflect.TypeOf(args.Resource)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSchemaAttributes(fieldType reflect.Type) []*SchemaAttribute {
|
||||||
|
if fieldType.Kind() == reflect.Ptr {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldType.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes := make([]*SchemaAttribute, 0, fieldType.NumField())
|
||||||
|
for i := 0; i < fieldType.NumField(); i++ {
|
||||||
|
field := fieldType.Field(i)
|
||||||
|
attribute := buildAttribute(field)
|
||||||
|
|
||||||
|
if attribute != nil {
|
||||||
|
attributes = append(attributes, attribute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAttribute(field reflect.StructField) *SchemaAttribute {
|
||||||
|
info := getFieldSchemaInfo(field)
|
||||||
|
if info.Ignore {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldType := getFieldType(field)
|
||||||
|
attribute := &SchemaAttribute{
|
||||||
|
Name: getFieldJsonName(field),
|
||||||
|
Description: "For details see RFC7643",
|
||||||
|
Type: getFieldAttributeType(fieldType),
|
||||||
|
MultiValued: isFieldMultiValued(field),
|
||||||
|
Required: info.Required,
|
||||||
|
CaseExact: info.CaseExact,
|
||||||
|
Mutability: SchemaAttributeMutabilityReadWrite,
|
||||||
|
Returned: SchemaAttributeReturnedAlways,
|
||||||
|
Uniqueness: SchemaAttributeUniquenessNone,
|
||||||
|
}
|
||||||
|
|
||||||
|
if attribute.Type == SchemaAttributeTypeComplex {
|
||||||
|
attribute.SubAttributes = buildSchemaAttributes(fieldType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldType == writeOnlyStringType {
|
||||||
|
attribute.Returned = SchemaAttributeReturnedNever
|
||||||
|
attribute.Mutability = SchemaAttributeMutabilityWriteOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Unique {
|
||||||
|
attribute.Uniqueness = SchemaAttributeUniquenessServer
|
||||||
|
}
|
||||||
|
|
||||||
|
return attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFieldMultiValued(field reflect.StructField) bool {
|
||||||
|
if field.Type.Kind() != reflect.Ptr {
|
||||||
|
return field.Type.Kind() == reflect.Slice
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.Type.Elem().Kind() == reflect.Slice
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldAttributeType(fieldType reflect.Type) SchemaAttributeType {
|
||||||
|
switch fieldType.Kind() { //nolint:exhaustive
|
||||||
|
case reflect.String:
|
||||||
|
return SchemaAttributeTypeString
|
||||||
|
case reflect.Bool:
|
||||||
|
return SchemaAttributeTypeBoolean
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return SchemaAttributeTypeInteger
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return SchemaAttributeTypeDecimal
|
||||||
|
case reflect.Struct:
|
||||||
|
switch fieldType {
|
||||||
|
case timeType:
|
||||||
|
return SchemaAttributeTypeDateTime
|
||||||
|
case writeOnlyStringType, languageTagType, httpURLType:
|
||||||
|
return SchemaAttributeTypeString
|
||||||
|
default:
|
||||||
|
return SchemaAttributeTypeComplex
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported field type: %v", fieldType.Kind()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldType(field reflect.StructField) reflect.Type {
|
||||||
|
fieldType := field.Type
|
||||||
|
if fieldType.Kind() == reflect.Ptr {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldType.Kind() == reflect.Slice || fieldType.Kind() == reflect.Array {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
|
||||||
|
if fieldType.Kind() == reflect.Ptr {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fieldType
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldSchemaInfo(field reflect.StructField) *fieldSchemaInfo {
|
||||||
|
tag := field.Tag.Get("scim")
|
||||||
|
tagOptions := strings.Split(tag, ",")
|
||||||
|
return &fieldSchemaInfo{
|
||||||
|
Ignore: slices.Contains(tagOptions, "ignoreInSchema"),
|
||||||
|
Required: slices.Contains(tagOptions, "required"),
|
||||||
|
CaseExact: !slices.Contains(tagOptions, "caseInsensitive"),
|
||||||
|
Unique: slices.Contains(tagOptions, "unique"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldJsonName(field reflect.StructField) string {
|
||||||
|
jsonTag := field.Tag.Get("json")
|
||||||
|
|
||||||
|
// Skip fields explicitly excluded
|
||||||
|
if jsonTag == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// use field name as default
|
||||||
|
if jsonTag == "" {
|
||||||
|
return field.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip other options such as omitempty
|
||||||
|
return strings.Split(jsonTag, ",")[0]
|
||||||
|
}
|
@ -1,5 +1,14 @@
|
|||||||
package schemas
|
package schemas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/http"
|
||||||
|
)
|
||||||
|
|
||||||
type ScimSchemaType string
|
type ScimSchemaType string
|
||||||
type ScimResourceTypeSingular string
|
type ScimResourceTypeSingular string
|
||||||
type ScimResourceTypePlural string
|
type ScimResourceTypePlural string
|
||||||
@ -9,17 +18,147 @@ const (
|
|||||||
idPrefixCore = "urn:ietf:params:scim:schemas:core:2.0:"
|
idPrefixCore = "urn:ietf:params:scim:schemas:core:2.0:"
|
||||||
idPrefixZitadelMessages = "urn:ietf:params:scim:api:zitadel:messages:2.0:"
|
idPrefixZitadelMessages = "urn:ietf:params:scim:api:zitadel:messages:2.0:"
|
||||||
|
|
||||||
IdUser ScimSchemaType = idPrefixCore + "User"
|
IdUser ScimSchemaType = idPrefixCore + "User"
|
||||||
IdListResponse ScimSchemaType = idPrefixMessages + "ListResponse"
|
IdServiceProviderConfig ScimSchemaType = idPrefixCore + "ServiceProviderConfig"
|
||||||
IdPatchOperation ScimSchemaType = idPrefixMessages + "PatchOp"
|
IdResourceType ScimSchemaType = idPrefixCore + "ResourceType"
|
||||||
IdSearchRequest ScimSchemaType = idPrefixMessages + "SearchRequest"
|
IdSchema ScimSchemaType = idPrefixCore + "Schema"
|
||||||
IdBulkRequest ScimSchemaType = idPrefixMessages + "BulkRequest"
|
IdListResponse ScimSchemaType = idPrefixMessages + "ListResponse"
|
||||||
IdBulkResponse ScimSchemaType = idPrefixMessages + "BulkResponse"
|
IdPatchOperation ScimSchemaType = idPrefixMessages + "PatchOp"
|
||||||
IdError ScimSchemaType = idPrefixMessages + "Error"
|
IdSearchRequest ScimSchemaType = idPrefixMessages + "SearchRequest"
|
||||||
IdZitadelErrorDetail ScimSchemaType = idPrefixZitadelMessages + "ErrorDetail"
|
IdBulkRequest ScimSchemaType = idPrefixMessages + "BulkRequest"
|
||||||
|
IdBulkResponse ScimSchemaType = idPrefixMessages + "BulkResponse"
|
||||||
|
IdError ScimSchemaType = idPrefixMessages + "Error"
|
||||||
|
IdZitadelErrorDetail ScimSchemaType = idPrefixZitadelMessages + "ErrorDetail"
|
||||||
|
|
||||||
UserResourceType ScimResourceTypeSingular = "User"
|
UserResourceType ScimResourceTypeSingular = "User"
|
||||||
UsersResourceType ScimResourceTypePlural = "Users"
|
UsersResourceType ScimResourceTypePlural = "Users"
|
||||||
|
|
||||||
|
ServiceProviderConfigResourceType ScimResourceTypeSingular = "ServiceProviderConfig"
|
||||||
|
ServiceProviderConfigsResourceType ScimResourceTypePlural = "ServiceProviderConfig"
|
||||||
|
|
||||||
|
SchemaResourceType ScimResourceTypeSingular = "Schema"
|
||||||
|
SchemasResourceType ScimResourceTypePlural = "Schemas"
|
||||||
|
|
||||||
|
ResourceTypesResourceType ScimResourceTypePlural = "ResourceTypes"
|
||||||
|
|
||||||
HandlerPrefix = "/scim/v2"
|
HandlerPrefix = "/scim/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Resource struct {
|
||||||
|
ID string `json:"-"`
|
||||||
|
Schemas []ScimSchemaType `json:"schemas"`
|
||||||
|
Meta *ResourceMeta `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceMeta struct {
|
||||||
|
ResourceType ScimResourceTypeSingular `json:"resourceType"`
|
||||||
|
Created *time.Time `json:"created,omitempty"`
|
||||||
|
LastModified *time.Time `json:"lastModified,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceType struct {
|
||||||
|
*Resource
|
||||||
|
ID ScimResourceTypeSingular `json:"id"`
|
||||||
|
Name ScimResourceTypeSingular `json:"name"`
|
||||||
|
Endpoint ScimResourceTypePlural `json:"endpoint"`
|
||||||
|
Schema ScimSchemaType `json:"schema"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceSchema struct {
|
||||||
|
*Resource
|
||||||
|
ID ScimSchemaType `json:"id"`
|
||||||
|
Name ScimResourceTypeSingular `json:"name"`
|
||||||
|
PluralName ScimResourceTypePlural `json:"-"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Attributes []*SchemaAttribute `json:"attributes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SchemaAttribute struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Type SchemaAttributeType `json:"type"`
|
||||||
|
SubAttributes []*SchemaAttribute `json:"subAttributes,omitempty"`
|
||||||
|
MultiValued bool `json:"multiValued"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
CaseExact bool `json:"caseExact"`
|
||||||
|
Mutability SchemaAttributeMutability `json:"mutability"`
|
||||||
|
Returned SchemaAttributeReturned `json:"returned"`
|
||||||
|
Uniqueness SchemaAttributeUniqueness `json:"uniqueness"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SchemaAttributeType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SchemaAttributeTypeString SchemaAttributeType = "string"
|
||||||
|
SchemaAttributeTypeBoolean SchemaAttributeType = "boolean"
|
||||||
|
SchemaAttributeTypeDecimal SchemaAttributeType = "decimal"
|
||||||
|
SchemaAttributeTypeInteger SchemaAttributeType = "integer"
|
||||||
|
SchemaAttributeTypeDateTime SchemaAttributeType = "dateTime"
|
||||||
|
SchemaAttributeTypeComplex SchemaAttributeType = "complex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SchemaAttributeMutability string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SchemaAttributeMutabilityReadWrite SchemaAttributeMutability = "readWrite"
|
||||||
|
SchemaAttributeMutabilityWriteOnly SchemaAttributeMutability = "writeOnly"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SchemaAttributeReturned string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SchemaAttributeReturnedAlways SchemaAttributeReturned = "always"
|
||||||
|
SchemaAttributeReturnedNever SchemaAttributeReturned = "never"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SchemaAttributeUniqueness string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SchemaAttributeUniquenessNone SchemaAttributeUniqueness = "none"
|
||||||
|
SchemaAttributeUniquenessServer SchemaAttributeUniqueness = "server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *ResourceType) GetSchemas() []ScimSchemaType {
|
||||||
|
return s.Resource.Schemas
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResourceType) GetResource() *Resource {
|
||||||
|
return s.Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResourceSchema) GetSchemas() []ScimSchemaType {
|
||||||
|
return s.Resource.Schemas
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResourceSchema) GetResource() *Resource {
|
||||||
|
return s.Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResourceSchema) ToResourceType(ctx context.Context, orgID string) *ResourceType {
|
||||||
|
return &ResourceType{
|
||||||
|
Resource: &Resource{
|
||||||
|
Schemas: []ScimSchemaType{IdResourceType},
|
||||||
|
ID: string(s.Name),
|
||||||
|
Meta: &ResourceMeta{
|
||||||
|
ResourceType: s.Name,
|
||||||
|
Location: BuildLocationWithOrg(ctx, orgID, ResourceTypesResourceType, string(s.Name)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID: s.Name,
|
||||||
|
Name: s.Name,
|
||||||
|
Endpoint: s.PluralName,
|
||||||
|
Schema: s.ID,
|
||||||
|
Description: s.Description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildLocationForResource(ctx context.Context, resourceName ScimResourceTypePlural, id string) string {
|
||||||
|
return BuildLocationWithOrg(ctx, authz.GetCtxData(ctx).OrgID, resourceName, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildLocationWithOrg(ctx context.Context, orgID string, resourceName ScimResourceTypePlural, id string) string {
|
||||||
|
return http.DomainContext(ctx).Origin() + path.Join(HandlerPrefix, orgID, string(resourceName), id)
|
||||||
|
}
|
||||||
|
@ -7,11 +7,6 @@ import "encoding/json"
|
|||||||
// This increases security to really ensure this is never sent to a client.
|
// This increases security to really ensure this is never sent to a client.
|
||||||
type WriteOnlyString string
|
type WriteOnlyString string
|
||||||
|
|
||||||
func NewWriteOnlyString(s string) *WriteOnlyString {
|
|
||||||
wos := WriteOnlyString(s)
|
|
||||||
return &wos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *WriteOnlyString) MarshalJSON() ([]byte, error) {
|
func (s *WriteOnlyString) MarshalJSON() ([]byte, error) {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,13 @@ func buildHandler(
|
|||||||
bulkHandler := sresources.NewBulkHandler(cfg.Bulk, usersHandler)
|
bulkHandler := sresources.NewBulkHandler(cfg.Bulk, usersHandler)
|
||||||
router.Handle("/"+zhttp.OrgIdInPathVariable+"/Bulk", middleware(handleJsonResponse(bulkHandler.BulkFromHttp))).Methods(http.MethodPost)
|
router.Handle("/"+zhttp.OrgIdInPathVariable+"/Bulk", middleware(handleJsonResponse(bulkHandler.BulkFromHttp))).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
serviceProviderHandler := newServiceProviderHandler(cfg, usersHandler)
|
||||||
|
router.Handle("/"+zhttp.OrgIdInPathVariable+"/ServiceProviderConfig", middleware(handleJsonResponse(serviceProviderHandler.GetConfig))).Methods(http.MethodGet)
|
||||||
|
router.Handle("/"+zhttp.OrgIdInPathVariable+"/ResourceTypes", middleware(handleJsonResponse(serviceProviderHandler.ListResourceTypes))).Methods(http.MethodGet)
|
||||||
|
router.Handle("/"+zhttp.OrgIdInPathVariable+"/ResourceTypes/{name}", middleware(handleResourceResponse(serviceProviderHandler.GetResourceType))).Methods(http.MethodGet)
|
||||||
|
router.Handle("/"+zhttp.OrgIdInPathVariable+"/Schemas", middleware(handleJsonResponse(serviceProviderHandler.ListSchemas))).Methods(http.MethodGet)
|
||||||
|
router.Handle("/"+zhttp.OrgIdInPathVariable+"/Schemas/{id}", middleware(handleResourceResponse(serviceProviderHandler.GetSchema))).Methods(http.MethodGet)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +71,7 @@ func buildMiddleware(cfg *sconfig.Config, query *query.Queries, middlewares []zh
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mapResource[T sresources.ResourceHolder](router *mux.Router, mw zhttp_middlware.ErrorHandlerFunc, adapter *sresources.ResourceHandlerAdapter[T]) {
|
func mapResource[T sresources.ResourceHolder](router *mux.Router, mw zhttp_middlware.ErrorHandlerFunc, adapter *sresources.ResourceHandlerAdapter[T]) {
|
||||||
resourceRouter := router.PathPrefix("/" + path.Join(zhttp.OrgIdInPathVariable, string(adapter.ResourceNamePlural()))).Subrouter()
|
resourceRouter := router.PathPrefix("/" + path.Join(zhttp.OrgIdInPathVariable, string(adapter.Schema().PluralName))).Subrouter()
|
||||||
|
|
||||||
resourceRouter.Handle("", mw(handleResourceCreatedResponse(adapter.CreateFromHttp))).Methods(http.MethodPost)
|
resourceRouter.Handle("", mw(handleResourceCreatedResponse(adapter.CreateFromHttp))).Methods(http.MethodPost)
|
||||||
resourceRouter.Handle("", mw(handleJsonResponse(adapter.ListFromHttp))).Methods(http.MethodGet)
|
resourceRouter.Handle("", mw(handleJsonResponse(adapter.ListFromHttp))).Methods(http.MethodGet)
|
||||||
|
182
internal/api/scim/service_provider.go
Normal file
182
internal/api/scim/service_provider.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package scim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
zhttp "github.com/zitadel/zitadel/internal/api/http"
|
||||||
|
scim_config "github.com/zitadel/zitadel/internal/api/scim/config"
|
||||||
|
sresources "github.com/zitadel/zitadel/internal/api/scim/resources"
|
||||||
|
sschemas "github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serviceProviderHandler struct {
|
||||||
|
config *scim_config.Config
|
||||||
|
schemas []*sschemas.ResourceSchema
|
||||||
|
schemasByID map[sschemas.ScimSchemaType]*sschemas.ResourceSchema
|
||||||
|
schemasByResourceName map[sschemas.ScimResourceTypeSingular]*sschemas.ResourceSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceProviderConfig struct {
|
||||||
|
*sschemas.Resource
|
||||||
|
DocumentationUri string `json:"documentationUri"`
|
||||||
|
Patch serviceProviderConfigSupported `json:"patch"`
|
||||||
|
Bulk serviceProviderConfigBulk `json:"bulk"`
|
||||||
|
Filter serviceProviderFilterSupported `json:"filter"`
|
||||||
|
ChangePassword serviceProviderConfigSupported `json:"changePassword"`
|
||||||
|
Sort serviceProviderConfigSupported `json:"sort"`
|
||||||
|
ETag serviceProviderConfigSupported `json:"etag"`
|
||||||
|
AuthenticationSchemes []*scim_config.ServiceProviderConfigAuthenticationScheme `json:"authenticationSchemes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceProviderConfigSupported struct {
|
||||||
|
Supported bool `json:"supported"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceProviderFilterSupported struct {
|
||||||
|
Supported bool `json:"supported"`
|
||||||
|
MaxResults int `json:"maxResults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceProviderConfigBulk struct {
|
||||||
|
Supported bool `json:"supported"`
|
||||||
|
MaxOperations int `json:"maxOperations"`
|
||||||
|
MaxPayloadSize int64 `json:"maxPayloadSize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultConfigSearchRequest = query.SearchRequest{
|
||||||
|
Offset: 0,
|
||||||
|
Limit: 100,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newServiceProviderHandler(cfg *scim_config.Config, handlers ...sresources.RawResourceHandlerAdapter) *serviceProviderHandler {
|
||||||
|
schemas := make([]*sschemas.ResourceSchema, len(handlers))
|
||||||
|
schemasByID := make(map[sschemas.ScimSchemaType]*sschemas.ResourceSchema, len(handlers))
|
||||||
|
schemasByResourceName := make(map[sschemas.ScimResourceTypeSingular]*sschemas.ResourceSchema, len(handlers))
|
||||||
|
for i, handler := range handlers {
|
||||||
|
schema := handler.Schema()
|
||||||
|
schemas[i] = schema
|
||||||
|
schemasByID[schema.ID] = schema
|
||||||
|
schemasByResourceName[schema.Name] = schema
|
||||||
|
}
|
||||||
|
|
||||||
|
return &serviceProviderHandler{
|
||||||
|
config: cfg,
|
||||||
|
schemas: schemas,
|
||||||
|
schemasByID: schemasByID,
|
||||||
|
schemasByResourceName: schemasByResourceName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *serviceProviderHandler) GetConfig(r *http.Request) (*serviceProviderConfig, error) {
|
||||||
|
// the request is unauthenticated, read the orgID from the url instead of the context
|
||||||
|
orgID := mux.Vars(r)[zhttp.OrgIdInPathVariableName]
|
||||||
|
return &serviceProviderConfig{
|
||||||
|
Resource: &sschemas.Resource{
|
||||||
|
Schemas: []sschemas.ScimSchemaType{sschemas.IdServiceProviderConfig},
|
||||||
|
Meta: &sschemas.ResourceMeta{
|
||||||
|
ResourceType: sschemas.ServiceProviderConfigResourceType,
|
||||||
|
Location: sschemas.BuildLocationWithOrg(r.Context(), orgID, sschemas.ServiceProviderConfigsResourceType, ""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DocumentationUri: h.config.DocumentationUrl,
|
||||||
|
Patch: serviceProviderConfigSupported{
|
||||||
|
Supported: true,
|
||||||
|
},
|
||||||
|
Bulk: serviceProviderConfigBulk{
|
||||||
|
Supported: true,
|
||||||
|
MaxOperations: h.config.Bulk.MaxOperationsCount,
|
||||||
|
MaxPayloadSize: h.config.MaxRequestBodySize,
|
||||||
|
},
|
||||||
|
Filter: serviceProviderFilterSupported{
|
||||||
|
Supported: true,
|
||||||
|
MaxResults: sresources.MaxListCount,
|
||||||
|
},
|
||||||
|
ChangePassword: serviceProviderConfigSupported{
|
||||||
|
Supported: true,
|
||||||
|
},
|
||||||
|
Sort: serviceProviderConfigSupported{
|
||||||
|
Supported: true,
|
||||||
|
},
|
||||||
|
ETag: serviceProviderConfigSupported{
|
||||||
|
Supported: false,
|
||||||
|
},
|
||||||
|
AuthenticationSchemes: h.config.AuthenticationSchemes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *serviceProviderHandler) ListResourceTypes(r *http.Request) (*sresources.ListResponse[*sschemas.ResourceType], error) {
|
||||||
|
// the request is unauthenticated, read the orgID from the url instead of the context
|
||||||
|
ctx := r.Context()
|
||||||
|
orgID := mux.Vars(r)[zhttp.OrgIdInPathVariableName]
|
||||||
|
|
||||||
|
resourceTypes := make([]*sschemas.ResourceType, len(h.schemas))
|
||||||
|
for i, schema := range h.schemas {
|
||||||
|
resourceTypes[i] = schema.ToResourceType(ctx, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sresources.NewListResponse(uint64(len(resourceTypes)), defaultConfigSearchRequest, resourceTypes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *serviceProviderHandler) GetResourceType(r *http.Request) (*sschemas.ResourceType, error) {
|
||||||
|
// the request is unauthenticated, read the orgID from the url instead of the context
|
||||||
|
ctx := r.Context()
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
orgID := vars[zhttp.OrgIdInPathVariableName]
|
||||||
|
name := sschemas.ScimResourceTypeSingular(vars["name"])
|
||||||
|
|
||||||
|
schema, ok := h.schemasByResourceName[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, zerrors.ThrowNotFoundf(nil, "SCIMSP-148z", "Scim resource type %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.ToResourceType(ctx, orgID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *serviceProviderHandler) ListSchemas(r *http.Request) (*sresources.ListResponse[*sschemas.ResourceSchema], error) {
|
||||||
|
// the request is unauthenticated, read the orgID from the url instead of the context
|
||||||
|
ctx := r.Context()
|
||||||
|
orgID := mux.Vars(r)[zhttp.OrgIdInPathVariableName]
|
||||||
|
|
||||||
|
schemas := make([]*sschemas.ResourceSchema, len(h.schemas))
|
||||||
|
for i, schema := range h.schemas {
|
||||||
|
schemas[i] = buildSchema(ctx, orgID, schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sresources.NewListResponse(uint64(len(h.schemas)), defaultConfigSearchRequest, schemas), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *serviceProviderHandler) GetSchema(r *http.Request) (*sschemas.ResourceSchema, error) {
|
||||||
|
// the request is unauthenticated, read the orgID from the url instead of the context
|
||||||
|
ctx := r.Context()
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
orgID := vars[zhttp.OrgIdInPathVariableName]
|
||||||
|
id := sschemas.ScimSchemaType(vars["id"])
|
||||||
|
|
||||||
|
schema, ok := h.schemasByID[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, zerrors.ThrowNotFoundf(nil, "SCIMSP-148y", "Scim schema %s not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildSchema(ctx, orgID, schema), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSchema shallow copies the provided schema and sets the correct location based on the provided context information.
|
||||||
|
func buildSchema(ctx context.Context, orgID string, schema *sschemas.ResourceSchema) *sschemas.ResourceSchema {
|
||||||
|
newSchema := *schema
|
||||||
|
newSchema.Resource = &sschemas.Resource{
|
||||||
|
ID: schema.Resource.ID,
|
||||||
|
Schemas: schema.Resource.Schemas,
|
||||||
|
Meta: &sschemas.ResourceMeta{
|
||||||
|
ResourceType: schema.Resource.Meta.ResourceType,
|
||||||
|
Location: sschemas.BuildLocationWithOrg(ctx, orgID, sschemas.SchemasResourceType, string(schema.ID)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &newSchema
|
||||||
|
}
|
@ -125,6 +125,26 @@ func NewScimClient(target string) *Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetServiceProviderConfig(ctx context.Context, orgID string) ([]byte, error) {
|
||||||
|
return c.getWithRawResponse(ctx, orgID, "/ServiceProviderConfig")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetSchemas(ctx context.Context, orgID string) ([]byte, error) {
|
||||||
|
return c.getWithRawResponse(ctx, orgID, "/Schemas")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetSchema(ctx context.Context, orgID, schemaID string) ([]byte, error) {
|
||||||
|
return c.getWithRawResponse(ctx, orgID, "/Schemas/"+schemaID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetResourceTypes(ctx context.Context, orgID string) ([]byte, error) {
|
||||||
|
return c.getWithRawResponse(ctx, orgID, "/ResourceTypes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetResourceType(ctx context.Context, orgID, name string) ([]byte, error) {
|
||||||
|
return c.getWithRawResponse(ctx, orgID, "/ResourceTypes/"+name)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Bulk(ctx context.Context, orgID string, body []byte) (*BulkResponse, error) {
|
func (c *Client) Bulk(ctx context.Context, orgID string, body []byte) (*BulkResponse, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/"+orgID+"/Bulk", bytes.NewReader(body))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/"+orgID+"/Bulk", bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -233,6 +253,29 @@ func (c *ResourceClient[T]) doWithBody(ctx context.Context, method, orgID, url s
|
|||||||
return responseEntity, doReq(c.client, req, responseEntity)
|
return responseEntity, doReq(c.client, req, responseEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) getWithRawResponse(ctx context.Context, orgID, url string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/"+orgID+url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := resp.Body.Close()
|
||||||
|
logging.OnError(err).Error("Failed to close response body")
|
||||||
|
}()
|
||||||
|
|
||||||
|
if (resp.StatusCode / 100) != 2 {
|
||||||
|
return nil, readScimError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
func doReq(client *http.Client, req *http.Request, responseEntity interface{}) error {
|
func doReq(client *http.Client, req *http.Request, responseEntity interface{}) error {
|
||||||
addTokenAsHeader(req)
|
addTokenAsHeader(req)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user