mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-02 11:11:47 +08:00
feat: support SCIM protocol (#2393)
* 111 * feat: support scim/Users GET and POST request * feat: support scim/Users DELETE/PATCH/PUT request * feat: better support scim/Users PATCH request * feat: fix scim/Users logic * feat: gofumpt * feat: fix bug in scim/Users * feat: fix typo --------- Co-authored-by: hsluoyz <hsluoyz@qq.com>
This commit is contained in:
260
scim/user_handler.go
Normal file
260
scim/user_handler.go
Normal file
@ -0,0 +1,260 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scim
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/elimity-com/scim"
|
||||
"github.com/elimity-com/scim/errors"
|
||||
)
|
||||
|
||||
type UserResourceHandler struct{}
|
||||
|
||||
// https://github.com/elimity-com/scim/blob/master/resource_handler_test.go Example in-memory resource handler
|
||||
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.4 How to query/update resources
|
||||
|
||||
func (h UserResourceHandler) Create(r *http.Request, attrs scim.ResourceAttributes) (scim.Resource, error) {
|
||||
resource := &scim.Resource{Attributes: attrs}
|
||||
err := AddScimUser(resource)
|
||||
return *resource, err
|
||||
}
|
||||
|
||||
func (h UserResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) {
|
||||
resource, err := GetScimUser(id)
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
if resource == nil {
|
||||
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
|
||||
}
|
||||
return *resource, nil
|
||||
}
|
||||
|
||||
func (h UserResourceHandler) Delete(r *http.Request, id string) error {
|
||||
user, err := object.GetUserByUserIdOnly(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return errors.ScimErrorResourceNotFound(id)
|
||||
}
|
||||
_, err = object.DeleteUser(user)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h UserResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) {
|
||||
if params.Count == 0 {
|
||||
count, err := object.GetGlobalUserCount("", "")
|
||||
if err != nil {
|
||||
return scim.Page{}, err
|
||||
}
|
||||
return scim.Page{TotalResults: int(count)}, nil
|
||||
}
|
||||
|
||||
resources := make([]scim.Resource, 0)
|
||||
// startIndex is 1-based index
|
||||
users, err := object.GetPaginationGlobalUsers(params.StartIndex-1, params.Count, "", "", "", "")
|
||||
if err != nil {
|
||||
return scim.Page{}, err
|
||||
}
|
||||
for _, user := range users {
|
||||
resources = append(resources, *user2resource(user))
|
||||
}
|
||||
return scim.Page{
|
||||
TotalResults: len(resources),
|
||||
Resources: resources,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h UserResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) {
|
||||
user, err := object.GetUserByUserIdOnly(id)
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
if user == nil {
|
||||
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
|
||||
}
|
||||
return UpdateScimUserByPatchOperation(id, operations)
|
||||
}
|
||||
|
||||
func (h UserResourceHandler) Replace(r *http.Request, id string, attrs scim.ResourceAttributes) (scim.Resource, error) {
|
||||
user, err := object.GetUserByUserIdOnly(id)
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
if user == nil {
|
||||
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
|
||||
}
|
||||
resource := &scim.Resource{Attributes: attrs}
|
||||
err = UpdateScimUser(id, resource)
|
||||
return *resource, err
|
||||
}
|
||||
|
||||
func GetScimUser(id string) (*scim.Resource, error) {
|
||||
user, err := object.GetUserByUserIdOnly(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, nil
|
||||
}
|
||||
r := user2resource(user)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func AddScimUser(r *scim.Resource) error {
|
||||
newUser, err := resource2user(r.Attributes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check whether the user exists.
|
||||
oldUser, err := object.GetUser(newUser.GetId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if oldUser != nil {
|
||||
return errors.ScimErrorUniqueness
|
||||
}
|
||||
|
||||
affect, err := object.AddUser(newUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !affect {
|
||||
return fmt.Errorf("add new user failed")
|
||||
}
|
||||
|
||||
r.Attributes = user2resource(newUser).Attributes
|
||||
r.ID = newUser.Id
|
||||
r.ExternalID = buildExternalId(newUser)
|
||||
r.Meta = buildMeta(newUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateScimUser(id string, r *scim.Resource) error {
|
||||
oldUser, err := object.GetUserByUserIdOnly(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if oldUser == nil {
|
||||
return errors.ScimErrorResourceNotFound(id)
|
||||
}
|
||||
newUser, err := resource2user(r.Attributes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = object.UpdateUser(oldUser.GetId(), newUser, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.ID = newUser.Id
|
||||
r.ExternalID = buildExternalId(newUser)
|
||||
r.Meta = buildMeta(newUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2 Modifying with PATCH
|
||||
func UpdateScimUserByPatchOperation(id string, ops []scim.PatchOperation) (r scim.Resource, err error) {
|
||||
user, err := object.GetUserByUserIdOnly(id)
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
if user == nil {
|
||||
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("invalid patch op value: %v", r)
|
||||
}
|
||||
}()
|
||||
old := user.GetId()
|
||||
for _, op := range ops {
|
||||
value := op.Value
|
||||
if op.Op == scim.PatchOperationRemove {
|
||||
value = nil
|
||||
}
|
||||
// PatchOperationAdd and PatchOperationReplace is same in Casdoor, just replace the value
|
||||
switch op.Path.String() {
|
||||
case "userName":
|
||||
user.Name = ToString(value, "")
|
||||
case "password":
|
||||
user.Password = ToString(value, "")
|
||||
case "externalId":
|
||||
user.ExternalId = ToString(value, "")
|
||||
case "displayName":
|
||||
user.DisplayName = ToString(value, "")
|
||||
case "profileUrl":
|
||||
user.Homepage = ToString(value, "")
|
||||
case "userType":
|
||||
user.Type = ToString(value, "")
|
||||
case "name.givenName":
|
||||
user.FirstName = ToString(value, "")
|
||||
case "name.familyName":
|
||||
user.LastName = ToString(value, "")
|
||||
case "name":
|
||||
defaultV := AnyMap{"givenName": "", "familyName": ""}
|
||||
v := ToAnyMap(value, defaultV) // e.g. {"givenName": "AA", "familyName": "BB"}
|
||||
user.FirstName = ToString(v["givenName"], user.FirstName)
|
||||
user.LastName = ToString(v["familyName"], user.LastName)
|
||||
case "emails":
|
||||
defaultV := AnyArray{AnyMap{"value": ""}}
|
||||
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "test@casdoor"}]
|
||||
if len(vs) > 0 {
|
||||
v := ToAnyMap(vs[0])
|
||||
user.Email = ToString(v["value"], user.Email)
|
||||
}
|
||||
case "phoneNumbers":
|
||||
defaultV := AnyArray{AnyMap{"value": ""}}
|
||||
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "18750004417"}]
|
||||
if len(vs) > 0 {
|
||||
v := ToAnyMap(vs[0])
|
||||
user.Phone = ToString(v["value"], user.Phone)
|
||||
}
|
||||
case "photos":
|
||||
defaultV := AnyArray{AnyMap{"value": ""}}
|
||||
vs := ToAnyArray(value, defaultV) // e.g. [{"value": "https://cdn.casbin.org/img/casbin.svg"}]
|
||||
if len(vs) > 0 {
|
||||
v := ToAnyMap(vs[0])
|
||||
user.Avatar = ToString(v["value"], user.Avatar)
|
||||
}
|
||||
case "addresses":
|
||||
defaultV := AnyArray{AnyMap{"locality": "", "region": "", "country": ""}}
|
||||
vs := ToAnyArray(value, defaultV) // e.g. [{"locality": "Hollywood", "region": "CN", "country": "USA"}]
|
||||
if len(vs) > 0 {
|
||||
v := ToAnyMap(vs[0])
|
||||
user.Location = ToString(v["locality"], user.Location)
|
||||
user.Region = ToString(v["region"], user.Region)
|
||||
user.CountryCode = ToString(v["country"], user.CountryCode)
|
||||
}
|
||||
case UserExtensionKey:
|
||||
defaultV := AnyMap{"organization": user.Owner}
|
||||
v := ToAnyMap(value, defaultV) // e.g. {"organization": "org1"}
|
||||
user.Owner = ToString(v["organization"], user.Owner)
|
||||
case fmt.Sprintf("%v.%v", UserExtensionKey, "organization"):
|
||||
user.Owner = ToString(value, user.Owner)
|
||||
}
|
||||
}
|
||||
_, err = object.UpdateUser(old, user, nil, true)
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
r = *user2resource(user)
|
||||
return r, nil
|
||||
}
|
Reference in New Issue
Block a user