Add basic SCIM implementation
This commit is contained in:
@@ -23,6 +23,7 @@ type (
|
||||
Websocket options.WebsocketOpt
|
||||
Eventbus options.EventbusOpt
|
||||
Federation options.FederationOpt
|
||||
SCIM options.SCIMOpt
|
||||
}
|
||||
)
|
||||
|
||||
@@ -46,5 +47,6 @@ func NewOptions() *Options {
|
||||
Websocket: *options.Websocket(),
|
||||
Eventbus: *options.Eventbus(),
|
||||
Federation: *options.Federation(),
|
||||
SCIM: *options.SCIM(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
messagingRest "github.com/cortezaproject/corteza-server/messaging/rest"
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza-server/pkg/api/server"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/pkg/webapp"
|
||||
systemRest "github.com/cortezaproject/corteza-server/system/rest"
|
||||
"github.com/cortezaproject/corteza-server/system/scim"
|
||||
"github.com/go-chi/chi"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
@@ -80,6 +82,33 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
app.Log.Info("JSON REST API disabled")
|
||||
}
|
||||
|
||||
if app.Opt.SCIM.Enabled {
|
||||
if app.Opt.SCIM.Secret == "" {
|
||||
app.Log.
|
||||
WithOptions(zap.AddStacktrace(zap.PanicLevel)).
|
||||
Error("SCIM secret empty")
|
||||
}
|
||||
|
||||
var (
|
||||
baseUrl = "/" + strings.Trim(app.Opt.SCIM.BaseURL, "/")
|
||||
)
|
||||
|
||||
app.Log.Debug(
|
||||
"SCIM enabled",
|
||||
zap.String("baseUrl", baseUrl),
|
||||
logger.Mask("secret", app.Opt.SCIM.Secret),
|
||||
)
|
||||
|
||||
r.Route(baseUrl, func(r chi.Router) {
|
||||
|
||||
if !app.Opt.Environment.IsDevelopment() {
|
||||
r.Use(scim.Guard(app.Opt.SCIM))
|
||||
}
|
||||
|
||||
scim.Routes(r)
|
||||
})
|
||||
}
|
||||
|
||||
if app.Opt.HTTPServer.WebappEnabled {
|
||||
r.Route("/"+webappBaseUrl, webapp.MakeWebappServer(app.Opt.HTTPServer))
|
||||
|
||||
|
||||
37
pkg/options/SCIM.gen.go
Normal file
37
pkg/options/SCIM.gen.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package options
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// pkg/options/SCIM.yaml
|
||||
|
||||
type (
|
||||
SCIMOpt struct {
|
||||
Enabled bool `env:"SCIM_ENABLED"`
|
||||
BaseURL string `env:"SCIM_BASE_URL"`
|
||||
Secret string `env:"SCIM_SECRET"`
|
||||
}
|
||||
)
|
||||
|
||||
// SCIM initializes and returns a SCIMOpt with default values
|
||||
func SCIM() (o *SCIMOpt) {
|
||||
o = &SCIMOpt{
|
||||
BaseURL: "/scim",
|
||||
}
|
||||
|
||||
fill(o)
|
||||
|
||||
// Function that allows access to custom logic inside the parent function.
|
||||
// The custom logic in the other file should be like:
|
||||
// func (o *SCIM) Defaults() {...}
|
||||
func(o interface{}) {
|
||||
if def, ok := o.(interface{ Defaults() }); ok {
|
||||
def.Defaults()
|
||||
}
|
||||
}(o)
|
||||
|
||||
return
|
||||
}
|
||||
8
pkg/options/SCIM.yaml
Normal file
8
pkg/options/SCIM.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
name: SCIM
|
||||
|
||||
props:
|
||||
- name: enabled
|
||||
type: bool
|
||||
- name: baseURL
|
||||
default: "/scim"
|
||||
- name: secret
|
||||
11
system/scim/Makefile
Normal file
11
system/scim/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
.PHONY: clean all
|
||||
|
||||
include ../../Makefile.inc
|
||||
|
||||
all: static.go
|
||||
|
||||
static.go: $(STATIK)
|
||||
$(STATIK) -p assets -m -Z -f -src=$(@D)/assets
|
||||
|
||||
clean:
|
||||
rm -f static.go
|
||||
6
system/scim/README.adoc
Normal file
6
system/scim/README.adoc
Normal file
@@ -0,0 +1,6 @@
|
||||
= SCIM Support for Corteza
|
||||
|
||||
Here is a bare minimum support for SCIM.
|
||||
|
||||
NOTE: Experiments with github.com/imulab/go-scim lib failed due to complexity of the implementation
|
||||
and resources needed for bending the lib to our needs.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "Group",
|
||||
"name": "Group",
|
||||
"endpoint": "/Groups",
|
||||
"schema": "urn:ietf:params:scim:schemas:core:2.0:Group"
|
||||
}
|
||||
12
system/scim/assets/resource_types/user_resource_type.json
Normal file
12
system/scim/assets/resource_types/user_resource_type.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "User",
|
||||
"name": "User",
|
||||
"endpoint": "/Users",
|
||||
"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
"schemaExtensions": [
|
||||
{
|
||||
"schema": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
128
system/scim/assets/schemas/core_schema.json
Normal file
128
system/scim/assets/schemas/core_schema.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"id": "core",
|
||||
"name": "Core",
|
||||
"description": "Shared attributes for all SCIM resources",
|
||||
"attributes": [
|
||||
{
|
||||
"id": "schemas",
|
||||
"name": "schemas",
|
||||
"type": "reference",
|
||||
"multiValued": true,
|
||||
"required": true,
|
||||
"caseExact": true,
|
||||
"returned": "always",
|
||||
"_index": 0,
|
||||
"_path": "schemas",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "id",
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"caseExact": true,
|
||||
"returned": "always",
|
||||
"mutability": "readOnly",
|
||||
"uniqueness": "global",
|
||||
"_index": 1,
|
||||
"_path": "id",
|
||||
"_annotations": {
|
||||
"@ReadOnly": {
|
||||
"reset": true,
|
||||
"copy": true
|
||||
},
|
||||
"@UUID": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "externalId",
|
||||
"name": "externalId",
|
||||
"type": "string",
|
||||
"_index": 2,
|
||||
"_path": "externalId"
|
||||
},
|
||||
{
|
||||
"id": "meta",
|
||||
"name": "meta",
|
||||
"type": "complex",
|
||||
"mutability": "readOnly",
|
||||
"_index": 3,
|
||||
"_path": "meta",
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "meta.resourceType",
|
||||
"name": "resourceType",
|
||||
"type": "string",
|
||||
"caseExact": true,
|
||||
"mutability": "readOnly",
|
||||
"_index": 0,
|
||||
"_path": "meta.resourceType",
|
||||
"_annotations": {
|
||||
"@ReadOnly": {
|
||||
"reset": true,
|
||||
"copy": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "meta.created",
|
||||
"name": "created",
|
||||
"type": "dateTime",
|
||||
"mutability": "readOnly",
|
||||
"_index": 1,
|
||||
"_path": "meta.created",
|
||||
"_annotations": {
|
||||
"@ReadOnly": {
|
||||
"reset": true,
|
||||
"copy": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "meta.lastModified",
|
||||
"name": "lastModified",
|
||||
"type": "dateTime",
|
||||
"mutability": "readOnly",
|
||||
"_index": 2,
|
||||
"_path": "meta.lastModified",
|
||||
"_annotations": {
|
||||
"@ReadOnly": {
|
||||
"reset": true,
|
||||
"copy": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "meta.location",
|
||||
"name": "location",
|
||||
"type": "reference",
|
||||
"mutability": "readOnly",
|
||||
"caseExact": true,
|
||||
"_index": 3,
|
||||
"_path": "meta.location",
|
||||
"_annotations": {
|
||||
"@ReadOnly": {
|
||||
"reset": true,
|
||||
"copy": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "meta.version",
|
||||
"name": "version",
|
||||
"type": "string",
|
||||
"mutability": "readOnly",
|
||||
"_index": 4,
|
||||
"_path": "meta.version",
|
||||
"_annotations": {
|
||||
"@ReadOnly": {
|
||||
"reset": true,
|
||||
"copy": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
56
system/scim/assets/schemas/group_schema.json
Normal file
56
system/scim/assets/schemas/group_schema.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:Group",
|
||||
"name": "Group",
|
||||
"description": "Defined attributes for the group schema",
|
||||
"attributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:Group:displayName",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"_index": 100,
|
||||
"_path": "displayName"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:Group:members",
|
||||
"name": "members",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:Group:members.value",
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"mutability": "immutable",
|
||||
"_index": 0,
|
||||
"_path": "members.value",
|
||||
"_annotations":{
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:Group:members.$ref",
|
||||
"name": "$ref",
|
||||
"type": "reference",
|
||||
"mutability": "immutable",
|
||||
"_index": 1,
|
||||
"_path": "members.$ref"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:Group:members.display",
|
||||
"name": "display",
|
||||
"type": "string",
|
||||
"_index": 2,
|
||||
"_path": "members.display"
|
||||
}
|
||||
],
|
||||
"_index": 101,
|
||||
"_path": "members",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
|
||||
"name": "Enterprise User",
|
||||
"description": "Extension attributes for enterprises",
|
||||
"attributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber",
|
||||
"name": "employeeNumber",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:costCenter",
|
||||
"name": "costCenter",
|
||||
"type": "string",
|
||||
"_index": 1,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:costCenter"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization",
|
||||
"name": "organization",
|
||||
"type": "string",
|
||||
"_index": 2,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:division",
|
||||
"name": "division",
|
||||
"type": "string",
|
||||
"_index": 3,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:division"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department",
|
||||
"name": "department",
|
||||
"type": "string",
|
||||
"_index": 4,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager",
|
||||
"name": "manager",
|
||||
"type": "complex",
|
||||
"_index": 5,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager",
|
||||
"_annotations": {
|
||||
"@StateSummary": {}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value",
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.$ref",
|
||||
"name": "$ref",
|
||||
"type": "reference",
|
||||
"_index": 1,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.$ref"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.displayName",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"_index": 2,
|
||||
"_path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.displayName"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
738
system/scim/assets/schemas/user_schema.json
Normal file
738
system/scim/assets/schemas/user_schema.json
Normal file
@@ -0,0 +1,738 @@
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
"name": "User",
|
||||
"description": "Defined attributes for the user schema",
|
||||
"attributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:userName",
|
||||
"name": "userName",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"uniqueness": "server",
|
||||
"_index": 100,
|
||||
"_path": "userName"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:name",
|
||||
"name": "name",
|
||||
"type": "complex",
|
||||
"_index": 101,
|
||||
"_path": "name",
|
||||
"_annotations": {
|
||||
"@StateSummary": {}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:name.formatted",
|
||||
"name": "formatted",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "name.formatted",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:name.familyName",
|
||||
"name": "familyName",
|
||||
"type": "string",
|
||||
"_index": 1,
|
||||
"_path": "name.familyName",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:name.givenName",
|
||||
"name": "givenName",
|
||||
"type": "string",
|
||||
"_index": 2,
|
||||
"_path": "name.givenName",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:name.middleName",
|
||||
"name": "middleName",
|
||||
"type": "string",
|
||||
"_index": 3,
|
||||
"_path": "name.middleName",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:name.honorificPrefix",
|
||||
"name": "honorificPrefix",
|
||||
"type": "string",
|
||||
"_index": 4,
|
||||
"_path": "name.honorificPrefix",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:name.honorificSuffix",
|
||||
"name": "honorificSuffix",
|
||||
"type": "string",
|
||||
"_index": 5,
|
||||
"_path": "name.honorificSuffix",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:displayName",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"_index": 102,
|
||||
"_path": "displayName"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:nickName",
|
||||
"name": "nickName",
|
||||
"type": "string",
|
||||
"_index": 103,
|
||||
"_path": "nickName"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:profileUrl",
|
||||
"name": "profileUrl",
|
||||
"type": "reference",
|
||||
"referenceTypes": [
|
||||
"external"
|
||||
],
|
||||
"_index": 104,
|
||||
"_path": "profileUrl"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:title",
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"_index": 105,
|
||||
"_path": "title"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:userType",
|
||||
"name": "userType",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"Employee",
|
||||
"Intern"
|
||||
],
|
||||
"_index": 106,
|
||||
"_path": "userType"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:preferredLanguage",
|
||||
"name": "preferredLanguage",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"zh_CN",
|
||||
"en_US"
|
||||
],
|
||||
"_index": 107,
|
||||
"_path": "preferredLanguage"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:locale",
|
||||
"name": "locale",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"en_US",
|
||||
"zh_CN"
|
||||
],
|
||||
"_index": 108,
|
||||
"_path": "locale"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:timezone",
|
||||
"name": "timezone",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"Asia/Shanghai",
|
||||
"Asia/Beijing",
|
||||
"America/New_York",
|
||||
"America/Toronto"
|
||||
],
|
||||
"_index": 109,
|
||||
"_path": "timezone"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:active",
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"_index": 110,
|
||||
"_path": "active"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:password",
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"mutability": "writeOnly",
|
||||
"returned": "never",
|
||||
"_index": 111,
|
||||
"_path": "password",
|
||||
"_annotations": {
|
||||
"@BCrypt": {
|
||||
"cost": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:emails",
|
||||
"name": "emails",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"required": true,
|
||||
"_index": 112,
|
||||
"_path": "emails",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ExclusivePrimary": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:emails.value",
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "emails.value",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:emails.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"work",
|
||||
"home",
|
||||
"other"
|
||||
],
|
||||
"_index": 1,
|
||||
"_path": "emails.type",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:emails.primary",
|
||||
"name": "primary",
|
||||
"type": "boolean",
|
||||
"_index": 2,
|
||||
"_path": "emails.primary",
|
||||
"_annotations": {
|
||||
"@Primary": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:emails.display",
|
||||
"name": "display",
|
||||
"type": "string",
|
||||
"_index": 3,
|
||||
"_path": "emails.display"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:phoneNumbers",
|
||||
"name": "phoneNumbers",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"_index": 113,
|
||||
"_path": "phoneNumbers",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ExclusivePrimary": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:phoneNumbers.value",
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "phoneNumbers.value",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:phoneNumbers.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"work",
|
||||
"home",
|
||||
"mobile",
|
||||
"fax",
|
||||
"other"
|
||||
],
|
||||
"_index": 1,
|
||||
"_path": "phoneNumbers.type",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:phoneNumbers.primary",
|
||||
"name": "primary",
|
||||
"type": "boolean",
|
||||
"_index": 2,
|
||||
"_path": "phoneNumbers.primary",
|
||||
"_annotations": {
|
||||
"@Primary": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:phoneNumbers.display",
|
||||
"name": "display",
|
||||
"type": "string",
|
||||
"_index": 3,
|
||||
"_path": "phoneNumbers.display"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:ims",
|
||||
"name": "ims",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"_index": 114,
|
||||
"_path": "ims",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ExclusivePrimary": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:ims.value",
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "ims.value",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:ims.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"skype",
|
||||
"qq",
|
||||
"wechat",
|
||||
"weibo",
|
||||
"other"
|
||||
],
|
||||
"_index": 1,
|
||||
"_path": "ims.type",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:ims.primary",
|
||||
"name": "primary",
|
||||
"type": "boolean",
|
||||
"_index": 2,
|
||||
"_path": "ims.primary",
|
||||
"_annotations": {
|
||||
"@Primary": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:ims.display",
|
||||
"name": "display",
|
||||
"type": "string",
|
||||
"_index": 3,
|
||||
"_path": "ims.display"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:photos",
|
||||
"name": "photos",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"_index": 115,
|
||||
"_path": "photos",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ExclusivePrimary": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:photos.value",
|
||||
"name": "value",
|
||||
"type": "reference",
|
||||
"referenceTypes": [
|
||||
"external"
|
||||
],
|
||||
"_index": 0,
|
||||
"_path": "photos.value",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:photos.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"photo",
|
||||
"thumbnail"
|
||||
],
|
||||
"_index": 1,
|
||||
"_path": "photos.type",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:photos.primary",
|
||||
"name": "primary",
|
||||
"type": "boolean",
|
||||
"_index": 2,
|
||||
"_path": "photos.primary",
|
||||
"_annotations": {
|
||||
"@Primary": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses",
|
||||
"name": "addresses",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"_index": 116,
|
||||
"_path": "addresses",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ExclusivePrimary": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses.formatted",
|
||||
"name": "formatted",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "photos.formatted"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses.streetAddress",
|
||||
"name": "streetAddress",
|
||||
"type": "string",
|
||||
"_index": 1,
|
||||
"_path": "photos.streetAddress",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses.locality",
|
||||
"name": "locality",
|
||||
"type": "string",
|
||||
"_index": 2,
|
||||
"_path": "photos.locality",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses.region",
|
||||
"name": "region",
|
||||
"type": "string",
|
||||
"_index": 3,
|
||||
"_path": "photos.region",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses.postalCode",
|
||||
"name": "postalCode",
|
||||
"type": "string",
|
||||
"_index": 4,
|
||||
"_path": "photos.postalCode",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses.country",
|
||||
"name": "country",
|
||||
"type": "string",
|
||||
"_index": 5,
|
||||
"_path": "photos.country",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"canonicalValues": [
|
||||
"work",
|
||||
"home",
|
||||
"id",
|
||||
"driver",
|
||||
"other"
|
||||
],
|
||||
"_index": 6,
|
||||
"_path": "photos.type",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:addresses.primary",
|
||||
"name": "primary",
|
||||
"type": "boolean",
|
||||
"_index": 7,
|
||||
"_path": "photos.primary",
|
||||
"_annotations": {
|
||||
"@Primary": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:groups",
|
||||
"name": "groups",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"mutability": "readOnly",
|
||||
"_index": 117,
|
||||
"_path": "groups",
|
||||
"_annotations": {
|
||||
"@ReadOnly": {
|
||||
"reset": true,
|
||||
"copy": true
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:groups.value",
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"mutability": "readOnly",
|
||||
"_index": 0,
|
||||
"_path": "groups.value"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:groups.$ref",
|
||||
"name": "$ref",
|
||||
"type": "reference",
|
||||
"mutability": "readOnly",
|
||||
"_index": 1,
|
||||
"_path": "groups.$ref"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:groups.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"mutability": "readOnly",
|
||||
"canonicalValues": [
|
||||
"direct",
|
||||
"indirect"
|
||||
],
|
||||
"_index": 2,
|
||||
"_path": "groups.type"
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:groups.display",
|
||||
"name": "display",
|
||||
"type": "string",
|
||||
"mutability": "readOnly",
|
||||
"_index": 3,
|
||||
"_path": "groups.display"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:entitlements",
|
||||
"name": "entitlements",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"_index": 118,
|
||||
"_path": "entitlements",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ExclusivePrimary": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:entitlements.value",
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "entitlements.value",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:entitlements.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "entitlements.type",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:entitlements.primary",
|
||||
"name": "primary",
|
||||
"type": "boolean",
|
||||
"_index": 0,
|
||||
"_path": "entitlements.primary",
|
||||
"_annotations": {
|
||||
"@Primary": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:entitlements.display",
|
||||
"name": "display",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "entitlements.display"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:roles",
|
||||
"name": "roles",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"_index": 119,
|
||||
"_path": "roles",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ExclusivePrimary": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:roles.value",
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"_index": 0,
|
||||
"_path": "roles.value",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:roles.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"_index": 1,
|
||||
"_path": "roles.type",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:roles.primary",
|
||||
"name": "primary",
|
||||
"type": "boolean",
|
||||
"_index": 2,
|
||||
"_path": "roles.primary",
|
||||
"_annotations": {
|
||||
"@Primary": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:roles.display",
|
||||
"name": "display",
|
||||
"type": "string",
|
||||
"_index": 3,
|
||||
"_path": "roles.display"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:x509Certificates",
|
||||
"name": "x509Certificates",
|
||||
"type": "complex",
|
||||
"multiValued": true,
|
||||
"_index": 120,
|
||||
"_path": "x509Certificates",
|
||||
"_annotations": {
|
||||
"@AutoCompact": {},
|
||||
"@ExclusivePrimary": {},
|
||||
"@ElementAnnotations": {
|
||||
"@StateSummary": {}
|
||||
}
|
||||
},
|
||||
"subAttributes": [
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:x509Certificates.value",
|
||||
"name": "value",
|
||||
"type": "binary",
|
||||
"_index": 0,
|
||||
"_path": "x509Certificates.value",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:x509Certificates.type",
|
||||
"name": "type",
|
||||
"type": "string",
|
||||
"_index": 1,
|
||||
"_path": "x509Certificates.type",
|
||||
"_annotations": {
|
||||
"@Identity": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:x509Certificates.primary",
|
||||
"name": "primary",
|
||||
"type": "boolean",
|
||||
"_index": 2,
|
||||
"_path": "x509Certificates.primary",
|
||||
"_annotations": {
|
||||
"@Primary": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urn:ietf:params:scim:schemas:core:2.0:User:x509Certificates.display",
|
||||
"name": "display",
|
||||
"type": "string",
|
||||
"_index": 3,
|
||||
"_path": "x509Certificates.display"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
6
system/scim/assets/static.go
Normal file
6
system/scim/assets/static.go
Normal file
File diff suppressed because one or more lines are too long
24
system/scim/gen_response.go
Normal file
24
system/scim/gen_response.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package scim
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
metaResponse struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
Created time.Time `json:"created"`
|
||||
LastModified *time.Time `json:"lastModified,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func newUserMetaResponse(u *types.User) *metaResponse {
|
||||
rsp := &metaResponse{
|
||||
ResourceType: "User",
|
||||
Created: u.CreatedAt,
|
||||
LastModified: u.UpdatedAt,
|
||||
}
|
||||
|
||||
return rsp
|
||||
}
|
||||
14
system/scim/http.go
Normal file
14
system/scim/http.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package scim
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func send(w http.ResponseWriter, payload interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
log.Error("could not encode payload", zap.Error(err))
|
||||
}
|
||||
}
|
||||
54
system/scim/routes.go
Normal file
54
system/scim/routes.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package scim
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/options"
|
||||
"github.com/cortezaproject/corteza-server/system/scim/assets"
|
||||
"github.com/cortezaproject/corteza-server/system/service"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/goware/statik/fs"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
embedded http.FileSystem
|
||||
log = zap.NewNop()
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
embedded, err = fs.New(assets.Asset)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Guard(opt options.SCIMOpt) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
// temp authorization mechanism so we do not have to
|
||||
// pre-create users and generate their auth tokens
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authPrefix := "Bearer "
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if (len(authPrefix)+len(opt.Secret)) == len(authHeader) && opt.Secret == authHeader[len(authPrefix):] {
|
||||
// all good, auth header matches the secret
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Unauthorized", http.StatusForbidden)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Routes(r chi.Router) {
|
||||
uh := &usersHandler{svc: service.DefaultUser}
|
||||
|
||||
r.Route("/Users", func(r chi.Router) {
|
||||
r.Get("/{id}", uh.get)
|
||||
r.Post("/", uh.create)
|
||||
r.Put("/{id}", uh.replace)
|
||||
r.Delete("/{id}", uh.delete)
|
||||
})
|
||||
}
|
||||
144
system/scim/user_handler.go
Normal file
144
system/scim/user_handler.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package scim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/api"
|
||||
"github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza-server/system/service"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/go-chi/chi"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
usersHandler struct {
|
||||
svc service.UserService
|
||||
}
|
||||
)
|
||||
|
||||
func (h usersHandler) get(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
id, _ = strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
|
||||
ctx = auth.SetSuperUserContext(r.Context())
|
||||
svc = h.svc.With(ctx)
|
||||
)
|
||||
|
||||
if id == 0 {
|
||||
http.Error(w, "invalid user id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if u, err := svc.FindByID(id); err != nil {
|
||||
errors.ServeHTTP(w, r, err, !api.DebugFromContext(r.Context()))
|
||||
return
|
||||
} else {
|
||||
send(w, newUserResourceResponse(u))
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
||||
func (h usersHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
var (
|
||||
ctx = auth.SetSuperUserContext(r.Context())
|
||||
)
|
||||
|
||||
if u, err := h.createFromJSON(ctx, r.Body); err != nil {
|
||||
errors.ServeHTTP(w, r, err, !api.DebugFromContext(r.Context()))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
send(w, newUserResourceResponse(u))
|
||||
}
|
||||
}
|
||||
|
||||
func (h usersHandler) createFromJSON(ctx context.Context, j io.Reader) (u *types.User, err error) {
|
||||
var (
|
||||
svc = h.svc.With(ctx)
|
||||
payload = &userResourceRequest{}
|
||||
)
|
||||
|
||||
if err = payload.decodeJSON(j); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// do we need to upsert?
|
||||
if email := payload.Emails.getFirst(); email != "" {
|
||||
u, err = svc.FindByEmail(email)
|
||||
if err != nil && !errors.Is(err, service.UserErrNotFound()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if u == nil || !u.Valid() {
|
||||
// in case when we did not find a valid user,
|
||||
// start from blank
|
||||
u = &types.User{}
|
||||
}
|
||||
|
||||
payload.applyTo(u)
|
||||
|
||||
if u.ID > 0 {
|
||||
return svc.Update(u)
|
||||
} else {
|
||||
return svc.Create(u)
|
||||
}
|
||||
}
|
||||
|
||||
func (h usersHandler) replace(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
var (
|
||||
ctx = auth.SetSuperUserContext(r.Context())
|
||||
userID, _ = strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
|
||||
)
|
||||
|
||||
if u, err := h.updateFromJSON(ctx, userID, r.Body); err != nil {
|
||||
errors.ServeHTTP(w, r, err, !api.DebugFromContext(r.Context()))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
send(w, newUserResourceResponse(u))
|
||||
}
|
||||
}
|
||||
|
||||
func (h usersHandler) updateFromJSON(ctx context.Context, id uint64, j io.Reader) (u *types.User, err error) {
|
||||
var (
|
||||
svc = h.svc.With(ctx)
|
||||
payload = &userResourceRequest{}
|
||||
)
|
||||
|
||||
if u, err = svc.FindByID(id); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if u == nil || !u.Valid() {
|
||||
return nil, fmt.Errorf("refusing to update invalid user")
|
||||
}
|
||||
|
||||
if err = payload.decodeJSON(j); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
payload.applyTo(u)
|
||||
|
||||
return h.svc.With(ctx).Update(u)
|
||||
}
|
||||
|
||||
func (h usersHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = auth.SetSuperUserContext(r.Context())
|
||||
userID, _ = strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
|
||||
svc = h.svc.With(ctx)
|
||||
)
|
||||
|
||||
if err := svc.Delete(userID); err != nil {
|
||||
send(w, err)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
115
system/scim/user_payloads.go
Normal file
115
system/scim/user_payloads.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package scim
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/handle"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
urnUser = "urn:ietf:params:scim:schemas:core:2.0:User"
|
||||
userLabel_SCIM_externalId = "SCIM_externalId"
|
||||
)
|
||||
|
||||
type (
|
||||
emailResponse struct {
|
||||
Value string `json:"value"`
|
||||
Primary bool `json:"primary,omitempty"`
|
||||
}
|
||||
|
||||
emailsResponse []*emailResponse
|
||||
|
||||
userNameResponse struct {
|
||||
Formatted string `json:"formatted"`
|
||||
}
|
||||
|
||||
userResourceResponse struct {
|
||||
Schemas []string `json:"schemas"`
|
||||
Meta *metaResponse `json:"meta,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
ExternalId string `json:"externalId,omitempty"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
Name *userNameResponse `json:"displayName"`
|
||||
Emails emailsResponse `json:"emails,omitempty"`
|
||||
}
|
||||
|
||||
userResourceRequest struct {
|
||||
Schemas []string `json:"schemas"`
|
||||
Meta *metaResponse `json:"meta,omitempty"`
|
||||
ExternalId *string `json:"externalId,omitempty"`
|
||||
UserName *string `json:"userName,omitempty"`
|
||||
NickName *string `json:"nickName,omitempty"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
Name *userNameResponse `json:"name"`
|
||||
Emails emailsResponse `json:"emails,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func newUserResourceResponse(u *types.User) *userResourceResponse {
|
||||
rsp := &userResourceResponse{
|
||||
Schemas: []string{urnUser},
|
||||
Meta: newUserMetaResponse(u),
|
||||
ID: strconv.FormatUint(u.ID, 10),
|
||||
ExternalId: u.Labels[userLabel_SCIM_externalId],
|
||||
UserName: u.Username,
|
||||
NickName: u.Handle,
|
||||
Emails: emailsResponse{{u.Email, true}},
|
||||
}
|
||||
|
||||
if u.Name != "" {
|
||||
rsp.Name = &userNameResponse{Formatted: u.Name}
|
||||
}
|
||||
|
||||
return rsp
|
||||
}
|
||||
|
||||
// returns first (primary) email
|
||||
func (ee emailsResponse) getFirst() string {
|
||||
if len(ee) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var match int
|
||||
for i, e := range ee {
|
||||
if e.Primary {
|
||||
match = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ee[match].Value
|
||||
}
|
||||
|
||||
func (req *userResourceRequest) decodeJSON(r io.Reader) error {
|
||||
if err := json.NewDecoder(r).Decode(req); err != nil {
|
||||
return fmt.Errorf("could not decode user payload: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *userResourceRequest) applyTo(u *types.User) {
|
||||
if v := req.Emails.getFirst(); len(v) > 0 {
|
||||
u.Email = v
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
u.Name = req.Name.Formatted
|
||||
}
|
||||
|
||||
if req.UserName != nil {
|
||||
u.Username = *req.UserName
|
||||
}
|
||||
|
||||
if req.NickName != nil && handle.IsValid(*req.NickName) {
|
||||
u.Handle = *req.NickName
|
||||
}
|
||||
|
||||
if req.ExternalId != nil {
|
||||
u.SetLabel("SCIM_externalId", *req.ExternalId)
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,6 @@ func InitTestApp() {
|
||||
eventbus.Set(eventBus)
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
|
||||
129
tests/system/scim_test.go
Normal file
129
tests/system/scim_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/api/server"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
"github.com/cortezaproject/corteza-server/system/scim"
|
||||
"github.com/cortezaproject/corteza-server/system/service"
|
||||
"github.com/cortezaproject/corteza-server/tests/helpers"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/steinfletcher/apitest"
|
||||
jsonpath "github.com/steinfletcher/apitest-jsonpath"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
scimRoutes chi.Router
|
||||
)
|
||||
|
||||
// apitest basics, initialize, set handler, add auth
|
||||
func (h helper) scimApiInit() *apitest.APITest {
|
||||
InitTestApp()
|
||||
|
||||
if scimRoutes == nil {
|
||||
scimRoutes = chi.NewRouter()
|
||||
scimRoutes.Use(server.BaseMiddleware(false, logger.Default())...)
|
||||
scim.Routes(scimRoutes)
|
||||
}
|
||||
|
||||
return apitest.
|
||||
New().
|
||||
Handler(scimRoutes)
|
||||
}
|
||||
|
||||
func TestScimUserGet(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearUsers()
|
||||
|
||||
u := h.createUserWithEmail(h.randEmail())
|
||||
|
||||
h.scimApiInit().
|
||||
Get(fmt.Sprintf("/Users/%d", u.ID)).
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Assert(helpers.AssertNoErrors).
|
||||
Assert(jsonpath.Contains(`$.schemas`, "urn:ietf:params:scim:schemas:core:2.0:User")).
|
||||
Assert(jsonpath.Equal(`$.id`, fmt.Sprintf("%d", u.ID))).
|
||||
End()
|
||||
}
|
||||
|
||||
func TestScimUserCreate(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearUsers()
|
||||
|
||||
h.scimApiInit().
|
||||
Debug().
|
||||
Post("/Users").
|
||||
JSON(`{
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User"
|
||||
],
|
||||
"userName": "foo",
|
||||
"nickName": "baz",
|
||||
"emails": [
|
||||
{
|
||||
"value": "foo@bar.com",
|
||||
"primary": true
|
||||
},
|
||||
{
|
||||
"value": "bar@foo.com"
|
||||
}
|
||||
]
|
||||
}`).
|
||||
Expect(t).
|
||||
Status(http.StatusCreated).
|
||||
Assert(helpers.AssertNoErrors).
|
||||
End()
|
||||
|
||||
u, err := store.LookupUserByEmail(context.Background(), service.DefaultStore, "foo@bar.com")
|
||||
h.a.NoError(err)
|
||||
h.a.Equal("foo", u.Username)
|
||||
h.a.Equal("baz", u.Handle)
|
||||
}
|
||||
|
||||
func TestScimUserReplace(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearUsers()
|
||||
|
||||
u := h.createUserWithEmail(h.randEmail())
|
||||
|
||||
h.scimApiInit().
|
||||
Debug().
|
||||
Put(fmt.Sprintf("/Users/%d", u.ID)).
|
||||
JSON(`{
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User"
|
||||
],
|
||||
"userName": "bar",
|
||||
"emails": [
|
||||
{
|
||||
"value": "foo@bar.com"
|
||||
}
|
||||
]
|
||||
}`).
|
||||
Expect(t).
|
||||
//Status(http.StatusNoContent).
|
||||
End()
|
||||
|
||||
u, err := store.LookupUserByID(context.Background(), service.DefaultStore, u.ID)
|
||||
h.a.NoError(err)
|
||||
h.a.NotNil(u)
|
||||
h.a.Equal("foo@bar.com", u.Email)
|
||||
}
|
||||
|
||||
func TestScimUserDelete(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearUsers()
|
||||
|
||||
u := h.createUserWithEmail(h.randEmail())
|
||||
|
||||
h.scimApiInit().
|
||||
Delete(fmt.Sprintf("/Users/%d", u.ID)).
|
||||
Expect(t).
|
||||
Status(http.StatusNoContent).
|
||||
End()
|
||||
}
|
||||
Reference in New Issue
Block a user