From 89030e313677bb730c1ae0bb770062b569f2d4bf Mon Sep 17 00:00:00 2001 From: florindragos Date: Mon, 24 Mar 2025 16:43:13 +0200 Subject: [PATCH 01/13] pkg common --- .github/workflows/ci.yaml | 33 +- common/assets.go | 25 ++ common/assets/users-groups-roles.tmpl | 156 +++++++++ common/assets/users-groups.tmpl | 130 ++++++++ common/assets/users.tmpl | 110 +++++++ common/config/config.go | 84 +++++ common/convert/config.go | 120 +++++++ common/convert/convert.go | 206 ++++++++++++ common/convert/converter_test.go | 102 ++++++ common/directory/client.go | 318 +++++++++++++++++++ common/go.mod | 51 +++ common/go.sum | 126 ++++++++ common/handlers/groups/create.go | 61 ++++ common/handlers/groups/delete.go | 16 + common/handlers/groups/get.go | 88 +++++ common/handlers/groups/handler.go | 28 ++ common/handlers/groups/patch.go | 99 ++++++ common/handlers/groups/replace.go | 27 ++ common/handlers/handler.go | 16 + common/handlers/users/create.go | 65 ++++ {pkg/app => common}/handlers/users/delete.go | 52 +-- {pkg/app => common}/handlers/users/get.go | 32 +- common/handlers/users/handler.go | 30 ++ common/handlers/users/patch.go | 101 ++++++ common/handlers/users/replace.go | 29 ++ {pkg/common => common/model}/group.go | 6 +- {pkg/common => common/model}/user.go | 2 +- common/patch.go | 236 ++++++++++++++ go.mod | 49 +-- go.sum | 116 ++++--- go.work | 6 + makefile | 6 +- pkg/{ => app}/directory/client.go | 1 + pkg/app/groups.go | 42 +++ pkg/app/handlers/groups/create.go | 50 --- pkg/app/handlers/groups/delete.go | 32 -- pkg/app/handlers/groups/get.go | 74 ----- pkg/app/handlers/groups/handler.go | 60 ---- pkg/app/handlers/groups/patch.go | 238 -------------- pkg/app/handlers/groups/replace.go | 61 ---- pkg/app/handlers/users/create.go | 72 ----- pkg/app/handlers/users/handler.go | 263 --------------- pkg/app/handlers/users/patch.go | 236 -------------- pkg/app/handlers/users/replace.go | 84 ----- pkg/app/run.go | 42 ++- pkg/app/users.go | 42 +++ pkg/common/convert.go | 115 ------- pkg/config/config.go | 57 +--- 48 files changed, 2531 insertions(+), 1464 deletions(-) create mode 100644 common/assets.go create mode 100644 common/assets/users-groups-roles.tmpl create mode 100644 common/assets/users-groups.tmpl create mode 100644 common/assets/users.tmpl create mode 100644 common/config/config.go create mode 100644 common/convert/config.go create mode 100644 common/convert/convert.go create mode 100644 common/convert/converter_test.go create mode 100644 common/directory/client.go create mode 100644 common/go.mod create mode 100644 common/go.sum create mode 100644 common/handlers/groups/create.go create mode 100644 common/handlers/groups/delete.go create mode 100644 common/handlers/groups/get.go create mode 100644 common/handlers/groups/handler.go create mode 100644 common/handlers/groups/patch.go create mode 100644 common/handlers/groups/replace.go create mode 100644 common/handlers/handler.go create mode 100644 common/handlers/users/create.go rename {pkg/app => common}/handlers/users/delete.go (56%) rename {pkg/app => common}/handlers/users/get.go (73%) create mode 100644 common/handlers/users/handler.go create mode 100644 common/handlers/users/patch.go create mode 100644 common/handlers/users/replace.go rename {pkg/common => common/model}/group.go (81%) rename {pkg/common => common/model}/user.go (99%) create mode 100644 common/patch.go create mode 100644 go.work rename pkg/{ => app}/directory/client.go (99%) create mode 100644 pkg/app/groups.go delete mode 100644 pkg/app/handlers/groups/create.go delete mode 100644 pkg/app/handlers/groups/delete.go delete mode 100644 pkg/app/handlers/groups/get.go delete mode 100644 pkg/app/handlers/groups/handler.go delete mode 100644 pkg/app/handlers/groups/patch.go delete mode 100644 pkg/app/handlers/groups/replace.go delete mode 100644 pkg/app/handlers/users/create.go delete mode 100644 pkg/app/handlers/users/handler.go delete mode 100644 pkg/app/handlers/users/patch.go delete mode 100644 pkg/app/handlers/users/replace.go create mode 100644 pkg/app/users.go delete mode 100644 pkg/common/convert.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bad9c40..7f37c41 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,12 +18,19 @@ env: PRE_RELEASE: ${{ github.ref == 'refs/heads/main' && 'main' || '' }} GO_VERSION: "1.23" GO_RELEASER_VERSION: "v2.3.2" - GO_LANGCI_LINT_VERSION: "v1.61.0" + GO_LANGCI_LINT_VERSION: "v1.64.5" GO_TESTSUM_VERSION: "1.11.0" jobs: test: runs-on: ubuntu-latest + + strategy: + matrix: + package: + - "./" + - "./common" + steps: - uses: actions/checkout@v4 @@ -33,21 +40,21 @@ jobs: with: go-version: ${{ env.GO_VERSION }} - - name: Build - uses: goreleaser/goreleaser-action@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - distribution: goreleaser - version: ${{ env.GO_RELEASER_VERSION }} - args: build --clean --snapshot --single-target - - - name: Lint + name: Lint package ${{ matrix.package }} uses: golangci/golangci-lint-action@v6 with: version: ${{ env.GO_LANGCI_LINT_VERSION }} - verify: false - args: --timeout=30m + install-mode: binary + working-directory: ${{ matrix.package}} + args: --config="${{ github.workspace }}/.golangci.yaml" + - + name: Test Setup + uses: gertd/action-gotestsum@v3.0.0 + with: + gotestsum_version: ${{ env.GO_TESTSUM_VERSION }} + - name: Test package ${{ matrix.package }} + run: | + gotestsum --format short-verbose -- -count=1 -v ${{ matrix.package }}/... push: runs-on: ubuntu-latest diff --git a/common/assets.go b/common/assets.go new file mode 100644 index 0000000..353b865 --- /dev/null +++ b/common/assets.go @@ -0,0 +1,25 @@ +package common + +import ( + "embed" + "fmt" +) + +//go:embed assets/* +var staticAssets embed.FS + +func Assets() embed.FS { + return staticAssets +} + +func GetTemplateContent(templateName string) ([]byte, error) { + var templateContent []byte + var err error + templateFile := fmt.Sprintf("assets/%s.tmpl", templateName) + templateContent, err = Assets().ReadFile(templateFile) + if err != nil { + return nil, err + } + + return templateContent, nil +} diff --git a/common/assets/users-groups-roles.tmpl b/common/assets/users-groups-roles.tmpl new file mode 100644 index 0000000..0733fe9 --- /dev/null +++ b/common/assets/users-groups-roles.tmpl @@ -0,0 +1,156 @@ +{ + "objects": [ + {{ if eq .objectType "user" }} + { + "id": "{{ $.input.userName }}", + "type": "{{ $.vars.user.object_type }}", + "displayName": "{{ $.input.displayName }}" + }, + { + "id": "{{ $.input.userName }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties": { + "verified": true + } + }, + {{ range $i, $element := $.input.emails }} + {{ if $i }},{{ end }} + { + "id": "{{ $element.value }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties":{ + "type": "{{ $element.type }}", + "verified": true + } + } + {{ end }} + {{ if and ($.input.externalId) (ne $.input.externalId "") }} + , + { + "id": "{{ $.input.externalId }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties": { + "verified": true + } + } + {{ end }} + {{ if $.input.roles }} + {{ range $i, $element := $.input.roles }} + , + { + "id": "{{ $element.value }}", + "type": "{{ $.vars.role.object_type }}", + "displayName": "{{ $element.display }}", + "properties": { + "type": "{{ $element.type }}", + "primary": {{ $element.primary }} + } + } + {{ end }} + {{ end }} + {{ else }} + { + "id": "{{ $.input.displayName }}", + "type": "{{ $.vars.group.object_type }}", + "displayName": "{{ $.input.displayName }}" + } + {{ end }} + ], + "relations":[ + {{ if eq .objectType "user" }} + {{ $idRelationMap := splitn "#" 2 $.vars.user.identity_relation }} + {{ $idObjType := $idRelationMap._0 }} + {{ $idRelation := $idRelationMap._1 }} + {{ $idSubjType := $.vars.user.object_type }} + {{ $objId := $.input.userName }} + {{ $subjId := $.input.userName }} + + {{ if eq $idObjType $.vars.user.object_type }} + {{ $idSubjType = $.vars.user.identity_object_type }} + {{ $subjId = $.input.userName }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + }, + {{ range $i, $element := $.input.emails }} + {{ if $i }},{{ end }} + {{ if eq $idObjType $.vars.user.object_type }} + {{ $subjId = $element.value }} + {{ $objId := $.input.userName }} + {{ else }} + {{ $subjId := $.input.userName }} + {{ $objId = $element.value }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + } + {{ end }} + {{ if and ($.input.externalId) (ne $.input.externalId "") }} + , + {{ if eq $idObjType $.vars.user.object_type }} + {{ $objId := $.input.userName }} + {{ $subjId = $.input.externalId }} + {{ else }} + {{ $objId = $.input.externalId }} + {{ $subjId = $.input.userName }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + } + {{ end }} + {{ if and ($.input.manager_relation) (ne $.input.manager_relation "") }} + {{ $manager := index .input "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }} + {{ if $manager }} + {{ if and ($manager.manager.value) (ne $manager.manager.value "") }} + , + { + "object_type": "{{ $.vars.user.object_type }}", + "object_id": "{{ $.input.userName }}", + "relation": "{{ $.vars.user.manager_relation }}", + "subject_type": "{{ $.vars.user.object_type }}", + "subject_id": "{{ $manager.manager.value }}" + } + {{ end }} + {{ end }} + {{ end }} + {{ if $.input.roles }} + {{ range $i, $element := $.input.roles }} + , + { + "object_type": "{{ $.vars.role.object_type }}", + "object_id": "{{ $element.value }}", + "relation": "{{ $.vars.role.role_relation }}", + "subject_type": "{{ $.vars.user.object_type }}", + "subject_id": "{{ $.input.userName }}" + } + {{ end }} + {{ end }} + {{ else }} + {{ $members := index .input "members" }} + {{ if $members }} + {{ range $i, $member := $members }} + {{ if $i }},{{ end }} + { + "object_type": "{{ $.vars.group.object_type }}", + "object_id": "{{ $.input.displayName }}", + "relation": "{{ $.vars.group.group_member_relation }}", + "subject_type": "{{ $.vars.user.object_type }}", + "subject_id": "{{ $member.value }}" + } + {{ end }} + {{ end }} + {{ end }} + ] +} diff --git a/common/assets/users-groups.tmpl b/common/assets/users-groups.tmpl new file mode 100644 index 0000000..44d665a --- /dev/null +++ b/common/assets/users-groups.tmpl @@ -0,0 +1,130 @@ +{ + "objects": [ + {{ if eq .objectType "user" }} + { + "id": "{{ $.input.userName }}", + "type": "{{ $.vars.user.object_type }}", + "displayName": "{{ $.input.displayName }}" + }, + { + "id": "{{ $.input.userName }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties": { + "verified": true + } + }, + {{ range $i, $element := $.input.emails }} + {{ if $i }},{{ end }} + { + "id": "{{ $element.value }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties":{ + "type": "{{ $element.type }}", + "verified": true + } + } + {{ end }} + {{ if and ($.input.externalId) (ne $.input.externalId "") }} + , + { + "id": "{{ $.input.externalId }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties": { + "verified": true + } + } + {{ end }} + {{ else }} + { + "id": "{{ $.input.displayName }}", + "type": "{{ $.vars.group.object_type }}", + "displayName": "{{ $.input.displayName }}" + } + {{ end }} + ], + "relations":[ + {{ if eq .objectType "user" }} + {{ $idRelationMap := splitn "#" 2 $.vars.user.identity_relation }} + {{ $idObjType := $idRelationMap._0 }} + {{ $idRelation := $idRelationMap._1 }} + {{ $idSubjType := $.vars.user.object_type }} + {{ $objId := $.input.userName }} + {{ $subjId := $.input.userName }} + + {{ if eq $idObjType $.vars.user.object_type }} + {{ $idSubjType = $.vars.user.identity_object_type }} + {{ $subjId = $.input.userName }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + }, + {{ range $i, $element := $.input.emails }} + {{ if $i }},{{ end }} + {{ if eq $idObjType $.vars.user.object_type }} + {{ $subjId = $element.value }} + {{ $objId := $.input.userName }} + {{ else }} + {{ $subjId := $.input.userName }} + {{ $objId = $element.value }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + } + {{ end }} + {{ if and ($.input.externalId) (ne $.input.externalId "") }} + , + {{ if eq $idObjType $.vars.user.object_type }} + {{ $objId := $.input.userName }} + {{ $subjId = $.input.externalId }} + {{ else }} + {{ $objId = $.input.externalId }} + {{ $subjId = $.input.userName }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + } + {{ end }} + {{ if and ($.input.manager_relation) (ne $.input.manager_relation "") }} + {{ $manager := index .input "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }} + {{ if $manager }} + {{ if and ($manager.manager.value) (ne $manager.manager.value "") }} + , + { + "object_type": "{{ $.vars.user.object_type }}", + "object_id": "{{ $.input.userName }}", + "relation": "{{ $.vars.user.manager_relation }}", + "subject_type": "{{ $.vars.user.object_type }}", + "subject_id": "{{ $manager.manager.value }}" + } + {{ end }} + {{ end }} + {{ end }} + {{ else }} + {{ $members := index .input "members" }} + {{ if $members }} + {{ range $i, $member := $members }} + {{ if $i }},{{ end }} + { + "object_type": "{{ $.vars.group.object_type }}", + "object_id": "{{ $.input.displayName }}", + "relation": "{{ $.vars.group.group_member_relation }}", + "subject_type": "{{ $.vars.user.object_type }}", + "subject_id": "{{ $member.value }}" + } + {{ end }} + {{ end }} + {{ end }} + ] +} diff --git a/common/assets/users.tmpl b/common/assets/users.tmpl new file mode 100644 index 0000000..4fcb933 --- /dev/null +++ b/common/assets/users.tmpl @@ -0,0 +1,110 @@ +{ + "objects": [ + {{ if eq .objectType "user" }} + { + "id": "{{ $.input.userName }}", + "type": "{{ $.vars.user.object_type }}", + "displayName": "{{ $.input.displayName }}" + }, + { + "id": "{{ $.input.userName }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties": { + "verified": true + } + }, + {{ range $i, $element := $.input.emails }} + {{ if $i }},{{ end }} + { + "id": "{{ $element.value }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties":{ + "type": "{{ $element.type }}", + "verified": true + } + } + {{ end }} + {{ if and ($.input.externalId) (ne $.input.externalId "") }} + , + { + "id": "{{ $.input.externalId }}", + "type": "{{ $.vars.user.identity_object_type }}", + "properties": { + "verified": true + } + } + {{ end }} + {{ end }} + ], + "relations":[ + {{ if eq .objectType "user" }} + {{ $idRelationMap := splitn "#" 2 $.vars.user.identity_relation }} + {{ $idObjType := $idRelationMap._0 }} + {{ $idRelation := $idRelationMap._1 }} + {{ $idSubjType := $.vars.user.object_type }} + {{ $objId := $.input.userName }} + {{ $subjId := $.input.userName }} + + {{ if eq $idObjType $.vars.user.object_type }} + {{ $idSubjType = $.vars.user.identity_object_type }} + {{ $subjId = $.input.userName }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + }, + {{ range $i, $element := $.input.emails }} + {{ if $i }},{{ end }} + {{ if eq $idObjType $.vars.user.object_type }} + {{ $subjId = $element.value }} + {{ $objId := $.input.userName }} + {{ else }} + {{ $subjId := $.input.userName }} + {{ $objId = $element.value }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + } + {{ end }} + {{ if and ($.input.externalId) (ne $.input.externalId "") }} + , + {{ if eq $idObjType $.vars.user.object_type }} + {{ $objId := $.input.userName }} + {{ $subjId = $.input.externalId }} + {{ else }} + {{ $objId = $.input.externalId }} + {{ $subjId = $.input.userName }} + {{ end }} + { + "object_type": "{{ $idObjType }}", + "object_id": "{{ $objId }}", + "relation": "{{ $idRelation }}", + "subject_type": "{{ $idSubjType }}", + "subject_id": "{{ $subjId }}" + } + {{ end }} + {{ if and ($.input.manager_relation) (ne $.input.manager_relation "") }} + {{ $manager := index .input "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }} + {{ if $manager }} + {{ if and ($manager.manager.value) (ne $manager.manager.value "") }} + , + { + "object_type": "{{ $.vars.user.object_type }}", + "object_id": "{{ $.input.userName }}", + "relation": "{{ $.vars.user.manager_relation }}", + "subject_type": "{{ $.vars.user.object_type }}", + "subject_id": "{{ $manager.manager.value }}" + } + {{ end }} + {{ end }} + {{ end }} + {{ end }} + ] +} diff --git a/common/config/config.go b/common/config/config.go new file mode 100644 index 0000000..2a0a6cb --- /dev/null +++ b/common/config/config.go @@ -0,0 +1,84 @@ +package config + +import ( + "strings" + + "github.com/pkg/errors" +) + +var ErrInvalidConfig = errors.New("invalid config") + +type SCIMConfig struct { + User *UserOptions `json:"user"` + Group *GroupOptions `json:"group"` + Role *RoleOptions `json:"role"` + Relations []*Relation `json:"relations"` + Identity struct { + ObjectType string + Relation string + } `json:"-"` +} + +type UserOptions struct { + ObjectType string `json:"object_type"` + IdentityObjectType string `json:"identity_object_type"` + IdentityRelation string `json:"identity_relation"` + PropertyMapping map[string]string `json:"property_mapping"` + SourceObjectType string `json:"source_object_type"` + ManagerRelation string `json:"manager_relation"` +} + +type GroupOptions struct { + ObjectType string `json:"object_type"` + GroupMemberRelation string `json:"group_member_relation"` + SourceObjectType string `json:"source_object_type"` +} +type RoleOptions struct { + ObjectType string `json:"object_type"` + RoleRelation string `json:"role_relation"` +} + +type Relation struct { + SubjectType string `json:"subject_type"` + SubjectID string `json:"subject_id"` + ObjectType string `json:"object_type"` + ObjectID string `json:"object_id"` + Relation string `json:"relation"` + SubjectRelation string `json:"subject_relation"` +} + +func (cfg *SCIMConfig) Validate() error { + if cfg.User.ObjectType == "" { + return errors.Wrap(ErrInvalidConfig, "scim.user_object_type is required") + } + if cfg.User.IdentityObjectType == "" { + return errors.Wrap(ErrInvalidConfig, "scim.identity_object_type is required") + } + if cfg.User.IdentityRelation == "" { + return errors.Wrap(ErrInvalidConfig, "scim.identity_relation is required") + } else { + object, relation, found := strings.Cut(cfg.User.IdentityRelation, "#") + if !found { + return errors.Wrap(ErrInvalidConfig, "identity relation must be in the format object#relation") + } + if object != cfg.User.IdentityObjectType && object != cfg.User.ObjectType { + return errors.Wrapf(ErrInvalidConfig, "identity relation object type [%s] doesn't match user or identity type", object) + } + if relation == "" { + return errors.Wrap(ErrInvalidConfig, "identity relation relation is required") + } + + cfg.Identity.ObjectType = object + cfg.Identity.Relation = relation + } + if cfg.Group != nil { + if cfg.Group.ObjectType == "" { + return errors.Wrap(ErrInvalidConfig, "scim.group_object_type is required") + } + if cfg.Group.GroupMemberRelation == "" { + return errors.Wrap(ErrInvalidConfig, "scim.group_member_relation is required") + } + } + + return nil +} diff --git a/common/convert/config.go b/common/convert/config.go new file mode 100644 index 0000000..6cb97e1 --- /dev/null +++ b/common/convert/config.go @@ -0,0 +1,120 @@ +package convert + +import ( + "encoding/json" + "strings" + + dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" + "github.com/aserto-dev/scim/common" + "github.com/aserto-dev/scim/common/config" + "github.com/pkg/errors" +) + +type TemplateName int + +const ( + Users TemplateName = iota + UsersGroups + UsersGroupsRoles +) + +var ErrInvalidConfig = errors.New("invalid config") + +func (t TemplateName) String() string { + switch t { + case Users: + return "users" + case UsersGroups: + return "users-groups" + case UsersGroupsRoles: + return "users-groups-roles" + } + return "unknown" +} + +type TransformConfig struct { + template TemplateName + *config.SCIMConfig + IdentityObjectType string `json:"identity_object_type,omitempty"` + IdentityRelation string `json:"identity_relation,omitempty"` +} + +func NewTransformConfig(cfg *config.SCIMConfig) (*TransformConfig, error) { + template := Users + + if cfg.Group != nil { + template = UsersGroups + if cfg.Role != nil { + template = UsersGroupsRoles + } + } + + object, relation, found := strings.Cut(cfg.User.IdentityRelation, "#") + if !found { + return nil, errors.Wrap(ErrInvalidConfig, "identity relation must be in the format object#relation") + } + if object != cfg.User.IdentityObjectType && object != cfg.User.ObjectType { + return nil, errors.Wrapf(ErrInvalidConfig, "identity relation object type [%s] doesn't match user or identity type", object) + } + if relation == "" { + return nil, errors.Wrap(ErrInvalidConfig, "identity relation is required") + } + + return &TransformConfig{ + SCIMConfig: cfg, + template: template, + IdentityObjectType: object, + IdentityRelation: relation, + }, nil +} + +func (c *TransformConfig) Groups() bool { + return c.SCIMConfig.Group != nil +} + +func (c *TransformConfig) ToTemplateVars() (map[string]interface{}, error) { + var result map[string]interface{} + + cfg, err := json.Marshal(c) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal ScimConfig to json") + } + err = json.Unmarshal(cfg, &result) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal ScimConfig to map") + } + + return result, nil +} + +func (c *TransformConfig) GetTemplate() ([]byte, error) { + template, err := common.GetTemplateContent(c.template.String()) + if err != nil { + return nil, err + } + + return template, nil +} + +func (c *TransformConfig) GetIdentityRelation(userID, identity string) (*dsc.Relation, error) { + switch c.IdentityObjectType { + case c.User.IdentityObjectType: + return &dsc.Relation{ + SubjectId: userID, + SubjectType: c.User.ObjectType, + Relation: c.IdentityRelation, + ObjectType: c.User.IdentityObjectType, + ObjectId: identity, + }, nil + case c.User.ObjectType: + return &dsc.Relation{ + SubjectId: identity, + SubjectType: c.User.IdentityObjectType, + Relation: c.IdentityRelation, + ObjectType: c.User.ObjectType, + ObjectId: userID, + }, nil + default: + return nil, errors.New("invalid identity relation") + } +} diff --git a/common/convert/convert.go b/common/convert/convert.go new file mode 100644 index 0000000..9c84a55 --- /dev/null +++ b/common/convert/convert.go @@ -0,0 +1,206 @@ +package convert + +import ( + "encoding/json" + + "github.com/aserto-dev/ds-load/sdk/common/msg" + "github.com/aserto-dev/ds-load/sdk/transform" + dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" + "github.com/aserto-dev/scim/common/model" + "github.com/elimity-com/scim" + "github.com/elimity-com/scim/optional" + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" +) + +var ( + ErrSourceUserTypeNotSet = errors.New("source user type not set") + ErrSourceGroupTypeNotSet = errors.New("source group type not set") + ErrGroupsNotEnabled = errors.New("groups not enabled") +) + +type Converter struct { + cfg *TransformConfig +} + +func NewConverter(cfg *TransformConfig) *Converter { + return &Converter{cfg: cfg} +} + +func (c *Converter) ObjectToResource(object *dsc.Object, meta scim.Meta) scim.Resource { + eID := optional.String{} + attr := c.ObjectToResourceAttributes(object) + + return scim.Resource{ + ID: object.Id, + ExternalID: eID, + Attributes: attr, + Meta: meta, + } +} + +func (c *Converter) ObjectToResourceAttributes(object *dsc.Object) scim.ResourceAttributes { + attr := object.Properties.AsMap() + delete(attr, "password") + + return attr +} + +func ResourceAttributesToUser(attributes scim.ResourceAttributes) (*model.User, error) { + var user model.User + data, err := json.Marshal(attributes) + if err != nil { + return &model.User{}, err + } + + if err := json.Unmarshal(data, &user); err != nil { + return &model.User{}, err + } + return &user, nil +} + +func ResourceAttributesToGroup(attributes scim.ResourceAttributes) (*model.Group, error) { + var group model.Group + data, err := json.Marshal(attributes) + if err != nil { + return &model.Group{}, err + } + + if err := json.Unmarshal(data, &group); err != nil { + return &model.Group{}, err + } + return &group, nil +} + +func ToResourceAttributes(value interface{}) (scim.ResourceAttributes, error) { + var result scim.ResourceAttributes + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + err = json.Unmarshal(data, &result) + return result, err +} + +func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { + attributes, err := ToResourceAttributes(&user) + if err != nil { + return scim.Resource{}, err + } + eID := optional.String{} + if user.ExternalID != "" { + eID = optional.NewString(user.ExternalID) + } + return scim.Resource{ + ID: user.ID, + ExternalID: eID, + Attributes: attributes, + Meta: meta, + }, nil +} + +func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { + attributes, err := ToResourceAttributes(&user) + if err != nil { + return nil, err + } + delete(attributes, "password") + props, err := structpb.NewStruct(attributes) + if err != nil { + return nil, err + } + + userID := user.ID + if userID == "" { + userID = user.UserName + } + + displayName := user.DisplayName + if displayName == "" { + displayName = userID + } + + sourceUserType := c.cfg.User.SourceObjectType + if sourceUserType == "" { + return nil, ErrSourceUserTypeNotSet + } + + object := &dsc.Object{ + Type: sourceUserType, + Properties: props, + Id: userID, + DisplayName: displayName, + } + return object, nil +} + +func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { + if c.cfg.Group == nil { + return nil, ErrGroupsNotEnabled + } + + attributes, err := ToResourceAttributes(&group) + if err != nil { + return nil, err + } + props, err := structpb.NewStruct(attributes) + if err != nil { + return nil, err + } + + objID := group.ID + if objID == "" { + objID = group.DisplayName + } + + displayName := group.DisplayName + if displayName == "" { + displayName = objID + } + + sourceGroupType := c.cfg.Group.SourceObjectType + if sourceGroupType == "" { + return nil, ErrSourceGroupTypeNotSet + } + + object := &dsc.Object{ + Type: sourceGroupType, + Properties: props, + Id: objID, + DisplayName: displayName, + } + return object, nil +} + +func (c *Converter) TransformResource(resource map[string]interface{}, objType string) (*msg.Transform, error) { + template, err := c.cfg.GetTemplate() + if err != nil { + return nil, err + } + + vars, err := c.cfg.ToTemplateVars() + if err != nil { + return nil, err + } + + transformInput := make(map[string]interface{}) + transformInput["input"] = resource + transformInput["vars"] = vars + transformInput["objectType"] = objType + transformer := transform.NewGoTemplateTransform(template) + return transformer.TransformObject(transformInput) +} + +func ProtobufStructToMap(s *structpb.Struct) (map[string]interface{}, error) { + b, err := protojson.Marshal(s) + if err != nil { + return nil, err + } + m := make(map[string]interface{}) + err = json.Unmarshal(b, &m) + if err != nil { + return nil, err + } + return m, nil +} diff --git a/common/convert/converter_test.go b/common/convert/converter_test.go new file mode 100644 index 0000000..0b25132 --- /dev/null +++ b/common/convert/converter_test.go @@ -0,0 +1,102 @@ +package convert_test + +import ( + "testing" + + "github.com/aserto-dev/scim/common/config" + "github.com/aserto-dev/scim/common/convert" + "github.com/stretchr/testify/require" +) + +var ScimUser map[string]interface{} = map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "foobar", + "name": map[string]interface{}{ + "givenName": "foo", + "familyName": "bar", + }, + "emails": []map[string]interface{}{ + { + "primary": true, + "value": "foo@bar.com", + "type": "work", + }, + }, + "displayName": "foo bar", + "externalId": "fooooo", + "locale": "en-US", + "groups": []interface{}{}, + "active": true, +} + +func TestTransform(t *testing.T) { + assert := require.New(t) + + cfg := config.SCIMConfig{ + User: &config.UserOptions{ + IdentityObjectType: "identity", + IdentityRelation: "identity#identitifier", + ObjectType: "user", + SourceObjectType: "scim:user", + ManagerRelation: "manager", + }, + } + + sCfg, err := convert.NewTransformConfig(&cfg) + assert.NoError(err) + cvt := convert.NewConverter(sCfg) + msg, err := cvt.TransformResource(ScimUser, "user") + assert.NoError(err) + + assert.NotNil(msg) + assert.NotEmpty(msg.Objects) + assert.NotEmpty(msg.Relations) + assert.Len(msg.Relations, 3) + + assert.Equal("foo@bar.com", msg.Relations[1].ObjectId) + assert.Equal("identity", msg.Relations[1].ObjectType) + assert.Equal("identitifier", msg.Relations[1].Relation) + assert.Equal("foobar", msg.Relations[1].SubjectId) + assert.Equal("user", msg.Relations[1].SubjectType) + + assert.Equal("foobar", msg.Relations[0].SubjectId) + assert.Equal("user", msg.Relations[0].SubjectType) + + assert.Equal("fooooo", msg.Relations[2].ObjectId) + assert.Equal("identity", msg.Relations[2].ObjectType) +} + +func TestTransformUserIdentifier(t *testing.T) { + assert := require.New(t) + + cfg := config.SCIMConfig{ + User: &config.UserOptions{ + IdentityObjectType: "identity", + IdentityRelation: "user#identitifier", + ObjectType: "user", + SourceObjectType: "scim:user", + ManagerRelation: "manager", + }, + } + + sCfg, err := convert.NewTransformConfig(&cfg) + assert.NoError(err) + cvt := convert.NewConverter(sCfg) + msg, err := cvt.TransformResource(ScimUser, "user") + assert.NoError(err) + + assert.NotNil(msg) + assert.NotEmpty(msg.Objects) + assert.NotEmpty(msg.Relations) + assert.Equal("foo@bar.com", msg.Relations[1].SubjectId) + assert.Equal("identity", msg.Relations[1].SubjectType) + assert.Equal("identitifier", msg.Relations[1].Relation) + assert.Equal("foobar", msg.Relations[1].ObjectId) + assert.Equal("user", msg.Relations[1].ObjectType) + + assert.Equal("foobar", msg.Relations[0].ObjectId) + assert.Equal("user", msg.Relations[0].ObjectType) + + assert.Equal("fooooo", msg.Relations[2].SubjectId) + assert.Equal("identity", msg.Relations[2].SubjectType) +} diff --git a/common/directory/client.go b/common/directory/client.go new file mode 100644 index 0000000..8dedbce --- /dev/null +++ b/common/directory/client.go @@ -0,0 +1,318 @@ +package directory + +import ( + "context" + "errors" + "slices" + + "github.com/aserto-dev/ds-load/sdk/common/msg" + cerr "github.com/aserto-dev/errors" + "github.com/aserto-dev/go-aserto/ds/v3" + dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" + dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" + dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" + "github.com/aserto-dev/go-directory/pkg/derr" + "github.com/aserto-dev/scim/common/convert" + "github.com/elimity-com/scim" + serrors "github.com/elimity-com/scim/errors" + "github.com/rs/zerolog" + "google.golang.org/protobuf/types/known/structpb" +) + +type Client struct { + cfg *convert.TransformConfig + client *ds.Client + logger *zerolog.Logger +} + +func NewDirectoryClient(transformCfg *convert.TransformConfig, logger *zerolog.Logger, dirClient *ds.Client) *Client { + return &Client{ + cfg: transformCfg, + logger: logger, + client: dirClient, + } +} + +func (s *Client) DS() *ds.Client { + return s.client +} + +func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform, userAttributes scim.ResourceAttributes) (scim.Meta, error) { + logger := s.logger.With().Str("method", "SetUser").Str("id", userID).Logger() + logger.Trace().Msg("set user") + relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ + ObjectType: s.cfg.User.ObjectType, + ObjectId: userID, + Relation: s.cfg.User.IdentityRelation, + WithObjects: false, + WithEmptySubjectRelation: true, + }) + if err != nil && !errors.Is(cerr.UnwrapAsertoError(err), derr.ErrRelationNotFound) { + return scim.Meta{}, err + } + + result, addedIdentities, err := s.importObjects(ctx, data.Objects, userAttributes) + if err != nil { + return result, err + } + + logger.Trace().Any("identities", addedIdentities).Msg("added identities") + + for _, relation := range data.Relations { + logger.Trace().Any("relation", relation).Msg("setting relation") + _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ + Relation: relation, + }) + if err != nil { + return result, err + } + } + + if relations != nil { + for _, rel := range relations.Results { + if !slices.Contains(addedIdentities, rel.ObjectId) { + logger.Trace().Str("id", rel.ObjectId).Msg("deleting identity") + _, err := s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectType: s.cfg.User.IdentityObjectType, + ObjectId: rel.ObjectId, + WithRelations: true, + }) + if err != nil { + return result, err + } + } + } + } + + return result, nil +} + +func (s *Client) importObjects(ctx context.Context, objects []*dsc.Object, userAttributes scim.ResourceAttributes) (scim.Meta, []string, error) { + var err error + result := scim.Meta{} + addedIdentities := make([]string, 0) + + for _, object := range objects { + if object.Type == s.cfg.User.ObjectType { + var userProperties map[string]interface{} + if object.Properties == nil { + userProperties = make(map[string]interface{}) + } else { + userProperties = object.Properties.AsMap() + } + for key, value := range s.cfg.User.PropertyMapping { + userProperties[key] = userAttributes[value] + } + object.Properties, err = structpb.NewStruct(userProperties) + if err != nil { + return result, addedIdentities, err + } + } + resp, err := s.client.Writer.SetObject(ctx, &dsw.SetObjectRequest{ + Object: object, + }) + if err != nil { + if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrAlreadyExists) { + return result, addedIdentities, serrors.ScimErrorUniqueness + } + return result, addedIdentities, err + } + + if resp.Result.Type == s.cfg.User.IdentityObjectType { + addedIdentities = append(addedIdentities, resp.Result.Id) + } + + if object.Type == s.cfg.User.ObjectType { + err = s.setRelations(ctx, resp.Result.Id, resp.Result.Type) + if err != nil { + return result, addedIdentities, err + } + + createdAt := resp.Result.CreatedAt.AsTime() + updatedAt := resp.Result.UpdatedAt.AsTime() + result.Created = &createdAt + result.LastModified = &updatedAt + result.Version = resp.Result.Etag + } + } + + return result, addedIdentities, nil +} + +func (s *Client) DeleteUser(ctx context.Context, userID string) error { + logger := s.logger.With().Str("method", "DeleteUser").Str("id", userID).Logger() + logger.Trace().Msg("delete user") + identityRelation, err := s.cfg.GetIdentityRelation(userID, "") + if err != nil { + return err + } + + relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ + SubjectType: identityRelation.SubjectType, + SubjectId: identityRelation.SubjectId, + ObjectType: identityRelation.ObjectType, + ObjectId: identityRelation.ObjectId, + Relation: identityRelation.Relation, + }) + if err != nil { + if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrNotFound) { + return serrors.ScimErrorResourceNotFound(userID) + } + } + + for _, v := range relations.Results { + var objectID string + switch v.ObjectType { + case s.cfg.User.IdentityObjectType: + objectID = v.ObjectId + case s.cfg.User.ObjectType: + objectID = v.SubjectId + default: + return serrors.ScimErrorBadRequest("unexpected object type in identity relation") + } + + logger.Trace().Str("id", v.ObjectId).Msg("deleting identity") + _, err = s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectId: objectID, + ObjectType: s.cfg.User.IdentityObjectType, + WithRelations: true, + }) + if err != nil { + return err + } + } + + logger.Trace().Msg("deleting user") + _, err = s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectType: s.cfg.User.ObjectType, + ObjectId: userID, + WithRelations: true, + }) + + return err +} + +func (s *Client) SetGroup(ctx context.Context, groupID string, data *msg.Transform) (scim.Meta, error) { + logger := s.logger.With().Str("method", "SetGroup").Str("id", groupID).Logger() + logger.Trace().Msg("set group") + if s.cfg.Group == nil { + logger.Warn().Msg("groups not enabled") + return scim.Meta{}, nil + } + + relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ + ObjectType: s.cfg.Group.ObjectType, + ObjectId: groupID, + Relation: s.cfg.Group.GroupMemberRelation, + WithObjects: false, + WithEmptySubjectRelation: true, + }) + if err != nil && !errors.Is(cerr.UnwrapAsertoError(err), derr.ErrRelationNotFound) { + return scim.Meta{}, err + } + + addedMembers := make([]string, 0) + + result := scim.Meta{} + for _, object := range data.Objects { + logger.Trace().Any("object", object).Msg("setting object") + resp, err := s.client.Writer.SetObject(ctx, &dsw.SetObjectRequest{ + Object: object, + }) + if err != nil { + if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrAlreadyExists) { + return result, serrors.ScimErrorUniqueness + } + return result, err + } + + if object.Type == s.cfg.Group.ObjectType { + err = s.setRelations(ctx, resp.Result.Id, resp.Result.Type) + if err != nil { + return result, err + } + + createdAt := resp.Result.CreatedAt.AsTime() + updatedAt := resp.Result.UpdatedAt.AsTime() + result.Created = &createdAt + result.LastModified = &updatedAt + result.Version = resp.Result.Etag + } + } + + for _, relation := range data.Relations { + if relation.Relation == s.cfg.Group.GroupMemberRelation { + addedMembers = append(addedMembers, relation.SubjectId) + } + logger.Trace().Any("relation", relation).Msg("setting relation") + _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ + Relation: relation, + }) + if err != nil { + return result, err + } + } + + if relations != nil { + for _, rel := range relations.Results { + if !slices.Contains(addedMembers, rel.SubjectId) { + logger.Trace().Str("id", rel.SubjectId).Msg("deleting relation") + _, err := s.client.Writer.DeleteRelation(ctx, &dsw.DeleteRelationRequest{ + ObjectType: s.cfg.Group.ObjectType, + ObjectId: groupID, + Relation: s.cfg.Group.GroupMemberRelation, + SubjectId: rel.SubjectId, + SubjectType: rel.SubjectType, + }) + if err != nil { + return result, err + } + } + } + } + + return result, nil +} + +func (s *Client) DeleteGroup(ctx context.Context, groupID string) error { + logger := s.logger.With().Str("method", "DeleteGroup").Str("id", groupID).Logger() + logger.Trace().Msg("delete group") + _, err := s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectType: s.cfg.Group.SourceObjectType, + ObjectId: groupID, + WithRelations: true, + }) + + if err != nil { + return err + } + + _, err = s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectType: s.cfg.Group.ObjectType, + ObjectId: groupID, + WithRelations: true, + }) + + return err +} + +func (s *Client) setRelations(ctx context.Context, subjID, subjType string) error { + for _, userMap := range s.cfg.Relations { + if userMap.SubjectID == subjID && userMap.SubjectType == subjType { + _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ + Relation: &dsc.Relation{ + SubjectType: userMap.SubjectType, + SubjectId: userMap.SubjectID, + Relation: userMap.Relation, + ObjectType: userMap.ObjectType, + ObjectId: userMap.ObjectID, + SubjectRelation: userMap.SubjectRelation, + }, + }) + if err != nil { + return err + } + } + } + return nil +} diff --git a/common/go.mod b/common/go.mod new file mode 100644 index 0000000..6f4bb97 --- /dev/null +++ b/common/go.mod @@ -0,0 +1,51 @@ +module github.com/aserto-dev/scim/common + +go 1.23.7 + +require ( + github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3 + github.com/aserto-dev/errors v0.0.15 + github.com/aserto-dev/go-aserto v0.33.7 + github.com/aserto-dev/go-directory v0.33.9 + github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.34.0 + github.com/scim2/filter-parser/v2 v2.2.0 + github.com/stretchr/testify v1.10.0 + google.golang.org/protobuf v1.36.6 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/aserto-dev/header v0.0.10 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/di-wu/parser v0.3.0 // indirect + github.com/di-wu/xsd-datetime v1.0.0 // indirect + github.com/dongri/phonenumber v0.1.12 // indirect + github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/grpc v1.71.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/common/go.sum b/common/go.sum new file mode 100644 index 0000000..713b768 --- /dev/null +++ b/common/go.sum @@ -0,0 +1,126 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3 h1:qyQXzQFa+0ocXnFeMhjOFfzZZ5opFt1m7e3Ln7Mq2E8= +github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3/go.mod h1:QAQ+JlEH011NOsZ8uBGFPDoixZkfM1WQigK6PTaFSVA= +github.com/aserto-dev/errors v0.0.15 h1:Mx/k7HITit+Istq8YLatiydEIbff39RXf3fW/PlKSwo= +github.com/aserto-dev/errors v0.0.15/go.mod h1:WntQkFRb4j41tp4ObRXTdhu/VZKIzIRTReLHjLLMWyc= +github.com/aserto-dev/go-aserto v0.33.7 h1:oLAj2nu4YKJ7q6pUFpABOQuY/70YfF5B89T0RkKSV8k= +github.com/aserto-dev/go-aserto v0.33.7/go.mod h1:R4Bo3Tgn2KnyvmeyW+gID7pBqaRcnqnib36DiLjcjiw= +github.com/aserto-dev/go-directory v0.33.9 h1:JxsNVRfjpRr0gtpzCTn64D74gUkbj/qdXYFFM7VaP58= +github.com/aserto-dev/go-directory v0.33.9/go.mod h1:jiKmqzVQ0eYnn4CrIp6bFneTOtOFcCthspo287BnWlg= +github.com/aserto-dev/header v0.0.10 h1:H6sz3F4pfv53FuyGNoZlRNHpAcOonTioQMnWRowyigU= +github.com/aserto-dev/header v0.0.10/go.mod h1:N3+nmX6nXmM9gI8VsGXOujPW6aW/8aEFa7dSu0FRerY= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/di-wu/parser v0.2.2/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo= +github.com/di-wu/parser v0.3.0 h1:NMOvy5ifswgt4gsdhySVcKOQtvjC43cHZIfViWctqQY= +github.com/di-wu/parser v0.3.0/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo= +github.com/di-wu/xsd-datetime v1.0.0 h1:vZoGNkbzpBNoc+JyfVLEbutNDNydYV8XwHeV7eUJoxI= +github.com/di-wu/xsd-datetime v1.0.0/go.mod h1:i3iEhrP3WchwseOBeIdW/zxeoleXTOzx1WyDXgdmOww= +github.com/dongri/phonenumber v0.1.12 h1:rR/4VZzxqpocUdyM4dIdfY0TWd8FcW43oiyPaOUxNIk= +github.com/dongri/phonenumber v0.1.12/go.mod h1:cuHFSstIxh6qh/Qs/SCV3Grb/JMYregBLuXELvSYmT4= +github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 h1:0+BTyxIYgiVAry/P5s8R4dYuLkhB9Nhso8ogFWNr4IQ= +github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= +github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM= +github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/common/handlers/groups/create.go b/common/handlers/groups/create.go new file mode 100644 index 0000000..60dc497 --- /dev/null +++ b/common/handlers/groups/create.go @@ -0,0 +1,61 @@ +package groups + +import ( + "context" + + dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" + "github.com/aserto-dev/scim/common/convert" + "github.com/elimity-com/scim" + serrors "github.com/elimity-com/scim/errors" +) + +func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.ResourceAttributes) (scim.Resource, error) { + groupName, ok := attributes["displayName"].(string) + if !ok { + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + logger := g.logger.With().Str("method", "Create").Str("name", groupName).Logger() + logger.Info().Msg("create group") + logger.Trace().Any("attributes", attributes).Msg("creating group") + + group, err := convert.ResourceAttributesToGroup(attributes) + if err != nil { + logger.Error().Err(err).Msg("failed to convert attributes to group") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + var result scim.Resource + + converter := convert.NewConverter(g.cfg) + object, err := converter.SCIMGroupToObject(group) + if err != nil { + logger.Error().Err(err).Msg("failed to convert group to object") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + sourceGroupResp, err := g.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ + Object: object, + }) + if err != nil { + logger.Error().Err(err).Msg("failed to create group") + return scim.Resource{}, err + } + + transformResult, err := converter.TransformResource(attributes, "group") + if err != nil { + logger.Error().Err(err).Msg("failed to transform group") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + meta, err := g.dirClient.SetGroup(ctx, sourceGroupResp.Result.Id, transformResult) + if err != nil { + logger.Error().Err(err).Msg("failed to sync group") + return scim.Resource{}, err + } + + result = converter.ObjectToResource(sourceGroupResp.Result, meta) + + logger.Trace().Any("response", result).Msg("group created") + + return result, nil +} diff --git a/common/handlers/groups/delete.go b/common/handlers/groups/delete.go new file mode 100644 index 0000000..3040d95 --- /dev/null +++ b/common/handlers/groups/delete.go @@ -0,0 +1,16 @@ +package groups + +import "context" + +func (g GroupResourceHandler) Delete(ctx context.Context, id string) error { + logger := g.logger.With().Str("method", "Delete").Str("id", id).Logger() + logger.Info().Msg("delete group") + + err := g.dirClient.DeleteGroup(ctx, id) + if err != nil { + logger.Err(err).Msg("failed to delete group") + return err + } + + return nil +} diff --git a/common/handlers/groups/get.go b/common/handlers/groups/get.go new file mode 100644 index 0000000..9af6843 --- /dev/null +++ b/common/handlers/groups/get.go @@ -0,0 +1,88 @@ +package groups + +import ( + "context" + + dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" + dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" + "github.com/aserto-dev/scim/common/convert" + "github.com/elimity-com/scim" + serrors "github.com/elimity-com/scim/errors" +) + +func (g GroupResourceHandler) Get(ctx context.Context, id string) (scim.Resource, error) { + logger := g.logger.With().Str("method", "Get").Str("id", id).Logger() + logger.Info().Msg("get group") + + if !g.cfg.Groups() { + logger.Error().Msg("groups not enabled") + return scim.Resource{}, serrors.ScimErrorBadRequest("groups not enabled") + } + + resp, err := g.dirClient.DS().Reader.GetObject(ctx, &dsr.GetObjectRequest{ + ObjectType: g.cfg.Group.SourceObjectType, + ObjectId: id, + WithRelations: false, + }) + if err != nil { + logger.Err(err).Msg("failed to get group") + return scim.Resource{}, err + } + + converter := convert.NewConverter(g.cfg) + + createdAt := resp.Result.CreatedAt.AsTime() + updatedAt := resp.Result.UpdatedAt.AsTime() + resource := converter.ObjectToResource(resp.Result, scim.Meta{ + Created: &createdAt, + LastModified: &updatedAt, + Version: resp.Result.Etag, + }) + + return resource, nil +} + +func (g GroupResourceHandler) GetAll(ctx context.Context, params scim.ListRequestParams) (scim.Page, error) { + logger := g.logger.With().Str("method", "GetAll").Logger() + logger.Info().Msg("getting all groups") + + var ( + resources = make([]scim.Resource, 0) + ) + + if !g.cfg.Groups() { + logger.Error().Msg("groups not enabled") + return scim.Page{}, serrors.ScimErrorBadRequest("groups not enabled") + } + + resp, err := g.dirClient.DS().Reader.GetObjects(ctx, &dsr.GetObjectsRequest{ + ObjectType: g.cfg.Group.SourceObjectType, + Page: &dsc.PaginationRequest{ + Size: int32(params.Count), //nolint:gosec + }, + }) + if err != nil { + logger.Err(err).Msg("failed to read groups") + return scim.Page{}, err + } + + converter := convert.NewConverter(g.cfg) + + for _, v := range resp.Results { + createdAt := v.CreatedAt.AsTime() + updatedAt := v.UpdatedAt.AsTime() + resource := converter.ObjectToResource(v, scim.Meta{ + Created: &createdAt, + LastModified: &updatedAt, + Version: v.Etag, + }) + resources = append(resources, resource) + } + + logger.Trace().Int("total_results", len(resources)).Msg("groups read") + + return scim.Page{ + TotalResults: len(resources), + Resources: resources, + }, nil +} diff --git a/common/handlers/groups/handler.go b/common/handlers/groups/handler.go new file mode 100644 index 0000000..2145c23 --- /dev/null +++ b/common/handlers/groups/handler.go @@ -0,0 +1,28 @@ +package groups + +import ( + "github.com/aserto-dev/go-aserto/ds/v3" + "github.com/aserto-dev/scim/common/convert" + "github.com/aserto-dev/scim/common/directory" + "github.com/rs/zerolog" +) + +type GroupResourceHandler struct { + cfg *convert.TransformConfig + logger *zerolog.Logger + dirClient *directory.Client +} + +func NewGroupResourceHandler(logger *zerolog.Logger, + cfg *convert.TransformConfig, + dsClient *ds.Client, +) (*GroupResourceHandler, error) { + groupLogger := logger.With().Str("component", "groups-handler").Logger() + dirClient := directory.NewDirectoryClient(cfg, &groupLogger, dsClient) + + return &GroupResourceHandler{ + cfg: cfg, + logger: &groupLogger, + dirClient: dirClient, + }, nil +} diff --git a/common/handlers/groups/patch.go b/common/handlers/groups/patch.go new file mode 100644 index 0000000..5c7e8cc --- /dev/null +++ b/common/handlers/groups/patch.go @@ -0,0 +1,99 @@ +package groups + +import ( + "context" + + cerr "github.com/aserto-dev/errors" + dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" + dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" + "github.com/aserto-dev/go-directory/pkg/derr" + "github.com/aserto-dev/scim/common" + "github.com/aserto-dev/scim/common/convert" + "github.com/elimity-com/scim" + serrors "github.com/elimity-com/scim/errors" + "github.com/pkg/errors" + structpb "google.golang.org/protobuf/types/known/structpb" +) + +func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations []scim.PatchOperation) (scim.Resource, error) { + logger := g.logger.With().Str("method", "Patch").Str("id", id).Logger() + logger.Info().Msg("patch group") + + if !g.cfg.Groups() { + logger.Error().Msg("groups not enabled") + return scim.Resource{}, serrors.ScimErrorBadRequest("groups not enabled") + } + + getObjResp, err := g.dirClient.DS().Reader.GetObject(ctx, &dsr.GetObjectRequest{ + ObjectType: g.cfg.Group.SourceObjectType, + ObjectId: id, + WithRelations: false, + }) + if err != nil { + logger.Err(err).Msg("failed to get group") + if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) + } + return scim.Resource{}, err + } + + converter := convert.NewConverter(g.cfg) + var attr scim.ResourceAttributes + oldAttr := converter.ObjectToResourceAttributes(getObjResp.Result) + + for _, op := range operations { + switch op.Op { + case scim.PatchOperationAdd: + attr, err = common.HandlePatchOPAdd(oldAttr, op) + if err != nil { + logger.Err(err).Msg("error adding property") + return scim.Resource{}, err + } + case scim.PatchOperationRemove: + attr, err = common.HandlePatchOPRemove(oldAttr, op) + if err != nil { + logger.Err(err).Msg("error removing property") + return scim.Resource{}, err + } + case scim.PatchOperationReplace: + attr, err = common.HandlePatchOPReplace(oldAttr, op) + if err != nil { + logger.Err(err).Msg("error replacing property") + return scim.Resource{}, err + } + } + } + + transformResult, err := converter.TransformResource(attr, "group") + if err != nil { + logger.Err(err).Msg("failed to convert group to object") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + groupObj := getObjResp.Result + props, err := structpb.NewStruct(attr) + if err != nil { + logger.Err(err).Msg("failed to convert attributes to struct") + return scim.Resource{}, err + } + groupObj.Properties = props + sourceGroupResp, err := g.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ + Object: groupObj, + }) + if err != nil { + logger.Err(err).Msg("failed to replace group") + return scim.Resource{}, err + } + + meta, err := g.dirClient.SetGroup(ctx, getObjResp.Result.Id, transformResult) + if err != nil { + logger.Err(err).Msg("failed to sync group") + return scim.Resource{}, err + } + + resource := converter.ObjectToResource(sourceGroupResp.Result, meta) + + logger.Trace().Any("response", resource).Msg("group patched") + + return resource, nil +} diff --git a/common/handlers/groups/replace.go b/common/handlers/groups/replace.go new file mode 100644 index 0000000..4df6d1f --- /dev/null +++ b/common/handlers/groups/replace.go @@ -0,0 +1,27 @@ +package groups + +import ( + "context" + + "github.com/elimity-com/scim" +) + +func (g GroupResourceHandler) Replace(ctx context.Context, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { + logger := g.logger.With().Str("method", "Replace").Str("id", id).Logger() + logger.Info().Msg("replace group") + + err := g.Delete(ctx, id) + if err != nil { + logger.Error().Err(err).Msg("failed to delete group") + return scim.Resource{}, err + } + + resource, err := g.Create(ctx, attributes) + if err != nil { + logger.Error().Err(err).Msg("failed to create group") + return scim.Resource{}, err + } + + logger.Trace().Any("resource", resource).Msg("group replaced") + return resource, nil +} diff --git a/common/handlers/handler.go b/common/handlers/handler.go new file mode 100644 index 0000000..8609163 --- /dev/null +++ b/common/handlers/handler.go @@ -0,0 +1,16 @@ +package handlers + +import ( + "context" + + "github.com/elimity-com/scim" +) + +type ResourceHandler interface { + Create(ctx context.Context, attributes scim.ResourceAttributes) (scim.Resource, error) + Get(ctx context.Context, id string) (scim.Resource, error) + GetAll(ctx context.Context, params scim.ListRequestParams) (scim.Page, error) + Patch(ctx context.Context, id string, operations []scim.PatchOperation) (scim.Resource, error) + Replace(ctx context.Context, id string, attributes scim.ResourceAttributes) (scim.Resource, error) + Delete(ctx context.Context, id string) error +} diff --git a/common/handlers/users/create.go b/common/handlers/users/create.go new file mode 100644 index 0000000..32560cd --- /dev/null +++ b/common/handlers/users/create.go @@ -0,0 +1,65 @@ +package users + +import ( + "context" + + dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" + "github.com/aserto-dev/scim/common/convert" + "github.com/elimity-com/scim" + serrors "github.com/elimity-com/scim/errors" +) + +func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.ResourceAttributes) (scim.Resource, error) { + userName, ok := attributes["userName"].(string) + if !ok { + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + logger := u.logger.With().Str("method", "Create").Str("userName", userName).Logger() + logger.Info().Msg("create user") + logger.Trace().Any("attributes", attributes).Msg("creating user") + user, err := convert.ResourceAttributesToUser(attributes) + if err != nil { + logger.Error().Err(err).Msg("failed to convert attributes to user") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + var result scim.Resource + + converter := convert.NewConverter(u.cfg) + object, err := converter.SCIMUserToObject(user) + if err != nil { + logger.Err(err).Msg("failed to convert user to object") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + sourceUserResp, err := u.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ + Object: object, + }) + if err != nil { + logger.Err(err).Msg("failed to create user") + return scim.Resource{}, err + } + + userMap, err := convert.ProtobufStructToMap(sourceUserResp.Result.Properties) + if err != nil { + logger.Error().Err(err).Msg("failed to convert user to map") + return scim.Resource{}, err + } + + transformResult, err := converter.TransformResource(userMap, "user") + if err != nil { + logger.Error().Err(err).Msg("failed to convert user to object") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + meta, err := u.dirClient.SetUser(ctx, sourceUserResp.Result.Id, transformResult, attributes) + if err != nil { + logger.Err(err).Msg("failed to sync user") + return scim.Resource{}, err + } + + result = converter.ObjectToResource(sourceUserResp.Result, meta) + + logger.Trace().Any("response", result).Msg("user created") + + return result, nil +} diff --git a/pkg/app/handlers/users/delete.go b/common/handlers/users/delete.go similarity index 56% rename from pkg/app/handlers/users/delete.go rename to common/handlers/users/delete.go index 06f4d68..9493764 100644 --- a/pkg/app/handlers/users/delete.go +++ b/common/handlers/users/delete.go @@ -1,10 +1,9 @@ package users import ( - "net/http" + "context" cerr "github.com/aserto-dev/errors" - dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" "github.com/aserto-dev/go-directory/pkg/derr" @@ -12,50 +11,48 @@ import ( "github.com/pkg/errors" ) -func (u UsersResourceHandler) Delete(r *http.Request, id string) error { +func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { logger := u.logger.With().Str("method", "Delete").Str("id", id).Logger() logger.Info().Msg("delete user") - identityRelation, err := u.getIdentityRelation(id, "") + identityRelation, err := u.cfg.GetIdentityRelation(id, "") if err != nil { - u.logger.Err(err).Msg("failed to get identity relation") - return err + logger.Err(err).Msg("failed to get identity relation") } - var identities []*dsc.Relation - - resp, err := u.dirClient.Reader.GetRelations(r.Context(), &dsr.GetRelationsRequest{ + resp, err := u.dirClient.DS().Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ SubjectType: identityRelation.SubjectType, SubjectId: identityRelation.SubjectId, - Relation: identityRelation.Relation, - ObjectId: identityRelation.ObjectId, ObjectType: identityRelation.ObjectType, + ObjectId: identityRelation.ObjectId, + Relation: identityRelation.Relation, }) if err != nil { - logger.Err(err).Msg("failed to get identities") + logger.Err(err).Msg("failed to get relations") if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { return serrors.ScimErrorResourceNotFound(id) } return err } - identities = resp.Results + + identities := resp.Results for _, v := range identities { var objectID string switch v.ObjectType { - case u.cfg.SCIM.IdentityObjectType: + case u.cfg.User.IdentityObjectType: objectID = v.ObjectId - case u.cfg.SCIM.UserObjectType: + case u.cfg.User.ObjectType: objectID = v.SubjectId default: logger.Error().Str("object_type", v.ObjectType).Msg("unexpected object type") return serrors.ScimErrorBadRequest("unexpected object type in identity relation") } - logger.Trace().Str("identity", objectID).Msg("deleting identity") - _, err = u.dirClient.Writer.DeleteObject(r.Context(), &dsw.DeleteObjectRequest{ + logger.Trace().Str("id", v.ObjectId).Msg("deleting identity") + _, err = u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectId: objectID, - ObjectType: u.cfg.SCIM.IdentityObjectType, + ObjectType: u.cfg.User.IdentityObjectType, WithRelations: true, }) if err != nil { @@ -64,8 +61,9 @@ func (u UsersResourceHandler) Delete(r *http.Request, id string) error { } } - _, err = u.dirClient.Writer.DeleteObject(r.Context(), &dsw.DeleteObjectRequest{ - ObjectType: u.cfg.SCIM.UserObjectType, + logger.Trace().Msg("deleting user") + _, err = u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectType: u.cfg.User.ObjectType, ObjectId: id, WithRelations: true, }) @@ -76,7 +74,19 @@ func (u UsersResourceHandler) Delete(r *http.Request, id string) error { } } - logger.Trace().Msg("user deleted") + logger.Trace().Msg("deleting user source object") + _, err = u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectType: u.cfg.User.SourceObjectType, + ObjectId: id, + WithRelations: true, + }) + if err != nil { + logger.Err(err).Msg("failed to delete user source object") + if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + return serrors.ScimErrorResourceNotFound(id) + } + } + logger.Trace().Msg("user deleted") return err } diff --git a/pkg/app/handlers/users/get.go b/common/handlers/users/get.go similarity index 73% rename from pkg/app/handlers/users/get.go rename to common/handlers/users/get.go index 5d2d80d..395a6ee 100644 --- a/pkg/app/handlers/users/get.go +++ b/common/handlers/users/get.go @@ -2,28 +2,30 @@ package users import ( "context" - "net/http" cerr "github.com/aserto-dev/errors" dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" "github.com/aserto-dev/go-directory/pkg/derr" - "github.com/aserto-dev/scim/pkg/common" + "github.com/aserto-dev/scim/common/convert" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" "github.com/pkg/errors" ) -func (u UsersResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) { +func (u UsersResourceHandler) Get(ctx context.Context, id string) (scim.Resource, error) { logger := u.logger.With().Str("method", "Get").Str("id", id).Logger() logger.Info().Msg("get user") - resp, err := u.dirClient.Reader.GetObject(r.Context(), &dsr.GetObjectRequest{ - ObjectType: u.cfg.SCIM.UserObjectType, + + converter := convert.NewConverter(u.cfg) + + resp, err := u.dirClient.DS().Reader.GetObject(ctx, &dsr.GetObjectRequest{ + ObjectType: u.cfg.User.SourceObjectType, ObjectId: id, - WithRelations: true, + WithRelations: false, }) if err != nil { - logger.Err(err).Str("id", id).Msg("failed to get user") + logger.Err(err).Msg("failed to get user") if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } @@ -32,7 +34,7 @@ func (u UsersResourceHandler) Get(r *http.Request, id string) (scim.Resource, er createdAt := resp.Result.CreatedAt.AsTime() updatedAt := resp.Result.UpdatedAt.AsTime() - resource := common.ObjectToResource(resp.Result, scim.Meta{ + resource := converter.ObjectToResource(resp.Result, scim.Meta{ Created: &createdAt, LastModified: &updatedAt, Version: resp.Result.Etag, @@ -43,9 +45,9 @@ func (u UsersResourceHandler) Get(r *http.Request, id string) (scim.Resource, er return resource, nil } -func (u UsersResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { +func (u UsersResourceHandler) GetAll(ctx context.Context, params scim.ListRequestParams) (scim.Page, error) { logger := u.logger.With().Str("method", "GetAll").Logger() - logger.Info().Msg("getall users") + logger.Info().Msg("getting all users") var ( resources = make([]scim.Resource, 0) @@ -58,8 +60,10 @@ func (u UsersResourceHandler) GetAll(r *http.Request, params scim.ListRequestPar pageSize = params.Count } + converter := convert.NewConverter(u.cfg) + for { - resp, err := u.getUsers(r.Context(), pageSize, pageToken) + resp, err := u.getUsers(ctx, pageSize, pageToken) if err != nil { logger.Err(err).Msg("failed to get users") return scim.Page{}, err @@ -70,7 +74,7 @@ func (u UsersResourceHandler) GetAll(r *http.Request, params scim.ListRequestPar for _, v := range resp.Results { createdAt := v.CreatedAt.AsTime() updatedAt := v.UpdatedAt.AsTime() - resource := common.ObjectToResource(v, scim.Meta{ + resource := converter.ObjectToResource(v, scim.Meta{ Created: &createdAt, LastModified: &updatedAt, Version: v.Etag, @@ -103,8 +107,8 @@ func (u UsersResourceHandler) GetAll(r *http.Request, params scim.ListRequestPar } func (u UsersResourceHandler) getUsers(ctx context.Context, count int, pageToken string) (*dsr.GetObjectsResponse, error) { - return u.dirClient.Reader.GetObjects(ctx, &dsr.GetObjectsRequest{ - ObjectType: u.cfg.SCIM.UserObjectType, + return u.dirClient.DS().Reader.GetObjects(ctx, &dsr.GetObjectsRequest{ + ObjectType: u.cfg.User.SourceObjectType, Page: &dsc.PaginationRequest{ Size: int32(count), //nolint:gosec Token: pageToken, diff --git a/common/handlers/users/handler.go b/common/handlers/users/handler.go new file mode 100644 index 0000000..d8ed947 --- /dev/null +++ b/common/handlers/users/handler.go @@ -0,0 +1,30 @@ +package users + +import ( + "github.com/aserto-dev/go-aserto/ds/v3" + "github.com/aserto-dev/scim/common/convert" + "github.com/aserto-dev/scim/common/directory" + + "github.com/rs/zerolog" +) + +type UsersResourceHandler struct { + cfg *convert.TransformConfig + logger *zerolog.Logger + dirClient *directory.Client +} + +func NewUsersResourceHandler(logger *zerolog.Logger, + cfg *convert.TransformConfig, + dsClient *ds.Client, +) (*UsersResourceHandler, error) { + usersLogger := logger.With().Str("component", "users-handler").Logger() + + dirClient := directory.NewDirectoryClient(cfg, &usersLogger, dsClient) + + return &UsersResourceHandler{ + cfg: cfg, + logger: &usersLogger, + dirClient: dirClient, + }, nil +} diff --git a/common/handlers/users/patch.go b/common/handlers/users/patch.go new file mode 100644 index 0000000..730eb1a --- /dev/null +++ b/common/handlers/users/patch.go @@ -0,0 +1,101 @@ +package users + +import ( + "context" + + cerr "github.com/aserto-dev/errors" + dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" + dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" + "github.com/aserto-dev/go-directory/pkg/derr" + "github.com/aserto-dev/scim/common" + "github.com/aserto-dev/scim/common/convert" + "github.com/elimity-com/scim" + serrors "github.com/elimity-com/scim/errors" + "github.com/pkg/errors" + "google.golang.org/protobuf/types/known/structpb" +) + +func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations []scim.PatchOperation) (scim.Resource, error) { + logger := u.logger.With().Str("method", "Patch").Str("id", id).Logger() + logger.Info().Msg("patch user") + logger.Trace().Any("operations", operations).Msg("patching user") + + converter := convert.NewConverter(u.cfg) + + getObjResp, err := u.dirClient.DS().Reader.GetObject(ctx, &dsr.GetObjectRequest{ + ObjectType: u.cfg.User.SourceObjectType, + ObjectId: id, + WithRelations: false, + }) + if err != nil { + logger.Error().Err(err).Msg("failed to get user") + if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) + } + return scim.Resource{}, err + } + + var attr scim.ResourceAttributes + oldAttr := converter.ObjectToResourceAttributes(getObjResp.Result) + + for _, op := range operations { + switch op.Op { + case scim.PatchOperationAdd: + attr, err = common.HandlePatchOPAdd(oldAttr, op) + if err != nil { + logger.Err(err).Msg("error adding property") + return scim.Resource{}, err + } + case scim.PatchOperationRemove: + attr, err = common.HandlePatchOPRemove(oldAttr, op) + if err != nil { + logger.Err(err).Msg("error removing property") + return scim.Resource{}, err + } + case scim.PatchOperationReplace: + attr, err = common.HandlePatchOPReplace(oldAttr, op) + if err != nil { + logger.Err(err).Msg("error replacing property") + return scim.Resource{}, err + } + } + } + + if err != nil { + logger.Err(err).Msg("error handling patch operation") + return scim.Resource{}, err + } + + transformResult, err := converter.TransformResource(attr, "user") + if err != nil { + logger.Err(err).Msg("failed to convert user to object") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + userObj := getObjResp.Result + props, err := structpb.NewStruct(attr) + if err != nil { + logger.Err(err).Msg("failed to convert resource attributes to struct") + return scim.Resource{}, err + } + userObj.Properties = props + sourceUserResp, err := u.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ + Object: userObj, + }) + if err != nil { + logger.Err(err).Msg("failed to replace user") + return scim.Resource{}, err + } + + meta, err := u.dirClient.SetUser(ctx, getObjResp.Result.Id, transformResult, attr) + if err != nil { + logger.Err(err).Msg("failed to sync user") + return scim.Resource{}, err + } + + resource := converter.ObjectToResource(sourceUserResp.Result, meta) + + logger.Trace().Any("response", resource).Msg("user patched") + + return resource, nil +} diff --git a/common/handlers/users/replace.go b/common/handlers/users/replace.go new file mode 100644 index 0000000..7f888ac --- /dev/null +++ b/common/handlers/users/replace.go @@ -0,0 +1,29 @@ +package users + +import ( + "context" + + "github.com/elimity-com/scim" +) + +func (u UsersResourceHandler) Replace(ctx context.Context, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { + logger := u.logger.With().Str("method", "Replace").Str("id", id).Logger() + logger.Info().Msg("replace user") + u.logger.Trace().Any("attributes", attributes).Msg("replacing user") + + err := u.Delete(ctx, id) + if err != nil { + logger.Err(err).Msg("failed to delete user") + return scim.Resource{}, err + } + + resource, err := u.Create(ctx, attributes) + if err != nil { + logger.Err(err).Msg("failed to create user") + return scim.Resource{}, err + } + + logger.Trace().Any("resource", resource).Msg("user replaced") + + return resource, nil +} diff --git a/pkg/common/group.go b/common/model/group.go similarity index 81% rename from pkg/common/group.go rename to common/model/group.go index 4906525..2f4f732 100644 --- a/pkg/common/group.go +++ b/common/model/group.go @@ -1,5 +1,5 @@ // Do not edit. This file is auto-generated. -package common +package model // Group type Group struct { @@ -12,7 +12,7 @@ type Group struct { // A list of members of the Group. type GroupMember struct { Value string `json:"value"` - Ref string `json:"$ref"` - Type string `json:"type"` + Ref string `json:"$ref,omitempty"` + Type string `json:"type,omitempty"` Display string `json:"display,omitempty"` } diff --git a/pkg/common/user.go b/common/model/user.go similarity index 99% rename from pkg/common/user.go rename to common/model/user.go index f6b0881..4edf48f 100644 --- a/pkg/common/user.go +++ b/common/model/user.go @@ -1,5 +1,5 @@ // Do not edit. This file is auto-generated. -package common +package model // User Account type User struct { diff --git a/common/patch.go b/common/patch.go new file mode 100644 index 0000000..4c36222 --- /dev/null +++ b/common/patch.go @@ -0,0 +1,236 @@ +package common + +import ( + "github.com/elimity-com/scim" + serrors "github.com/elimity-com/scim/errors" + "github.com/scim2/filter-parser/v2" +) + +func HandlePatchOPAdd(objectProps scim.ResourceAttributes, op scim.PatchOperation) (scim.ResourceAttributes, error) { + var err error + + if op.Path == nil || op.Path.ValueExpression == nil { + return AddProperty(objectProps, op) + } + + valueExpression, ok := op.Path.ValueExpression.(*filter.AttributeExpression) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + + fltr, err := filter.ParseAttrExp([]byte(valueExpression.String())) + if err != nil { + return nil, err + } + + properties := make(map[string]interface{}) + if op.Path.ValueExpression == nil { + properties[*op.Path.SubAttribute] = op.Value + + return objectProps, nil + } + + if objectProps[op.Path.AttributePath.AttributeName] != nil { + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + for _, v := range attrProps { + originalValue, ok := v.(map[string]interface{}) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + switch fltr.Operator { + case filter.EQ: + value, ok := originalValue[fltr.AttributePath.AttributeName].(string) + if ok && value == fltr.CompareValue { + if originalValue[*op.Path.SubAttribute] != nil { + return nil, serrors.ScimErrorUniqueness + } + properties = originalValue + } + case filter.PR, filter.NE, filter.CO, filter.SW, filter.EW, filter.GT, filter.GE, filter.LT, filter.LE: + return nil, serrors.ScimErrorBadRequest("operand not supported") + } + } + } else { + objectProps[op.Path.AttributePath.AttributeName] = make([]interface{}, 0) + } + if len(properties) == 0 { + properties[fltr.AttributePath.AttributeName] = fltr.CompareValue + properties[*op.Path.SubAttribute] = op.Value + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, properties) + } + + return objectProps, err +} + +func HandlePatchOPRemove(objectProps scim.ResourceAttributes, op scim.PatchOperation) (scim.ResourceAttributes, error) { + var err error + + switch value := objectProps[op.Path.AttributePath.AttributeName].(type) { + case string: + delete(objectProps, op.Path.AttributePath.AttributeName) + case []interface{}: + attrExpr, ok := op.Path.ValueExpression.(*filter.AttributeExpression) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + ftr, err := filter.ParseAttrExp([]byte(attrExpr.String())) + if err != nil { + return nil, err + } + + index := -1 + if ftr.Operator == filter.EQ { + for i, v := range value { + originalValue, ok := v.(map[string]interface{}) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + value, ok := originalValue[ftr.AttributePath.AttributeName].(string) + if ok && value == ftr.CompareValue { + index = i + } + } + if index == -1 { + return nil, serrors.ScimErrorMutability + } + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps[:index], attrProps[index+1:]...) + } + } + + return objectProps, err +} + +func HandlePatchOPReplace(objectProps scim.ResourceAttributes, op scim.PatchOperation) (scim.ResourceAttributes, error) { + var err error + + if op.Path == nil { + value, ok := op.Value.(map[string]interface{}) + if ok { + for k, v := range value { + objectProps[k] = v + } + } + + return objectProps, nil + } + + switch value := objectProps[op.Path.AttributePath.AttributeName].(type) { + case string: + objectProps[op.Path.AttributePath.AttributeName] = op.Value + case map[string]interface{}: + if op.Path.AttributePath.SubAttribute != nil { + value[*op.Path.AttributePath.SubAttribute] = op.Value + } else { + objectProps[op.Path.AttributePath.AttributeName] = op.Value + } + case []interface{}: + if op.Path.ValueExpression == nil { + objectProps[op.Path.AttributePath.AttributeName] = op.Value + break + } + + value, err := ReplaceInInterfaceArray(value, op) + if err != nil { + return nil, err + } + objectProps[op.Path.AttributePath.AttributeName] = value + } + + return objectProps, err +} + +func ReplaceInInterfaceArray(value []interface{}, op scim.PatchOperation) ([]interface{}, error) { + attrExpr, ok := op.Path.ValueExpression.(*filter.AttributeExpression) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + ftr, err := filter.ParseAttrExp([]byte(attrExpr.String())) + if err != nil { + return nil, err + } + + index := -1 + switch ftr.Operator { + case filter.EQ: + for i, v := range value { + originalValue, ok := v.(map[string]interface{}) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + value, ok := originalValue[ftr.AttributePath.AttributeName].(string) + if ok && value == ftr.CompareValue { + index = i + } + } + if index == -1 { + return nil, serrors.ScimErrorMutability + } + + if originalValue, ok := value[index].(map[string]interface{}); ok { + originalValue[*op.Path.SubAttribute] = op.Value + value[index] = originalValue + return value, nil + } else { + return nil, serrors.ScimErrorInvalidPath + } + case filter.PR, filter.NE, filter.CO, filter.SW, filter.EW, filter.GT, filter.GE, filter.LT, filter.LE: + return nil, serrors.ScimErrorBadRequest("operand not supported") + } + + return value, nil +} + +func AddProperty(objectProps scim.ResourceAttributes, op scim.PatchOperation) (scim.ResourceAttributes, error) { + // simple add property + switch value := op.Value.(type) { + case string: + if objectProps[op.Path.AttributePath.AttributeName] != nil { + return nil, serrors.ScimErrorUniqueness + } + objectProps[op.Path.AttributePath.AttributeName] = op.Value + case map[string]interface{}: + for k, v := range value { + if objectProps[k] != nil { + return nil, serrors.ScimErrorUniqueness + } + objectProps[k] = v + } + case []interface{}: + for _, v := range value { + switch val := v.(type) { + case string: + if objectProps[op.Path.AttributePath.AttributeName] == nil { + objectProps[op.Path.AttributePath.AttributeName] = make([]string, 0) + } + if attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}); ok { + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, v) + } else { + return nil, serrors.ScimErrorInvalidPath + } + case map[string]interface{}: + if objectProps[op.Path.AttributePath.AttributeName] == nil { + objectProps[op.Path.AttributePath.AttributeName] = make([]interface{}, 0) + } + properties := val + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, properties) + } + } + } + + return objectProps, nil +} diff --git a/go.mod b/go.mod index b0c5e11..4f873ae 100644 --- a/go.mod +++ b/go.mod @@ -1,59 +1,70 @@ module github.com/aserto-dev/scim -go 1.22.11 +go 1.23.7 -toolchain go1.23.4 +replace github.com/aserto-dev/scim/common => ./common require ( - github.com/aserto-dev/errors v0.0.13 - github.com/aserto-dev/go-aserto v0.33.6 - github.com/aserto-dev/go-directory v0.33.4 - github.com/aserto-dev/logger v0.0.6 + github.com/aserto-dev/go-aserto v0.33.7 + github.com/aserto-dev/logger v0.0.7 + github.com/aserto-dev/scim/common v0.0.0-00010101000000-000000000000 github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 - github.com/rs/zerolog v1.33.0 - github.com/scim2/filter-parser/v2 v2.2.0 + github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - google.golang.org/protobuf v1.36.3 ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.3-20241127180247-a33202765966.1 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3 // indirect + github.com/aserto-dev/errors v0.0.15 // indirect + github.com/aserto-dev/go-directory v0.33.9 // indirect github.com/aserto-dev/header v0.0.10 // indirect github.com/di-wu/parser v0.3.0 // indirect github.com/di-wu/xsd-datetime v1.0.0 // indirect + github.com/dongri/phonenumber v0.1.12 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/samber/lo v1.47.0 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/scim2/filter-parser/v2 v2.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250122153221-138b5a5a4fd4 // indirect - google.golang.org/grpc v1.70.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/grpc v1.71.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7c8fecf..d9eab45 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,23 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.3-20241127180247-a33202765966.1 h1:cQZXKoQ+eB0kykzfJe80RP3nc+3PWbbBrUBm8XNYAQY= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.3-20241127180247-a33202765966.1/go.mod h1:6VPKM8zbmgf9qsmkmKeH49a36Vtmidw3rG53B5mTenc= -github.com/aserto-dev/errors v0.0.13 h1:STx3azu5kymLiCCIwtPxqKvTF9LeyE6O3YP634Tapp8= -github.com/aserto-dev/errors v0.0.13/go.mod h1:0KXrbZV0/0mqyuUSv7IxuhIg0cHY0p1eOAXAWbXs1SM= -github.com/aserto-dev/go-aserto v0.33.6 h1:Ch64weFFYE3zMcorb3X8qkg3cs9LzJURankGkH3uwgU= -github.com/aserto-dev/go-aserto v0.33.6/go.mod h1:l8/lgmhuGJavgZIFUQ/mtl3q98zJy0gIS4d1oIm8Yq0= -github.com/aserto-dev/go-directory v0.33.4 h1:LhAL0RGKB7L2dm4hjAYVeuUhsu+EreYTUvu28t2OeXk= -github.com/aserto-dev/go-directory v0.33.4/go.mod h1:p0wsjtpBBW2huPDgi6I8OqfhwJWyMRqJHlwPVb3kTSM= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3 h1:qyQXzQFa+0ocXnFeMhjOFfzZZ5opFt1m7e3Ln7Mq2E8= +github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3/go.mod h1:QAQ+JlEH011NOsZ8uBGFPDoixZkfM1WQigK6PTaFSVA= +github.com/aserto-dev/errors v0.0.15 h1:Mx/k7HITit+Istq8YLatiydEIbff39RXf3fW/PlKSwo= +github.com/aserto-dev/errors v0.0.15/go.mod h1:WntQkFRb4j41tp4ObRXTdhu/VZKIzIRTReLHjLLMWyc= +github.com/aserto-dev/go-aserto v0.33.7 h1:oLAj2nu4YKJ7q6pUFpABOQuY/70YfF5B89T0RkKSV8k= +github.com/aserto-dev/go-aserto v0.33.7/go.mod h1:R4Bo3Tgn2KnyvmeyW+gID7pBqaRcnqnib36DiLjcjiw= +github.com/aserto-dev/go-directory v0.33.9 h1:JxsNVRfjpRr0gtpzCTn64D74gUkbj/qdXYFFM7VaP58= +github.com/aserto-dev/go-directory v0.33.9/go.mod h1:jiKmqzVQ0eYnn4CrIp6bFneTOtOFcCthspo287BnWlg= github.com/aserto-dev/header v0.0.10 h1:H6sz3F4pfv53FuyGNoZlRNHpAcOonTioQMnWRowyigU= github.com/aserto-dev/header v0.0.10/go.mod h1:N3+nmX6nXmM9gI8VsGXOujPW6aW/8aEFa7dSu0FRerY= -github.com/aserto-dev/logger v0.0.6 h1:C5u4eU6LJAlyWOjkz/IZmkIXfOH0SBomOHU74o6mUEc= -github.com/aserto-dev/logger v0.0.6/go.mod h1:0wakoQsaQiagtzLxqyOus7ITaY0P5n5MWoQo6GbenWY= +github.com/aserto-dev/logger v0.0.7 h1:ORvXxZDMNIcN/E3SYHj8fxmNZnOD7Gf87pOLB2XQavw= +github.com/aserto-dev/logger v0.0.7/go.mod h1:66ff7ALo68NT1HcCg5zytOnGh6I5R0HeDpN85cwHcD0= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -21,6 +29,8 @@ github.com/di-wu/parser v0.3.0 h1:NMOvy5ifswgt4gsdhySVcKOQtvjC43cHZIfViWctqQY= github.com/di-wu/parser v0.3.0/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo= github.com/di-wu/xsd-datetime v1.0.0 h1:vZoGNkbzpBNoc+JyfVLEbutNDNydYV8XwHeV7eUJoxI= github.com/di-wu/xsd-datetime v1.0.0/go.mod h1:i3iEhrP3WchwseOBeIdW/zxeoleXTOzx1WyDXgdmOww= +github.com/dongri/phonenumber v0.1.12 h1:rR/4VZzxqpocUdyM4dIdfY0TWd8FcW43oiyPaOUxNIk= +github.com/dongri/phonenumber v0.1.12/go.mod h1:cuHFSstIxh6qh/Qs/SCV3Grb/JMYregBLuXELvSYmT4= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 h1:0+BTyxIYgiVAry/P5s8R4dYuLkhB9Nhso8ogFWNr4IQ= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -36,12 +46,12 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 h1:VD1gqscl4nYs1YxVuSdemTrSgTKrwOWDK0FVFMqm+Cg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0/go.mod h1:4EgsQoS4TOhJizV+JTFg40qx1Ofh3XmXEQNBpgvNT40= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -49,6 +59,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -64,8 +76,12 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -75,26 +91,28 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= -github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM= github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -114,39 +132,43 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= -go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= -go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= -go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= -go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250122153221-138b5a5a4fd4 h1://y4MHaM7tNLqTeWKyfBIeoAMxwKwRm/nODb5IKA3BE= -google.golang.org/genproto/googleapis/api v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250122153221-138b5a5a4fd4 h1:yrTuav+chrF0zF/joFGICKTzYv7mh/gr9AgEXrVU8ao= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/go.work b/go.work new file mode 100644 index 0000000..5b78e7d --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.23.7 + +use ( + ./ + ./common +) diff --git a/makefile b/makefile index 72de7ba..eeb13c0 100644 --- a/makefile +++ b/makefile @@ -11,7 +11,7 @@ GOARCH := $(shell go env GOARCH) GOPRIVATE := "github.com/aserto-dev" GOTESTSUM_VERSION := 1.11.0 -GOLANGCI-LINT_VERSION := 1.61.0 +GOLANGCI-LINT_VERSION := 1.64.5 GORELEASER_VERSION := 2.3.2 RELEASE_TAG := $$(svu) @@ -33,12 +33,12 @@ build: .PHONY: lint lint: @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" - @${EXT_BIN_DIR}/golangci-lint run --config ${PWD}/.golangci.yaml + @go list -f '{{.Dir}}/...' -m | xargs ${EXT_BIN_DIR}/golangci-lint run --config ${PWD}/.golangci.yaml .PHONY: test test: @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)"; - ${EXT_BIN_DIR}/gotestsum --format short-verbose -- -count=1 -v ./... + @go list -f '{{.Dir}}/...' -m | xargs ${EXT_BIN_DIR}/gotestsum --format short-verbose -- -count=1 -parallel=1 -v -coverprofile=cover.out -coverpkg=./... .PHONY: snapshot snapshot: diff --git a/pkg/directory/client.go b/pkg/app/directory/client.go similarity index 99% rename from pkg/directory/client.go rename to pkg/app/directory/client.go index acb7aa1..c9f2508 100644 --- a/pkg/directory/client.go +++ b/pkg/app/directory/client.go @@ -12,4 +12,5 @@ func GetDirectoryClient(cfg *client.Config) (*ds.Client, error) { } return ds.FromConnection(conn), nil + } diff --git a/pkg/app/groups.go b/pkg/app/groups.go new file mode 100644 index 0000000..eb3e5ac --- /dev/null +++ b/pkg/app/groups.go @@ -0,0 +1,42 @@ +package app + +import ( + "net/http" + + "github.com/aserto-dev/scim/common/handlers/groups" + "github.com/elimity-com/scim" +) + +type GroupResourceHandler struct { + handler *groups.GroupResourceHandler +} + +func NewGroupResourceHandler(handler *groups.GroupResourceHandler) (*GroupResourceHandler, error) { + return &GroupResourceHandler{ + handler: handler, + }, nil +} + +func (g GroupResourceHandler) Create(r *http.Request, attributes scim.ResourceAttributes) (scim.Resource, error) { + return g.handler.Create(r.Context(), attributes) +} + +func (g GroupResourceHandler) Delete(r *http.Request, id string) error { + return g.handler.Delete(r.Context(), id) +} + +func (g GroupResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) { + return g.handler.Get(r.Context(), id) +} + +func (g GroupResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { + return g.handler.GetAll(r.Context(), params) +} + +func (g GroupResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) { + return g.handler.Patch(r.Context(), id, operations) +} + +func (g GroupResourceHandler) Replace(r *http.Request, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { + return g.handler.Replace(r.Context(), id, attributes) +} diff --git a/pkg/app/handlers/groups/create.go b/pkg/app/handlers/groups/create.go deleted file mode 100644 index 0586e49..0000000 --- a/pkg/app/handlers/groups/create.go +++ /dev/null @@ -1,50 +0,0 @@ -package groups - -import ( - "net/http" - - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/scim/pkg/common" - "github.com/elimity-com/scim" - serrors "github.com/elimity-com/scim/errors" -) - -func (u GroupResourceHandler) Create(r *http.Request, attributes scim.ResourceAttributes) (scim.Resource, error) { - logger := u.logger.With().Str("method", "Create").Str("displayName", attributes["displayName"].(string)).Logger() - logger.Info().Msg("create group") - logger.Trace().Any("attributes", attributes).Msg("creating group") - - object, err := common.ResourceAttributesToObject(attributes, u.cfg.SCIM.GroupObjectType, attributes["displayName"].(string)) - if err != nil { - logger.Error().Err(err).Msg("failed to convert attributes to object") - return scim.Resource{}, serrors.ScimErrorInvalidSyntax - } - - resp, err := u.dirClient.Writer.SetObject(r.Context(), &dsw.SetObjectRequest{ - Object: object, - }) - if err != nil { - logger.Error().Err(err).Msg("failed to create group") - return scim.Resource{}, err - } - - logger.Trace().Any("response", resp.Result).Msg("group object created") - - err = u.setGroupMappings(r.Context(), resp.Result.Id) - if err != nil { - logger.Err(err).Msg("failed to set group mappings") - return scim.Resource{}, err - } - - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() - resource := common.ObjectToResource(resp.Result, scim.Meta{ - Created: &createdAt, - LastModified: &updatedAt, - Version: resp.Result.Etag, - }) - - logger.Trace().Any("resource", resource).Msg("group created") - - return resource, nil -} diff --git a/pkg/app/handlers/groups/delete.go b/pkg/app/handlers/groups/delete.go deleted file mode 100644 index 145b7dd..0000000 --- a/pkg/app/handlers/groups/delete.go +++ /dev/null @@ -1,32 +0,0 @@ -package groups - -import ( - "net/http" - - cerr "github.com/aserto-dev/errors" - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" - serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" -) - -func (u GroupResourceHandler) Delete(r *http.Request, id string) error { - logger := u.logger.With().Str("method", "Delete").Str("id", id).Logger() - logger.Info().Msg("delete group") - - _, err := u.dirClient.Writer.DeleteObject(r.Context(), &dsw.DeleteObjectRequest{ - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: id, - WithRelations: true, - }) - if err != nil { - logger.Err(err).Msg("failed to delete group") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { - return serrors.ScimErrorResourceNotFound(id) - } - } - - logger.Trace().Msg("group deleted") - - return err -} diff --git a/pkg/app/handlers/groups/get.go b/pkg/app/handlers/groups/get.go deleted file mode 100644 index 42f67a5..0000000 --- a/pkg/app/handlers/groups/get.go +++ /dev/null @@ -1,74 +0,0 @@ -package groups - -import ( - "net/http" - - dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" - dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" - "github.com/aserto-dev/scim/pkg/common" - "github.com/elimity-com/scim" -) - -func (u GroupResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) { - logger := u.logger.With().Str("method", "Get").Str("id", id).Logger() - logger.Info().Msg("get group") - - resp, err := u.dirClient.Reader.GetObject(r.Context(), &dsr.GetObjectRequest{ - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: id, - WithRelations: true, - }) - if err != nil { - logger.Err(err).Str("id", id).Msg("failed to get group") - return scim.Resource{}, err - } - - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() - resource := common.ObjectToResource(resp.Result, scim.Meta{ - Created: &createdAt, - LastModified: &updatedAt, - Version: resp.Result.Etag, - }) - - logger.Trace().Any("group", resource).Msg("group retrieved") - - return resource, nil -} - -func (u GroupResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { - u.logger.Info().Msg("getall groups") - - var ( - resources = make([]scim.Resource, 0) - ) - - resp, err := u.dirClient.Reader.GetObjects(r.Context(), &dsr.GetObjectsRequest{ - ObjectType: u.cfg.SCIM.GroupObjectType, - Page: &dsc.PaginationRequest{ - Size: int32(params.Count), //nolint:gosec - }, - }) - if err != nil { - u.logger.Err(err).Msg("failed to read groups") - return scim.Page{}, err - } - - for _, v := range resp.Results { - createdAt := v.CreatedAt.AsTime() - updatedAt := v.UpdatedAt.AsTime() - resource := common.ObjectToResource(v, scim.Meta{ - Created: &createdAt, - LastModified: &updatedAt, - Version: v.Etag, - }) - resources = append(resources, resource) - } - - u.logger.Trace().Int("total_results", len(resources)).Msg("groups read") - - return scim.Page{ - TotalResults: len(resources), - Resources: resources, - }, nil -} diff --git a/pkg/app/handlers/groups/handler.go b/pkg/app/handlers/groups/handler.go deleted file mode 100644 index e20569e..0000000 --- a/pkg/app/handlers/groups/handler.go +++ /dev/null @@ -1,60 +0,0 @@ -package groups - -import ( - "context" - - "github.com/aserto-dev/go-aserto/ds/v3" - dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/scim/pkg/config" - "github.com/aserto-dev/scim/pkg/directory" - "github.com/rs/zerolog" -) - -const ( - GroupMembers = "members" -) - -type GroupResourceHandler struct { - dirClient *ds.Client - cfg *config.Config - logger *zerolog.Logger -} - -func NewGroupResourceHandler(cfg *config.Config, logger *zerolog.Logger) (*GroupResourceHandler, error) { - dirClient, err := directory.GetDirectoryClient(&cfg.Directory) - if err != nil { - return nil, err - } - - groupLogger := logger.With().Str("component", "groups").Logger() - - return &GroupResourceHandler{ - dirClient: dirClient, - cfg: cfg, - logger: &groupLogger, - }, nil -} - -func (u GroupResourceHandler) setGroupMappings(ctx context.Context, groupID string) error { - for _, groupMap := range u.cfg.SCIM.GroupMappings { - if groupMap.SubjectID == groupID { - u.logger.Trace().Str("groupID", groupID).Str("relation", groupMap.Relation).Str("objectID", groupMap.ObjectID).Msg("setting group mapping") - _, err := u.dirClient.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ - Relation: &dsc.Relation{ - SubjectType: u.cfg.SCIM.GroupObjectType, - SubjectId: groupID, - Relation: groupMap.Relation, - ObjectType: groupMap.ObjectType, - ObjectId: groupMap.ObjectID, - SubjectRelation: groupMap.SubjectRelation, - }, - }) - if err != nil { - u.logger.Error().Err(err).Str("groupID", groupID).Str("relation", groupMap.Relation).Str("objectID", groupMap.ObjectID).Msg("failed to set group mapping") - return err - } - } - } - return nil -} diff --git a/pkg/app/handlers/groups/patch.go b/pkg/app/handlers/groups/patch.go deleted file mode 100644 index d1da563..0000000 --- a/pkg/app/handlers/groups/patch.go +++ /dev/null @@ -1,238 +0,0 @@ -package groups - -import ( - "context" - "net/http" - - cerr "github.com/aserto-dev/errors" - dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" - dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" - "github.com/aserto-dev/scim/pkg/common" - "github.com/elimity-com/scim" - serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" - "github.com/scim2/filter-parser/v2" - structpb "google.golang.org/protobuf/types/known/structpb" -) - -func (u GroupResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) { - logger := u.logger.With().Str("method", "Patch").Str("id", id).Logger() - logger.Info().Msg("patch group") - logger.Trace().Any("operations", operations).Msg("patching group") - - getObjResp, err := u.dirClient.Reader.GetObject(r.Context(), &dsr.GetObjectRequest{ - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: id, - WithRelations: true, - }) - if err != nil { - logger.Err(err).Str("id", id).Msg("failed to get group") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { - return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) - } - return scim.Resource{}, err - } - - object := getObjResp.Result - - for _, op := range operations { - switch op.Op { - case scim.PatchOperationAdd: - err := u.handlePatchOPAdd(r.Context(), object, op) - if err != nil { - logger.Err(err).Msg("error adding property") - return scim.Resource{}, err - } - case scim.PatchOperationRemove: - err := u.handlePatchOPRemove(r.Context(), object, op) - if err != nil { - logger.Err(err).Msg("error removing property") - return scim.Resource{}, err - } - case scim.PatchOperationReplace: - err := u.handlePatchOPReplace(object, op) - if err != nil { - logger.Err(err).Msg("error replacing property") - return scim.Resource{}, err - } - } - } - - object.Etag = getObjResp.Result.Etag - resp, err := u.dirClient.Writer.SetObject(r.Context(), &dsw.SetObjectRequest{ - Object: object, - }) - if err != nil { - logger.Err(err).Msg("error setting object") - return scim.Resource{}, err - } - - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() - resource := common.ObjectToResource(resp.Result, scim.Meta{ - Created: &createdAt, - LastModified: &updatedAt, - Version: resp.Result.Etag, - }) - - logger.Trace().Any("group", resource).Msg("group patched") - - return resource, nil -} - -func (u GroupResourceHandler) handlePatchOPAdd(ctx context.Context, object *dsc.Object, op scim.PatchOperation) error { - var err error - objectProps := object.Properties.AsMap() - if op.Path == nil || op.Path.ValueExpression == nil { - // simple add property - switch value := op.Value.(type) { - case string: - if objectProps[op.Path.AttributePath.AttributeName] != nil { - return serrors.ScimErrorUniqueness - } - objectProps[op.Path.AttributePath.AttributeName] = op.Value - case map[string]interface{}: - for k, v := range value { - if objectProps[k] != nil { - return serrors.ScimErrorUniqueness - } - objectProps[k] = v - } - case []interface{}: - for _, v := range value { - switch val := v.(type) { - case string: - objectProps[op.Path.AttributePath.AttributeName] = append(objectProps[op.Path.AttributePath.AttributeName].([]interface{}), v) - case map[string]interface{}: - properties := val - objectProps[op.Path.AttributePath.AttributeName] = append(objectProps[op.Path.AttributePath.AttributeName].([]interface{}), properties) - if op.Path.AttributePath.AttributeName == GroupMembers { - err = u.addUserToGroup(ctx, properties["value"].(string), object.Id) - if err != nil { - return err - } - } - } - } - } - } - - object.Properties, err = structpb.NewStruct(objectProps) - return err -} - -func (u GroupResourceHandler) handlePatchOPRemove(ctx context.Context, object *dsc.Object, op scim.PatchOperation) error { - var err error - objectProps := object.Properties.AsMap() - var oldValue interface{} - - switch value := objectProps[op.Path.AttributePath.AttributeName].(type) { - case string: - oldValue = objectProps[op.Path.AttributePath.AttributeName] - delete(objectProps, op.Path.AttributePath.AttributeName) - case []interface{}: - ftr, err := filter.ParseAttrExp([]byte(op.Path.ValueExpression.(*filter.AttributeExpression).String())) - if err != nil { - return err - } - - index := -1 - if ftr.Operator == filter.EQ { - for i, v := range value { - originalValue := v.(map[string]interface{}) - if originalValue[ftr.AttributePath.AttributeName].(string) == ftr.CompareValue { - oldValue = originalValue - index = i - } - } - if index == -1 { - return serrors.ScimErrorMutability - } - objectProps[op.Path.AttributePath.AttributeName] = append(objectProps[op.Path.AttributePath.AttributeName].([]interface{})[:index], objectProps[op.Path.AttributePath.AttributeName].([]interface{})[index+1:]...) - } - } - - if op.Path.AttributePath.AttributeName == GroupMembers { - user := oldValue.(map[string]interface{})["value"].(string) - err = u.removeUserFromGroup(ctx, user, object.Id) - if err != nil { - return err - } - } - - object.Properties, err = structpb.NewStruct(objectProps) - return err -} - -func (u GroupResourceHandler) handlePatchOPReplace(object *dsc.Object, op scim.PatchOperation) error { - var err error - objectProps := object.Properties.AsMap() - - switch value := op.Value.(type) { - case string: - objectProps[op.Path.AttributePath.AttributeName] = op.Value - case map[string]interface{}: - for k, v := range value { - objectProps[k] = v - } - } - - object.Properties, err = structpb.NewStruct(objectProps) - return err -} - -func (u GroupResourceHandler) addUserToGroup(ctx context.Context, userID, group string) error { - rel, err := u.dirClient.Reader.GetRelation(ctx, &dsr.GetRelationRequest{ - SubjectType: u.cfg.SCIM.UserObjectType, - SubjectId: userID, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: group, - Relation: u.cfg.SCIM.GroupMemberRelation, - }) - if err != nil { - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrRelationNotFound) { - _, err = u.dirClient.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ - Relation: &dsc.Relation{ - SubjectId: userID, - SubjectType: u.cfg.SCIM.UserObjectType, - Relation: u.cfg.SCIM.GroupMemberRelation, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: group, - }}) - return err - } - return err - } - - if rel != nil { - return serrors.ScimErrorUniqueness - } - return nil -} - -func (u GroupResourceHandler) removeUserFromGroup(ctx context.Context, userID, group string) error { - _, err := u.dirClient.Reader.GetRelation(ctx, &dsr.GetRelationRequest{ - SubjectType: u.cfg.SCIM.UserObjectType, - SubjectId: userID, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: group, - Relation: u.cfg.SCIM.GroupMemberRelation, - }) - if err != nil { - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrRelationNotFound) { - return serrors.ScimErrorMutability - } - return err - } - - _, err = u.dirClient.Writer.DeleteRelation(ctx, &dsw.DeleteRelationRequest{ - SubjectType: u.cfg.SCIM.UserObjectType, - SubjectId: userID, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: group, - Relation: u.cfg.SCIM.GroupMemberRelation, - }) - return err -} diff --git a/pkg/app/handlers/groups/replace.go b/pkg/app/handlers/groups/replace.go deleted file mode 100644 index ccdd63a..0000000 --- a/pkg/app/handlers/groups/replace.go +++ /dev/null @@ -1,61 +0,0 @@ -package groups - -import ( - "net/http" - - cerr "github.com/aserto-dev/errors" - dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" - "github.com/aserto-dev/scim/pkg/common" - "github.com/elimity-com/scim" - serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" -) - -func (u GroupResourceHandler) Replace(r *http.Request, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { - logger := u.logger.With().Str("method", "Replace").Str("id", id).Logger() - logger.Info().Msg("replace group") - logger.Trace().Any("attributes", attributes).Msg("replacing group") - - getObjResp, err := u.dirClient.Reader.GetObject(r.Context(), &dsr.GetObjectRequest{ - ObjectType: "grroup", - ObjectId: id, - WithRelations: true, - }) - if err != nil { - logger.Err(err).Msg("failed to get group") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { - return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) - } - return scim.Resource{}, err - } - - object, err := common.ResourceAttributesToObject(attributes, u.cfg.SCIM.GroupObjectType, id) - if err != nil { - logger.Err(err).Msg("failed to convert attributes to object") - return scim.Resource{}, serrors.ScimErrorInvalidSyntax - } - object.Id = id - object.Etag = getObjResp.Result.Etag - - setResp, err := u.dirClient.Writer.SetObject(r.Context(), &dsw.SetObjectRequest{ - Object: object, - }) - if err != nil { - logger.Err(err).Msg("failed to replace group") - return scim.Resource{}, err - } - - createdAt := setResp.Result.CreatedAt.AsTime() - updatedAt := setResp.Result.UpdatedAt.AsTime() - resource := common.ObjectToResource(setResp.Result, scim.Meta{ - Created: &createdAt, - LastModified: &updatedAt, - Version: setResp.Result.Etag, - }) - - logger.Trace().Any("resource", resource).Msg("group replaced") - - return resource, nil -} diff --git a/pkg/app/handlers/users/create.go b/pkg/app/handlers/users/create.go deleted file mode 100644 index 4be7db2..0000000 --- a/pkg/app/handlers/users/create.go +++ /dev/null @@ -1,72 +0,0 @@ -package users - -import ( - "net/http" - - cerr "github.com/aserto-dev/errors" - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" - "github.com/aserto-dev/scim/pkg/common" - "github.com/elimity-com/scim" - serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" -) - -func (u UsersResourceHandler) Create(r *http.Request, attributes scim.ResourceAttributes) (scim.Resource, error) { - logger := u.logger.With().Str("method", "Create").Str("userName", attributes["userName"].(string)).Logger() - logger.Info().Msg("create user") - logger.Trace().Any("attributes", attributes).Msg("creating user") - user, err := common.ResourceAttributesToUser(attributes) - if err != nil { - logger.Err(err).Msg("failed to convert attributes to user") - return scim.Resource{}, serrors.ScimErrorInvalidSyntax - } - - object, err := common.UserToObject(user) - if err != nil { - logger.Err(err).Msg("failed to convert user to object") - return scim.Resource{}, serrors.ScimErrorInvalidSyntax - } - - logger.Trace().Any("object", object).Msg("creating user object") - resp, err := u.dirClient.Writer.SetObject(r.Context(), &dsw.SetObjectRequest{ - Object: object, - }) - if err != nil { - logger.Err(err).Msg("failed to create user") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrAlreadyExists) { - return scim.Resource{}, serrors.ScimErrorUniqueness - } - return scim.Resource{}, err - } - - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() - resource := common.ObjectToResource(resp.Result, scim.Meta{ - Created: &createdAt, - LastModified: &updatedAt, - Version: resp.Result.Etag, - }) - - err = u.setAllIdentities(r.Context(), resp.Result.Id, user) - if err != nil { - logger.Err(err).Msg("failed to set identities") - return scim.Resource{}, err - } - - err = u.setUserGroups(r.Context(), resp.Result.Id, user.Groups) - if err != nil { - logger.Err(err).Msg("failed to set groups") - return scim.Resource{}, err - } - - err = u.setUserMappings(r.Context(), resp.Result.Id) - if err != nil { - logger.Err(err).Msg("failed to set mappings") - return scim.Resource{}, err - } - - logger.Trace().Any("resource", resource).Msg("user created") - - return resource, nil -} diff --git a/pkg/app/handlers/users/handler.go b/pkg/app/handlers/users/handler.go deleted file mode 100644 index 2e4638c..0000000 --- a/pkg/app/handlers/users/handler.go +++ /dev/null @@ -1,263 +0,0 @@ -package users - -import ( - "context" - - cerr "github.com/aserto-dev/errors" - "github.com/aserto-dev/go-aserto/ds/v3" - dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" - dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" - "github.com/aserto-dev/scim/pkg/common" - "github.com/aserto-dev/scim/pkg/config" - "github.com/aserto-dev/scim/pkg/directory" - serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" - "github.com/rs/zerolog" - structpb "google.golang.org/protobuf/types/known/structpb" -) - -const ( - Emails = "emails" - Groups = "groups" - IdentityKindKey = "kind" -) - -type UsersResourceHandler struct { - dirClient *ds.Client - cfg *config.Config - logger *zerolog.Logger -} - -func NewUsersResourceHandler(cfg *config.Config, logger *zerolog.Logger) (*UsersResourceHandler, error) { - usersLogger := logger.With().Str("component", "users").Logger() - dirClient, err := directory.GetDirectoryClient(&cfg.Directory) - if err != nil { - return nil, err - } - return &UsersResourceHandler{ - dirClient: dirClient, - cfg: cfg, - logger: &usersLogger, - }, nil -} - -func (u UsersResourceHandler) setUserGroups(ctx context.Context, userID string, groups []common.UserGroup) error { - relations, err := u.dirClient.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ - SubjectType: u.cfg.SCIM.UserObjectType, - SubjectId: userID, - }) - if err != nil { - return err - } - - for _, v := range relations.Results { - if v.Relation == u.cfg.SCIM.GroupMemberRelation { - u.logger.Trace().Str("user_id", userID).Str("group", v.ObjectId).Msg("removing user from group") - _, err = u.dirClient.Writer.DeleteRelation(ctx, &dsw.DeleteRelationRequest{ - SubjectType: v.SubjectType, - SubjectId: v.SubjectId, - Relation: v.Relation, - ObjectType: v.ObjectType, - ObjectId: v.ObjectId, - }) - if err != nil { - return err - } - } - } - - for _, v := range groups { - u.logger.Trace().Str("user_id", userID).Str("group", v.Value).Msg("setting user group") - _, err = u.dirClient.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ - Relation: &dsc.Relation{ - SubjectId: userID, - SubjectType: u.cfg.SCIM.UserObjectType, - Relation: u.cfg.SCIM.GroupMemberRelation, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: v.Value, - }}) - if err != nil { - return err - } - } - - return nil -} - -func (u UsersResourceHandler) addUserToGroup(ctx context.Context, userID, group string) error { - rel, err := u.dirClient.Reader.GetRelation(ctx, &dsr.GetRelationRequest{ - SubjectType: u.cfg.SCIM.UserObjectType, - SubjectId: userID, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: group, - Relation: u.cfg.SCIM.GroupMemberRelation, - }) - if err != nil { - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrRelationNotFound) { - u.logger.Trace().Str("user_id", userID).Str("group", group).Msg("adding user to group") - _, err = u.dirClient.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ - Relation: &dsc.Relation{ - SubjectId: userID, - SubjectType: u.cfg.SCIM.UserObjectType, - Relation: u.cfg.SCIM.GroupMemberRelation, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: group, - }}) - return err - } - return err - } - - if rel != nil { - return serrors.ScimErrorUniqueness - } - return nil -} - -func (u UsersResourceHandler) removeUserFromGroup(ctx context.Context, userID, group string) error { - _, err := u.dirClient.Reader.GetRelation(ctx, &dsr.GetRelationRequest{ - SubjectType: u.cfg.SCIM.UserObjectType, - SubjectId: userID, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: group, - Relation: u.cfg.SCIM.GroupMemberRelation, - }) - if err != nil { - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrRelationNotFound) { - return serrors.ScimErrorMutability - } - return err - } - - u.logger.Trace().Str("user_id", userID).Str("group", group).Msg("removing user from group") - _, err = u.dirClient.Writer.DeleteRelation(ctx, &dsw.DeleteRelationRequest{ - SubjectType: u.cfg.SCIM.UserObjectType, - SubjectId: userID, - ObjectType: u.cfg.SCIM.GroupObjectType, - ObjectId: group, - Relation: u.cfg.SCIM.GroupMemberRelation, - }) - return err -} - -func (u UsersResourceHandler) setIdentity(ctx context.Context, userID, identity string, propsMap map[string]interface{}) error { - props, err := structpb.NewStruct(propsMap) - if err != nil { - return err - } - - u.logger.Trace().Str("user_id", userID).Str("identity", identity).Any("props", props).Msg("setting identity") - _, err = u.dirClient.Writer.SetObject(ctx, &dsw.SetObjectRequest{ - Object: &dsc.Object{ - Type: u.cfg.SCIM.IdentityObjectType, - Id: identity, - Properties: props, - }, - }) - if err != nil { - return err - } - - rel, err := u.getIdentityRelation(userID, identity) - if err != nil { - u.logger.Err(err).Msg("failed to get identity relation") - return err - } - - u.logger.Trace().Str("user_id", userID).Str("identity", identity).Any("relation", rel).Msg("setting identity relation") - _, err = u.dirClient.Writer.SetRelation(ctx, &dsw.SetRelationRequest{Relation: rel}) - return err -} - -func (u UsersResourceHandler) removeIdentity(ctx context.Context, identity string) error { - u.logger.Info().Str("identity", identity).Msg("removing identity") - _, err := u.dirClient.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ - ObjectType: u.cfg.SCIM.IdentityObjectType, - ObjectId: identity, - WithRelations: true, - }) - - return err -} - -func (u UsersResourceHandler) setAllIdentities(ctx context.Context, userID string, user *common.User) error { - u.logger.Info().Str("user_id", userID).Msg("setting identities") - if user.UserName != "" { - u.logger.Debug().Str("user_id", userID).Str("username", user.UserName).Msg("setting username identity") - err := u.setIdentity(ctx, userID, user.UserName, map[string]interface{}{IdentityKindKey: "IDENTITY_KIND_USERNAME"}) - if err != nil { - return err - } - } - - if u.cfg.SCIM.CreateEmailIdentities { - for _, email := range user.Emails { - if email.Value == user.UserName { - continue - } - - u.logger.Debug().Str("user_id", userID).Str("email", email.Value).Msg("setting email identity") - err := u.setIdentity(ctx, userID, email.Value, map[string]interface{}{IdentityKindKey: "IDENTITY_KIND_EMAIL"}) - if err != nil { - return err - } - } - } - - if user.ExternalID != "" { - u.logger.Debug().Str("user_id", userID).Str("external_id", user.ExternalID).Msg("setting external_id identity") - err := u.setIdentity(ctx, userID, user.ExternalID, map[string]interface{}{IdentityKindKey: "IDENTITY_KIND_PID"}) - if err != nil { - return err - } - } - - return nil -} - -func (u UsersResourceHandler) setUserMappings(ctx context.Context, userID string) error { - for _, userMap := range u.cfg.SCIM.UserMappings { - if userMap.SubjectID == userID { - u.logger.Trace().Str("user_id", userID).Str("object_id", userMap.ObjectID).Msg("setting user mapping") - _, err := u.dirClient.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ - Relation: &dsc.Relation{ - SubjectType: u.cfg.SCIM.UserObjectType, - SubjectId: userMap.SubjectID, - Relation: userMap.Relation, - ObjectType: userMap.ObjectType, - ObjectId: userMap.ObjectID, - SubjectRelation: userMap.SubjectRelation, - }, - }) - if err != nil { - return err - } - } - } - return nil -} - -func (u UsersResourceHandler) getIdentityRelation(userID, identity string) (*dsc.Relation, error) { - switch u.cfg.SCIM.Identity.ObjectType { - case u.cfg.SCIM.IdentityObjectType: - return &dsc.Relation{ - SubjectId: userID, - SubjectType: u.cfg.SCIM.UserObjectType, - Relation: u.cfg.SCIM.Identity.Relation, - ObjectType: u.cfg.SCIM.IdentityObjectType, - ObjectId: identity, - }, nil - case u.cfg.SCIM.UserObjectType: - return &dsc.Relation{ - SubjectId: identity, - SubjectType: u.cfg.SCIM.IdentityObjectType, - Relation: u.cfg.SCIM.Identity.Relation, - ObjectType: u.cfg.SCIM.UserObjectType, - ObjectId: userID, - }, nil - default: - return nil, errors.New("invalid identity relation") - } -} diff --git a/pkg/app/handlers/users/patch.go b/pkg/app/handlers/users/patch.go deleted file mode 100644 index 5fffae2..0000000 --- a/pkg/app/handlers/users/patch.go +++ /dev/null @@ -1,236 +0,0 @@ -package users - -import ( - "context" - "net/http" - - cerr "github.com/aserto-dev/errors" - dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" - dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" - "github.com/aserto-dev/scim/pkg/common" - "github.com/elimity-com/scim" - serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" - "github.com/scim2/filter-parser/v2" - structpb "google.golang.org/protobuf/types/known/structpb" -) - -func (u UsersResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) { - logger := u.logger.With().Str("method", "Patch").Str("id", id).Logger() - logger.Info().Msg("patch user") - logger.Trace().Str("id", id).Any("operations", operations).Msg("patching user") - getObjResp, err := u.dirClient.Reader.GetObject(r.Context(), &dsr.GetObjectRequest{ - ObjectType: u.cfg.SCIM.UserObjectType, - ObjectId: id, - WithRelations: true, - }) - if err != nil { - logger.Err(err).Msg("failed to get user") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { - return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) - } - return scim.Resource{}, err - } - - object := getObjResp.Result - - for _, op := range operations { - switch op.Op { - case scim.PatchOperationAdd: - err := u.handlePatchOPAdd(r.Context(), object, op) - if err != nil { - logger.Err(err).Msg("error adding property") - return scim.Resource{}, err - } - case scim.PatchOperationRemove: - err := u.handlePatchOPRemove(r.Context(), object, op) - if err != nil { - logger.Err(err).Msg("error removing property") - return scim.Resource{}, err - } - case scim.PatchOperationReplace: - err := u.handlePatchOPReplace(object, op) - if err != nil { - logger.Err(err).Msg("error replacing property") - return scim.Resource{}, err - } - } - } - - if err != nil { - return scim.Resource{}, err - } - object.Etag = getObjResp.Result.Etag - resp, err := u.dirClient.Writer.SetObject(r.Context(), &dsw.SetObjectRequest{ - Object: object, - }) - if err != nil { - logger.Err(err).Msg("error setting object") - return scim.Resource{}, err - } - - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() - resource := common.ObjectToResource(resp.Result, scim.Meta{ - Created: &createdAt, - LastModified: &updatedAt, - Version: resp.Result.Etag, - }) - - return resource, nil -} - -func (u UsersResourceHandler) handlePatchOPAdd(ctx context.Context, object *dsc.Object, op scim.PatchOperation) error { - var err error - if op.Path == nil || op.Path.ValueExpression == nil { - return u.addProperty(object, op) - } - - objectProps := object.Properties.AsMap() - fltr, err := filter.ParseAttrExp([]byte(op.Path.ValueExpression.(*filter.AttributeExpression).String())) - if err != nil { - return err - } - - switch op.Path.AttributePath.AttributeName { - case Emails, Groups: - properties := make(map[string]interface{}) - if op.Path.ValueExpression != nil { - if objectProps[op.Path.AttributePath.AttributeName] != nil { - for _, v := range objectProps[op.Path.AttributePath.AttributeName].([]interface{}) { - originalValue := v.(map[string]interface{}) - if fltr.Operator == filter.EQ { - if originalValue[fltr.AttributePath.AttributeName].(string) == fltr.CompareValue { - if originalValue[*op.Path.SubAttribute] != nil { - return serrors.ScimErrorUniqueness - } - properties = originalValue - } - } - } - } else { - objectProps[op.Path.AttributePath.AttributeName] = make([]interface{}, 0) - } - if len(properties) == 0 { - properties[fltr.AttributePath.AttributeName] = fltr.CompareValue - properties[*op.Path.SubAttribute] = op.Value - objectProps[op.Path.AttributePath.AttributeName] = append(objectProps[op.Path.AttributePath.AttributeName].([]interface{}), properties) - } - } else { - properties[*op.Path.SubAttribute] = op.Value - } - - if op.Path.AttributePath.AttributeName == Emails && u.cfg.SCIM.CreateEmailIdentities { - err = u.setIdentity(ctx, object.Id, op.Value.(string), map[string]interface{}{IdentityKindKey: "IDENTITY_KIND_EMAIL"}) - if err != nil { - return err - } - } else if op.Path.AttributePath.AttributeName == Groups { - err = u.addUserToGroup(ctx, object.Id, op.Value.(string)) - if err != nil { - return err - } - } - } - - object.Properties, err = structpb.NewStruct(objectProps) - return err -} - -func (u UsersResourceHandler) addProperty(object *dsc.Object, op scim.PatchOperation) error { - // simple add property - objectProps := object.Properties.AsMap() - switch v := op.Value.(type) { - case string: - if objectProps[op.Path.AttributePath.AttributeName] != nil { - return serrors.ScimErrorUniqueness - } - objectProps[op.Path.AttributePath.AttributeName] = op.Value - case map[string]interface{}: - value := v - for k, v := range value { - if objectProps[k] != nil { - return serrors.ScimErrorUniqueness - } - objectProps[k] = v - } - } - - props, err := structpb.NewStruct(objectProps) - if err == nil { - object.Properties = props - } - - return err -} - -func (u UsersResourceHandler) handlePatchOPRemove(ctx context.Context, object *dsc.Object, op scim.PatchOperation) error { - var err error - objectProps := object.Properties.AsMap() - var oldValue interface{} - - switch value := objectProps[op.Path.AttributePath.AttributeName].(type) { - case string: - oldValue = objectProps[op.Path.AttributePath.AttributeName] - delete(objectProps, op.Path.AttributePath.AttributeName) - case []interface{}: - ftr, err := filter.ParseAttrExp([]byte(op.Path.ValueExpression.(*filter.AttributeExpression).String())) - if err != nil { - return err - } - - index := -1 - if ftr.Operator == filter.EQ { - for i, v := range value { - originalValue := v.(map[string]interface{}) - if originalValue[ftr.AttributePath.AttributeName].(string) == ftr.CompareValue { - oldValue = originalValue - index = i - } - } - if index == -1 { - return serrors.ScimErrorMutability - } - objectProps[op.Path.AttributePath.AttributeName] = append(objectProps[op.Path.AttributePath.AttributeName].([]interface{})[:index], objectProps[op.Path.AttributePath.AttributeName].([]interface{})[index+1:]...) - } - } - - if op.Path.AttributePath.AttributeName == Emails && u.cfg.SCIM.CreateEmailIdentities { - email := oldValue.(map[string]interface{})["value"].(string) - err = u.removeIdentity(ctx, email) - if err != nil { - return err - } - } else if op.Path.AttributePath.AttributeName == Groups { - group := oldValue.(map[string]interface{})["value"].(string) - err = u.removeUserFromGroup(ctx, object.Id, group) - if err != nil { - return err - } - } - - object.Properties, err = structpb.NewStruct(objectProps) - return err -} - -func (u UsersResourceHandler) handlePatchOPReplace(object *dsc.Object, op scim.PatchOperation) error { - var err error - objectProps := object.Properties.AsMap() - - switch value := op.Value.(type) { - case string: - objectProps[op.Path.AttributePath.AttributeName] = op.Value - case map[string]interface{}: - for k, v := range value { - if k == "active" { - objectProps["enabled"] = v - } - objectProps[k] = v - } - } - - object.Properties, err = structpb.NewStruct(objectProps) - return err -} diff --git a/pkg/app/handlers/users/replace.go b/pkg/app/handlers/users/replace.go deleted file mode 100644 index e4a015a..0000000 --- a/pkg/app/handlers/users/replace.go +++ /dev/null @@ -1,84 +0,0 @@ -package users - -import ( - "net/http" - - cerr "github.com/aserto-dev/errors" - dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" - dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" - "github.com/aserto-dev/scim/pkg/common" - "github.com/elimity-com/scim" - serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" -) - -func (u UsersResourceHandler) Replace(r *http.Request, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { - logger := u.logger.With().Str("method", "Replace").Str("id", id).Logger() - logger.Info().Msg("replace user") - logger.Trace().Any("attributes", attributes).Msg("replacing user") - getObjResp, err := u.dirClient.Reader.GetObject(r.Context(), &dsr.GetObjectRequest{ - ObjectType: u.cfg.SCIM.UserObjectType, - ObjectId: id, - WithRelations: true, - }) - if err != nil { - logger.Err(err).Msg("failed to get user") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { - return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) - } - return scim.Resource{}, err - } - - user, err := common.ResourceAttributesToUser(attributes) - if err != nil { - logger.Err(err).Msg("failed to convert attributes to user") - return scim.Resource{}, serrors.ScimErrorInvalidSyntax - } - - object, err := common.UserToObject(user) - if err != nil { - logger.Err(err).Msg("failed to convert user to object") - return scim.Resource{}, serrors.ScimErrorInvalidSyntax - } - object.Id = id - object.Etag = getObjResp.Result.Etag - - setResp, err := u.dirClient.Writer.SetObject(r.Context(), &dsw.SetObjectRequest{ - Object: object, - }) - if err != nil { - logger.Err(err).Msg("failed to replace user") - return scim.Resource{}, err - } - - err = u.setAllIdentities(r.Context(), id, user) - if err != nil { - logger.Err(err).Msg("failed to set identities") - return scim.Resource{}, err - } - - err = u.setUserGroups(r.Context(), id, user.Groups) - if err != nil { - logger.Err(err).Msg("failed to set groups") - return scim.Resource{}, err - } - - err = u.setUserMappings(r.Context(), id) - if err != nil { - logger.Err(err).Msg("failed to set mappings") - return scim.Resource{}, err - } - - createdAt := setResp.Result.CreatedAt.AsTime() - updatedAt := setResp.Result.UpdatedAt.AsTime() - resource := common.ObjectToResource(setResp.Result, scim.Meta{ - Created: &createdAt, - LastModified: &updatedAt, - Version: setResp.Result.Etag, - }) - - logger.Trace().Any("resource", resource).Msg("user replaced") - - return resource, nil -} diff --git a/pkg/app/run.go b/pkg/app/run.go index 439afc1..7c50488 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -7,13 +7,17 @@ import ( "strings" "time" + "github.com/aserto-dev/go-aserto/ds/v3" "github.com/aserto-dev/logger" - "github.com/aserto-dev/scim/pkg/app/handlers/groups" - "github.com/aserto-dev/scim/pkg/app/handlers/users" + "github.com/aserto-dev/scim/common/convert" + "github.com/aserto-dev/scim/common/handlers/groups" + "github.com/aserto-dev/scim/common/handlers/users" + "github.com/aserto-dev/scim/pkg/app/directory" "github.com/aserto-dev/scim/pkg/config" "github.com/elimity-com/scim" "github.com/elimity-com/scim/optional" "github.com/elimity-com/scim/schema" + "github.com/rs/zerolog" ) func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) error { @@ -27,7 +31,17 @@ func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) er return err } - userHandler, err := users.NewUsersResourceHandler(cfg, scimLogger) + dsClient, err := directory.GetDirectoryClient(&cfg.Directory) + if err != nil { + return err + } + + transformCfg, err := convert.NewTransformConfig(&cfg.SCIM) + if err != nil { + return err + } + + userHandler, err := userHandler(scimLogger, transformCfg, dsClient) if err != nil { return err } @@ -44,7 +58,7 @@ func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) er Handler: userHandler, } - groupHandler, err := groups.NewGroupResourceHandler(cfg, scimLogger) + groupHandler, err := groupHandler(scimLogger, transformCfg, dsClient) if err != nil { return err } @@ -106,6 +120,26 @@ func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) er return srv.ListenAndServe() } +func userHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsClient *ds.Client) (scim.ResourceHandler, error) { + usersLogger := scimLogger.With().Str("component", "users").Logger() + usersResourceHandler, err := users.NewUsersResourceHandler(&usersLogger, cfg, dsClient) + if err != nil { + return nil, err + } + + return NewUsersResourceHandler(usersResourceHandler) +} + +func groupHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsClient *ds.Client) (scim.ResourceHandler, error) { + groupsLogger := scimLogger.With().Str("component", "groups").Logger() + groupsResourceHandler, err := groups.NewGroupResourceHandler(&groupsLogger, cfg, dsClient) + if err != nil { + return nil, err + } + + return NewGroupResourceHandler(groupsResourceHandler) +} + type application struct { cfg *config.AuthConfig } diff --git a/pkg/app/users.go b/pkg/app/users.go new file mode 100644 index 0000000..913753a --- /dev/null +++ b/pkg/app/users.go @@ -0,0 +1,42 @@ +package app + +import ( + "net/http" + + "github.com/aserto-dev/scim/common/handlers/users" + "github.com/elimity-com/scim" +) + +type UsersResourceHandler struct { + handler *users.UsersResourceHandler +} + +func NewUsersResourceHandler(handler *users.UsersResourceHandler) (*UsersResourceHandler, error) { + return &UsersResourceHandler{ + handler: handler, + }, nil +} + +func (u UsersResourceHandler) Create(r *http.Request, attributes scim.ResourceAttributes) (scim.Resource, error) { + return u.handler.Create(r.Context(), attributes) +} + +func (u UsersResourceHandler) Delete(r *http.Request, id string) error { + return u.handler.Delete(r.Context(), id) +} + +func (u UsersResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) { + return u.handler.Get(r.Context(), id) +} + +func (u UsersResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { + return u.handler.GetAll(r.Context(), params) +} + +func (u UsersResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) { + return u.handler.Patch(r.Context(), id, operations) +} + +func (u UsersResourceHandler) Replace(r *http.Request, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { + return u.handler.Replace(r.Context(), id, attributes) +} diff --git a/pkg/common/convert.go b/pkg/common/convert.go deleted file mode 100644 index 9e53fc4..0000000 --- a/pkg/common/convert.go +++ /dev/null @@ -1,115 +0,0 @@ -package common - -import ( - "encoding/json" - - dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" - "github.com/elimity-com/scim" - "github.com/elimity-com/scim/optional" - "google.golang.org/protobuf/types/known/structpb" -) - -func ObjectToResource(object *dsc.Object, meta scim.Meta) scim.Resource { - eID := optional.String{} - attr := object.Properties.AsMap() - delete(attr, "password") - - return scim.Resource{ - ID: object.Id, - ExternalID: eID, - Attributes: attr, - Meta: meta, - } -} - -func ResourceAttributesToObject(resourceAttributes scim.ResourceAttributes, objectType, id string) (*dsc.Object, error) { - props, err := structpb.NewStruct(resourceAttributes) - if err != nil { - return nil, err - } - - var displayName string - if resourceAttributes["displayName"] != nil { - displayName = resourceAttributes["displayName"].(string) - } else { - displayName = id - } - - object := &dsc.Object{ - Type: objectType, - Properties: props, - Id: id, - DisplayName: displayName, - } - return object, nil -} - -func ResourceAttributesToUser(attributes scim.ResourceAttributes) (*User, error) { - var user User - data, err := json.Marshal(attributes) - if err != nil { - return &User{}, err - } - - if err := json.Unmarshal(data, &user); err != nil { - return &User{}, err - } - return &user, nil -} - -func ToResourceAttributes(value interface{}) (result scim.ResourceAttributes, err error) { - data, err := json.Marshal(value) - if err != nil { - return nil, err - } - err = json.Unmarshal(data, &result) - return -} - -func UserToResource(meta scim.Meta, user *User) (scim.Resource, error) { - attributes, err := ToResourceAttributes(&user) - if err != nil { - return scim.Resource{}, err - } - eID := optional.String{} - if user.ExternalID != "" { - eID = optional.NewString(user.ExternalID) - } - return scim.Resource{ - ID: user.ID, - ExternalID: eID, - Attributes: attributes, - Meta: meta, - }, nil -} - -func UserToObject(user *User) (*dsc.Object, error) { - attributes, err := ToResourceAttributes(&user) - if err != nil { - return nil, err - } - props, err := structpb.NewStruct(attributes) - if err != nil { - return nil, err - } - - userID := user.ID - if userID == "" { - userID = user.UserName - } - - displayName := user.DisplayName - if displayName == "" { - displayName = userID - } - - props.Fields["enabled"] = structpb.NewBoolValue(user.Active) - - object := &dsc.Object{ - Type: "user", - Properties: props, - Id: userID, - DisplayName: displayName, - } - return object, nil -} diff --git a/pkg/config/config.go b/pkg/config/config.go index bb0c021..811191a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,6 +6,7 @@ import ( client "github.com/aserto-dev/go-aserto" "github.com/aserto-dev/logger" + config "github.com/aserto-dev/scim/common/config" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -26,29 +27,7 @@ type Config struct { Auth AuthConfig `json:"auth"` } `json:"server"` - SCIM struct { - CreateEmailIdentities bool `json:"create_email_identities"` - CreateRoleGroups bool `json:"create_role_groups"` - GroupMappings []ObjectMapping `json:"group_mappings"` - UserMappings []ObjectMapping `json:"user_mappings"` - UserObjectType string `json:"user_object_type"` - GroupMemberRelation string `json:"group_member_relation"` - GroupObjectType string `json:"group_object_type"` - IdentityObjectType string `json:"identity_object_type"` - IdentityRelation string `json:"identity_relation"` - Identity struct { - ObjectType string - Relation string - } `json:"-"` - } `json:"scim"` -} - -type ObjectMapping struct { - SubjectID string `json:"subject_id"` - ObjectType string `json:"object_type"` - ObjectID string `json:"object_id"` - Relation string `json:"relation"` - SubjectRelation string `json:"subject_relation"` + SCIM config.SCIMConfig `json:"scim"` } type AuthConfig struct { @@ -142,37 +121,7 @@ func NewConfig(configPath string) (*Config, error) { // nolint // function will } func (cfg *Config) Validate() error { - if cfg.SCIM.UserObjectType == "" { - return errors.Wrap(ErrInvalidConfig, "scim.user_object_type is required") - } - if cfg.SCIM.IdentityObjectType == "" { - return errors.Wrap(ErrInvalidConfig, "scim.identity_object_type is required") - } - if cfg.SCIM.IdentityRelation == "" { - return errors.Wrap(ErrInvalidConfig, "scim.identity_relation is required") - } else { - object, relation, found := strings.Cut(cfg.SCIM.IdentityRelation, "#") - if !found { - return errors.Wrap(ErrInvalidConfig, "identity relation must be in the format object#relation") - } - if object != cfg.SCIM.IdentityObjectType && object != cfg.SCIM.UserObjectType { - return errors.Wrapf(ErrInvalidConfig, "identity relation object type [%s] doesn't match user or identity type", object) - } - if relation == "" { - return errors.Wrap(ErrInvalidConfig, "identity relation relation is required") - } - - cfg.SCIM.Identity.ObjectType = object - cfg.SCIM.Identity.Relation = relation - } - if cfg.SCIM.GroupObjectType == "" { - return errors.Wrap(ErrInvalidConfig, "scim.group_object_type is required") - } - if cfg.SCIM.GroupMemberRelation == "" { - return errors.Wrap(ErrInvalidConfig, "scim.group_member_relation is required") - } - - return nil + return cfg.SCIM.Validate() } func fileExists(path string) (bool, error) { From 04f218bbda717c0a4b3bf501fa502533b746e9f2 Mon Sep 17 00:00:00 2001 From: florindragos Date: Wed, 26 Mar 2025 14:43:27 +0200 Subject: [PATCH 02/13] update lint --- .golangci.yaml | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index e78086a..ab4b0da 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,14 +2,10 @@ # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration +run: + timeout: 5m + linters-settings: - depguard: - list-type: blacklist - packages: - # logging is allowed only by zerolog - - github.com/sirupsen/logrus - packages-with-error-message: - - github.com/sirupsen/logrus: "logging is allowed only by zerolog" dupl: threshold: 100 funlen: @@ -35,16 +31,7 @@ linters-settings: min-complexity: 18 goimports: local-prefixes: github.com/golangci/golangci-lint - golint: - min-confidence: 0 - gomnd: - checks: - - argument - - case - - condition - - return govet: - shadow: true settings: printf: funcs: @@ -52,8 +39,6 @@ linters-settings: - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - maligned: - suggest-new: true misspell: locale: US nolintlint: @@ -68,9 +53,10 @@ linters: enable: - asciicheck - bodyclose + - copyloopvar - dogsled - errcheck - - copyloopvar + - errname - exhaustive - funlen - gochecknoinits @@ -78,7 +64,7 @@ linters: - gocritic - gocyclo - godot - - godox + - gosimple - err113 - gofmt - goimports @@ -86,6 +72,7 @@ linters: - gosec - gosimple - govet + - importas - ineffassign - misspell - nakedret @@ -93,17 +80,20 @@ linters: - rowserrcheck - staticcheck - stylecheck + - testifylint - testpackage - typecheck - unconvert - unparam - unused + - wastedassign # don't enable: # - depguard # - dupl # - gochecknoglobals # - gocognit + # - godox # - gomnd # - lll # - nestif @@ -145,6 +135,13 @@ issues: text: "hugeParam:" linters: - gocritic + # integer overflow conversion + - text: "G115" + linters: + - gosec - text: "G404" linters: - - gosec \ No newline at end of file + - gosec + - text: "SA1019: \\S+ is deprecated" + linters: + - staticcheck \ No newline at end of file From 1407f69d5037c6c97d16d4bb531b6c8c378d09ae Mon Sep 17 00:00:00 2001 From: florindragos Date: Fri, 28 Mar 2025 15:36:06 +0200 Subject: [PATCH 03/13] small fixes --- common/assets/users-groups-roles.tmpl | 2 +- common/assets/users-groups.tmpl | 2 +- common/assets/users.tmpl | 2 +- common/config/config.go | 7 ------- common/directory/client.go | 13 ++++++++++--- pkg/config/config.go | 13 +++++++------ 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/common/assets/users-groups-roles.tmpl b/common/assets/users-groups-roles.tmpl index 0733fe9..ff73f43 100644 --- a/common/assets/users-groups-roles.tmpl +++ b/common/assets/users-groups-roles.tmpl @@ -110,7 +110,7 @@ "subject_id": "{{ $subjId }}" } {{ end }} - {{ if and ($.input.manager_relation) (ne $.input.manager_relation "") }} + {{ if and ($.vars.user.manager_relation) (ne $.vars.user.manager_relation "") }} {{ $manager := index .input "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }} {{ if $manager }} {{ if and ($manager.manager.value) (ne $manager.manager.value "") }} diff --git a/common/assets/users-groups.tmpl b/common/assets/users-groups.tmpl index 44d665a..feb0acc 100644 --- a/common/assets/users-groups.tmpl +++ b/common/assets/users-groups.tmpl @@ -96,7 +96,7 @@ "subject_id": "{{ $subjId }}" } {{ end }} - {{ if and ($.input.manager_relation) (ne $.input.manager_relation "") }} + {{ if and ($.vars.user.manager_relation) (ne $.vars.user.manager_relation "") }} {{ $manager := index .input "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }} {{ if $manager }} {{ if and ($manager.manager.value) (ne $manager.manager.value "") }} diff --git a/common/assets/users.tmpl b/common/assets/users.tmpl index 4fcb933..42016b0 100644 --- a/common/assets/users.tmpl +++ b/common/assets/users.tmpl @@ -90,7 +90,7 @@ "subject_id": "{{ $subjId }}" } {{ end }} - {{ if and ($.input.manager_relation) (ne $.input.manager_relation "") }} + {{ if and ($.vars.user.manager_relation) (ne $.vars.user.manager_relation "") }} {{ $manager := index .input "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }} {{ if $manager }} {{ if and ($manager.manager.value) (ne $manager.manager.value "") }} diff --git a/common/config/config.go b/common/config/config.go index 2a0a6cb..084f050 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -13,10 +13,6 @@ type SCIMConfig struct { Group *GroupOptions `json:"group"` Role *RoleOptions `json:"role"` Relations []*Relation `json:"relations"` - Identity struct { - ObjectType string - Relation string - } `json:"-"` } type UserOptions struct { @@ -67,9 +63,6 @@ func (cfg *SCIMConfig) Validate() error { if relation == "" { return errors.Wrap(ErrInvalidConfig, "identity relation relation is required") } - - cfg.Identity.ObjectType = object - cfg.Identity.Relation = relation } if cfg.Group != nil { if cfg.Group.ObjectType == "" { diff --git a/common/directory/client.go b/common/directory/client.go index 8dedbce..f78eb62 100644 --- a/common/directory/client.go +++ b/common/directory/client.go @@ -40,10 +40,17 @@ func (s *Client) DS() *ds.Client { func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform, userAttributes scim.ResourceAttributes) (scim.Meta, error) { logger := s.logger.With().Str("method", "SetUser").Str("id", userID).Logger() logger.Trace().Msg("set user") + idRelation, err := s.cfg.GetIdentityRelation(userID, "") + if err != nil { + return scim.Meta{}, err + } + relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ - ObjectType: s.cfg.User.ObjectType, - ObjectId: userID, - Relation: s.cfg.User.IdentityRelation, + ObjectType: idRelation.ObjectType, + ObjectId: idRelation.ObjectId, + Relation: idRelation.Relation, + SubjectType: idRelation.SubjectType, + SubjectId: idRelation.SubjectId, WithObjects: false, WithEmptySubjectRelation: true, }) diff --git a/pkg/config/config.go b/pkg/config/config.go index 811191a..01634e1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -70,12 +70,13 @@ func NewConfig(configPath string) (*Config, error) { // nolint // function will v.SetDefault("server.auth.basic.enabled", "false") v.SetDefault("server.auth.bearer.enabled", "false") - v.SetDefault("scim.create_email_identities", true) - v.SetDefault("scim.user_object_type", "user") - v.SetDefault("scim.identity_object_type", "identity") - v.SetDefault("scim.identity_relation", "user#identifier") - v.SetDefault("scim.group_object_type", "group") - v.SetDefault("scim.group_member_relation", "member") + v.SetDefault("scim.user.object_type", "user") + v.SetDefault("scim.user.identity_object_type", "identity") + v.SetDefault("scim.user.identity_relation", "user#identifier") + v.SetDefault("scim.user.source_object_type", "scim-user") + v.SetDefault("scim.group.object_type", "group") + v.SetDefault("scim.group.group_member_relation", "member") + v.SetDefault("scim.group.source_object_type", "scim-group") // Allow setting via env vars. v.SetDefault("directory.api_key", "") From a76012b9d53c1cb66c1b63713ef6d99b836af70f Mon Sep 17 00:00:00 2001 From: florindragos Date: Thu, 3 Apr 2025 15:45:05 +0300 Subject: [PATCH 04/13] fix patch ops --- common/handlers/groups/patch.go | 9 ++++----- common/handlers/users/patch.go | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/common/handlers/groups/patch.go b/common/handlers/groups/patch.go index 5c7e8cc..2959f31 100644 --- a/common/handlers/groups/patch.go +++ b/common/handlers/groups/patch.go @@ -38,25 +38,24 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ } converter := convert.NewConverter(g.cfg) - var attr scim.ResourceAttributes - oldAttr := converter.ObjectToResourceAttributes(getObjResp.Result) + attr := converter.ObjectToResourceAttributes(getObjResp.Result) for _, op := range operations { switch op.Op { case scim.PatchOperationAdd: - attr, err = common.HandlePatchOPAdd(oldAttr, op) + attr, err = common.HandlePatchOPAdd(attr, op) if err != nil { logger.Err(err).Msg("error adding property") return scim.Resource{}, err } case scim.PatchOperationRemove: - attr, err = common.HandlePatchOPRemove(oldAttr, op) + attr, err = common.HandlePatchOPRemove(attr, op) if err != nil { logger.Err(err).Msg("error removing property") return scim.Resource{}, err } case scim.PatchOperationReplace: - attr, err = common.HandlePatchOPReplace(oldAttr, op) + attr, err = common.HandlePatchOPReplace(attr, op) if err != nil { logger.Err(err).Msg("error replacing property") return scim.Resource{}, err diff --git a/common/handlers/users/patch.go b/common/handlers/users/patch.go index 730eb1a..af1eaea 100644 --- a/common/handlers/users/patch.go +++ b/common/handlers/users/patch.go @@ -35,25 +35,24 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ return scim.Resource{}, err } - var attr scim.ResourceAttributes - oldAttr := converter.ObjectToResourceAttributes(getObjResp.Result) + attr := converter.ObjectToResourceAttributes(getObjResp.Result) for _, op := range operations { switch op.Op { case scim.PatchOperationAdd: - attr, err = common.HandlePatchOPAdd(oldAttr, op) + attr, err = common.HandlePatchOPAdd(attr, op) if err != nil { logger.Err(err).Msg("error adding property") return scim.Resource{}, err } case scim.PatchOperationRemove: - attr, err = common.HandlePatchOPRemove(oldAttr, op) + attr, err = common.HandlePatchOPRemove(attr, op) if err != nil { logger.Err(err).Msg("error removing property") return scim.Resource{}, err } case scim.PatchOperationReplace: - attr, err = common.HandlePatchOPReplace(oldAttr, op) + attr, err = common.HandlePatchOPReplace(attr, op) if err != nil { logger.Err(err).Msg("error replacing property") return scim.Resource{}, err From 15b696db782e5be57f9c661366f838d6e1408d65 Mon Sep 17 00:00:00 2001 From: florindragos Date: Thu, 3 Apr 2025 15:53:50 +0300 Subject: [PATCH 05/13] fix error logs --- common/handlers/groups/delete.go | 2 +- common/handlers/groups/get.go | 4 ++-- common/handlers/groups/patch.go | 16 ++++++++-------- common/handlers/users/create.go | 6 +++--- common/handlers/users/delete.go | 10 +++++----- common/handlers/users/get.go | 4 ++-- common/handlers/users/patch.go | 16 ++++++++-------- common/handlers/users/replace.go | 4 ++-- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/common/handlers/groups/delete.go b/common/handlers/groups/delete.go index 3040d95..b1219f6 100644 --- a/common/handlers/groups/delete.go +++ b/common/handlers/groups/delete.go @@ -8,7 +8,7 @@ func (g GroupResourceHandler) Delete(ctx context.Context, id string) error { err := g.dirClient.DeleteGroup(ctx, id) if err != nil { - logger.Err(err).Msg("failed to delete group") + logger.Error().Err(err).Msg("failed to delete group") return err } diff --git a/common/handlers/groups/get.go b/common/handlers/groups/get.go index 9af6843..8217c86 100644 --- a/common/handlers/groups/get.go +++ b/common/handlers/groups/get.go @@ -25,7 +25,7 @@ func (g GroupResourceHandler) Get(ctx context.Context, id string) (scim.Resource WithRelations: false, }) if err != nil { - logger.Err(err).Msg("failed to get group") + logger.Error().Err(err).Msg("failed to get group") return scim.Resource{}, err } @@ -62,7 +62,7 @@ func (g GroupResourceHandler) GetAll(ctx context.Context, params scim.ListReques }, }) if err != nil { - logger.Err(err).Msg("failed to read groups") + logger.Error().Err(err).Msg("failed to read groups") return scim.Page{}, err } diff --git a/common/handlers/groups/patch.go b/common/handlers/groups/patch.go index 2959f31..2edce28 100644 --- a/common/handlers/groups/patch.go +++ b/common/handlers/groups/patch.go @@ -30,7 +30,7 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ WithRelations: false, }) if err != nil { - logger.Err(err).Msg("failed to get group") + logger.Error().Err(err).Msg("failed to get group") if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } @@ -45,19 +45,19 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ case scim.PatchOperationAdd: attr, err = common.HandlePatchOPAdd(attr, op) if err != nil { - logger.Err(err).Msg("error adding property") + logger.Error().Err(err).Msg("error adding property") return scim.Resource{}, err } case scim.PatchOperationRemove: attr, err = common.HandlePatchOPRemove(attr, op) if err != nil { - logger.Err(err).Msg("error removing property") + logger.Error().Err(err).Msg("error removing property") return scim.Resource{}, err } case scim.PatchOperationReplace: attr, err = common.HandlePatchOPReplace(attr, op) if err != nil { - logger.Err(err).Msg("error replacing property") + logger.Error().Err(err).Msg("error replacing property") return scim.Resource{}, err } } @@ -65,14 +65,14 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ transformResult, err := converter.TransformResource(attr, "group") if err != nil { - logger.Err(err).Msg("failed to convert group to object") + logger.Error().Err(err).Msg("failed to convert group to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } groupObj := getObjResp.Result props, err := structpb.NewStruct(attr) if err != nil { - logger.Err(err).Msg("failed to convert attributes to struct") + logger.Error().Err(err).Msg("failed to convert attributes to struct") return scim.Resource{}, err } groupObj.Properties = props @@ -80,13 +80,13 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ Object: groupObj, }) if err != nil { - logger.Err(err).Msg("failed to replace group") + logger.Error().Err(err).Msg("failed to replace group") return scim.Resource{}, err } meta, err := g.dirClient.SetGroup(ctx, getObjResp.Result.Id, transformResult) if err != nil { - logger.Err(err).Msg("failed to sync group") + logger.Error().Err(err).Msg("failed to sync group") return scim.Resource{}, err } diff --git a/common/handlers/users/create.go b/common/handlers/users/create.go index 32560cd..97a3f27 100644 --- a/common/handlers/users/create.go +++ b/common/handlers/users/create.go @@ -28,14 +28,14 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour converter := convert.NewConverter(u.cfg) object, err := converter.SCIMUserToObject(user) if err != nil { - logger.Err(err).Msg("failed to convert user to object") + logger.Error().Err(err).Msg("failed to convert user to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } sourceUserResp, err := u.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: object, }) if err != nil { - logger.Err(err).Msg("failed to create user") + logger.Error().Err(err).Msg("failed to create user") return scim.Resource{}, err } @@ -53,7 +53,7 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour meta, err := u.dirClient.SetUser(ctx, sourceUserResp.Result.Id, transformResult, attributes) if err != nil { - logger.Err(err).Msg("failed to sync user") + logger.Error().Err(err).Msg("failed to sync user") return scim.Resource{}, err } diff --git a/common/handlers/users/delete.go b/common/handlers/users/delete.go index 9493764..c8888d4 100644 --- a/common/handlers/users/delete.go +++ b/common/handlers/users/delete.go @@ -17,7 +17,7 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { identityRelation, err := u.cfg.GetIdentityRelation(id, "") if err != nil { - logger.Err(err).Msg("failed to get identity relation") + logger.Error().Err(err).Msg("failed to get identity relation") } resp, err := u.dirClient.DS().Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ @@ -28,7 +28,7 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { Relation: identityRelation.Relation, }) if err != nil { - logger.Err(err).Msg("failed to get relations") + logger.Error().Err(err).Msg("failed to get relations") if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { return serrors.ScimErrorResourceNotFound(id) } @@ -56,7 +56,7 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { WithRelations: true, }) if err != nil { - logger.Err(err).Msg("failed to delete identity") + logger.Error().Err(err).Msg("failed to delete identity") return err } } @@ -68,7 +68,7 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { WithRelations: true, }) if err != nil { - logger.Err(err).Msg("failed to delete user") + logger.Error().Err(err).Msg("failed to delete user") if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { return serrors.ScimErrorResourceNotFound(id) } @@ -81,7 +81,7 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { WithRelations: true, }) if err != nil { - logger.Err(err).Msg("failed to delete user source object") + logger.Error().Err(err).Msg("failed to delete user source object") if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { return serrors.ScimErrorResourceNotFound(id) } diff --git a/common/handlers/users/get.go b/common/handlers/users/get.go index 395a6ee..0199317 100644 --- a/common/handlers/users/get.go +++ b/common/handlers/users/get.go @@ -25,7 +25,7 @@ func (u UsersResourceHandler) Get(ctx context.Context, id string) (scim.Resource WithRelations: false, }) if err != nil { - logger.Err(err).Msg("failed to get user") + logger.Error().Err(err).Msg("failed to get user") if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } @@ -65,7 +65,7 @@ func (u UsersResourceHandler) GetAll(ctx context.Context, params scim.ListReques for { resp, err := u.getUsers(ctx, pageSize, pageToken) if err != nil { - logger.Err(err).Msg("failed to get users") + logger.Error().Err(err).Msg("failed to get users") return scim.Page{}, err } diff --git a/common/handlers/users/patch.go b/common/handlers/users/patch.go index af1eaea..1aefaaf 100644 --- a/common/handlers/users/patch.go +++ b/common/handlers/users/patch.go @@ -42,39 +42,39 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ case scim.PatchOperationAdd: attr, err = common.HandlePatchOPAdd(attr, op) if err != nil { - logger.Err(err).Msg("error adding property") + logger.Error().Err(err).Msg("error adding property") return scim.Resource{}, err } case scim.PatchOperationRemove: attr, err = common.HandlePatchOPRemove(attr, op) if err != nil { - logger.Err(err).Msg("error removing property") + logger.Error().Err(err).Msg("error removing property") return scim.Resource{}, err } case scim.PatchOperationReplace: attr, err = common.HandlePatchOPReplace(attr, op) if err != nil { - logger.Err(err).Msg("error replacing property") + logger.Error().Err(err).Msg("error replacing property") return scim.Resource{}, err } } } if err != nil { - logger.Err(err).Msg("error handling patch operation") + logger.Error().Err(err).Msg("error handling patch operation") return scim.Resource{}, err } transformResult, err := converter.TransformResource(attr, "user") if err != nil { - logger.Err(err).Msg("failed to convert user to object") + logger.Error().Err(err).Msg("failed to convert user to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } userObj := getObjResp.Result props, err := structpb.NewStruct(attr) if err != nil { - logger.Err(err).Msg("failed to convert resource attributes to struct") + logger.Error().Err(err).Msg("failed to convert resource attributes to struct") return scim.Resource{}, err } userObj.Properties = props @@ -82,13 +82,13 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ Object: userObj, }) if err != nil { - logger.Err(err).Msg("failed to replace user") + logger.Error().Err(err).Msg("failed to replace user") return scim.Resource{}, err } meta, err := u.dirClient.SetUser(ctx, getObjResp.Result.Id, transformResult, attr) if err != nil { - logger.Err(err).Msg("failed to sync user") + logger.Error().Err(err).Msg("failed to sync user") return scim.Resource{}, err } diff --git a/common/handlers/users/replace.go b/common/handlers/users/replace.go index 7f888ac..f443eff 100644 --- a/common/handlers/users/replace.go +++ b/common/handlers/users/replace.go @@ -13,13 +13,13 @@ func (u UsersResourceHandler) Replace(ctx context.Context, id string, attributes err := u.Delete(ctx, id) if err != nil { - logger.Err(err).Msg("failed to delete user") + logger.Error().Err(err).Msg("failed to delete user") return scim.Resource{}, err } resource, err := u.Create(ctx, attributes) if err != nil { - logger.Err(err).Msg("failed to create user") + logger.Error().Err(err).Msg("failed to create user") return scim.Resource{}, err } From 5b8d57c328053e38b81b40866eccb9a7fb501700 Mon Sep 17 00:00:00 2001 From: florindragos Date: Tue, 8 Apr 2025 15:45:52 +0300 Subject: [PATCH 06/13] fix issues --- common/assets.go | 15 +------- common/config/config.go | 39 ++++++++++--------- common/convert/config.go | 22 ++++------- common/convert/convert.go | 65 ++++++++++---------------------- common/convert/converter_test.go | 8 ++-- common/directory/client.go | 31 +++++++-------- common/handlers/groups/create.go | 4 +- common/handlers/groups/get.go | 4 +- common/handlers/groups/patch.go | 2 +- common/handlers/users/create.go | 5 ++- pkg/config/config.go | 2 +- 11 files changed, 80 insertions(+), 117 deletions(-) diff --git a/common/assets.go b/common/assets.go index 353b865..6943707 100644 --- a/common/assets.go +++ b/common/assets.go @@ -8,18 +8,7 @@ import ( //go:embed assets/* var staticAssets embed.FS -func Assets() embed.FS { - return staticAssets -} - -func GetTemplateContent(templateName string) ([]byte, error) { - var templateContent []byte - var err error +func LoadTemplate(templateName string) ([]byte, error) { templateFile := fmt.Sprintf("assets/%s.tmpl", templateName) - templateContent, err = Assets().ReadFile(templateFile) - if err != nil { - return nil, err - } - - return templateContent, nil + return staticAssets.ReadFile(templateFile) } diff --git a/common/config/config.go b/common/config/config.go index 084f050..7192eb1 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -8,14 +8,14 @@ import ( var ErrInvalidConfig = errors.New("invalid config") -type SCIMConfig struct { - User *UserOptions `json:"user"` - Group *GroupOptions `json:"group"` - Role *RoleOptions `json:"role"` - Relations []*Relation `json:"relations"` +type Config struct { + User *User `json:"user"` + Group *Group `json:"group"` + Role *Role `json:"role"` + Relations []*Relation `json:"relations"` } -type UserOptions struct { +type User struct { ObjectType string `json:"object_type"` IdentityObjectType string `json:"identity_object_type"` IdentityRelation string `json:"identity_relation"` @@ -24,12 +24,12 @@ type UserOptions struct { ManagerRelation string `json:"manager_relation"` } -type GroupOptions struct { +type Group struct { ObjectType string `json:"object_type"` GroupMemberRelation string `json:"group_member_relation"` SourceObjectType string `json:"source_object_type"` } -type RoleOptions struct { +type Role struct { ObjectType string `json:"object_type"` RoleRelation string `json:"role_relation"` } @@ -43,7 +43,7 @@ type Relation struct { SubjectRelation string `json:"subject_relation"` } -func (cfg *SCIMConfig) Validate() error { +func (cfg *Config) Validate() error { if cfg.User.ObjectType == "" { return errors.Wrap(ErrInvalidConfig, "scim.user_object_type is required") } @@ -52,17 +52,16 @@ func (cfg *SCIMConfig) Validate() error { } if cfg.User.IdentityRelation == "" { return errors.Wrap(ErrInvalidConfig, "scim.identity_relation is required") - } else { - object, relation, found := strings.Cut(cfg.User.IdentityRelation, "#") - if !found { - return errors.Wrap(ErrInvalidConfig, "identity relation must be in the format object#relation") - } - if object != cfg.User.IdentityObjectType && object != cfg.User.ObjectType { - return errors.Wrapf(ErrInvalidConfig, "identity relation object type [%s] doesn't match user or identity type", object) - } - if relation == "" { - return errors.Wrap(ErrInvalidConfig, "identity relation relation is required") - } + } + object, relation, found := strings.Cut(cfg.User.IdentityRelation, "#") + if !found { + return errors.Wrap(ErrInvalidConfig, "identity relation must be in the format object#relation") + } + if object != cfg.User.IdentityObjectType && object != cfg.User.ObjectType { + return errors.Wrapf(ErrInvalidConfig, "identity relation object type [%s] doesn't match user or identity type", object) + } + if relation == "" { + return errors.Wrap(ErrInvalidConfig, "identity relation relation is required") } if cfg.Group != nil { if cfg.Group.ObjectType == "" { diff --git a/common/convert/config.go b/common/convert/config.go index 6cb97e1..a68dfdf 100644 --- a/common/convert/config.go +++ b/common/convert/config.go @@ -33,13 +33,13 @@ func (t TemplateName) String() string { } type TransformConfig struct { - template TemplateName - *config.SCIMConfig + *config.Config + template TemplateName IdentityObjectType string `json:"identity_object_type,omitempty"` IdentityRelation string `json:"identity_relation,omitempty"` } -func NewTransformConfig(cfg *config.SCIMConfig) (*TransformConfig, error) { +func NewTransformConfig(cfg *config.Config) (*TransformConfig, error) { template := Users if cfg.Group != nil { @@ -61,15 +61,15 @@ func NewTransformConfig(cfg *config.SCIMConfig) (*TransformConfig, error) { } return &TransformConfig{ - SCIMConfig: cfg, + Config: cfg, template: template, IdentityObjectType: object, IdentityRelation: relation, }, nil } -func (c *TransformConfig) Groups() bool { - return c.SCIMConfig.Group != nil +func (c *TransformConfig) HasGroups() bool { + return c.Config.Group != nil } func (c *TransformConfig) ToTemplateVars() (map[string]interface{}, error) { @@ -79,8 +79,7 @@ func (c *TransformConfig) ToTemplateVars() (map[string]interface{}, error) { if err != nil { return nil, errors.Wrap(err, "failed to marshal ScimConfig to json") } - err = json.Unmarshal(cfg, &result) - if err != nil { + if err := json.Unmarshal(cfg, &result); err != nil { return nil, errors.Wrap(err, "failed to unmarshal ScimConfig to map") } @@ -88,12 +87,7 @@ func (c *TransformConfig) ToTemplateVars() (map[string]interface{}, error) { } func (c *TransformConfig) GetTemplate() ([]byte, error) { - template, err := common.GetTemplateContent(c.template.String()) - if err != nil { - return nil, err - } - - return template, nil + return common.LoadTemplate(c.template.String()) } func (c *TransformConfig) GetIdentityRelation(userID, identity string) (*dsc.Relation, error) { diff --git a/common/convert/convert.go b/common/convert/convert.go index 9c84a55..4aeb4b9 100644 --- a/common/convert/convert.go +++ b/common/convert/convert.go @@ -10,6 +10,7 @@ import ( "github.com/elimity-com/scim" "github.com/elimity-com/scim/optional" "github.com/pkg/errors" + "github.com/samber/lo" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) @@ -47,44 +48,18 @@ func (c *Converter) ObjectToResourceAttributes(object *dsc.Object) scim.Resource return attr } -func ResourceAttributesToUser(attributes scim.ResourceAttributes) (*model.User, error) { - var user model.User - data, err := json.Marshal(attributes) +func Unmarshal[S any, D any](source S, dest *D) error { + data, err := json.Marshal(source) if err != nil { - return &model.User{}, err + return err } - if err := json.Unmarshal(data, &user); err != nil { - return &model.User{}, err - } - return &user, nil -} - -func ResourceAttributesToGroup(attributes scim.ResourceAttributes) (*model.Group, error) { - var group model.Group - data, err := json.Marshal(attributes) - if err != nil { - return &model.Group{}, err - } - - if err := json.Unmarshal(data, &group); err != nil { - return &model.Group{}, err - } - return &group, nil -} - -func ToResourceAttributes(value interface{}) (scim.ResourceAttributes, error) { - var result scim.ResourceAttributes - data, err := json.Marshal(value) - if err != nil { - return nil, err - } - err = json.Unmarshal(data, &result) - return result, err + return json.Unmarshal(data, dest) } func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { - attributes, err := ToResourceAttributes(&user) + var attributes scim.ResourceAttributes + err := Unmarshal(user, &attributes) if err != nil { return scim.Resource{}, err } @@ -101,7 +76,8 @@ func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { } func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { - attributes, err := ToResourceAttributes(&user) + var attributes scim.ResourceAttributes + err := Unmarshal(user, &attributes) if err != nil { return nil, err } @@ -111,10 +87,7 @@ func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { return nil, err } - userID := user.ID - if userID == "" { - userID = user.UserName - } + userID := lo.Ternary(user.ID != "", user.ID, user.UserName) displayName := user.DisplayName if displayName == "" { @@ -140,7 +113,8 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { return nil, ErrGroupsNotEnabled } - attributes, err := ToResourceAttributes(&group) + var attributes scim.ResourceAttributes + err := Unmarshal(group, &attributes) if err != nil { return nil, err } @@ -173,7 +147,7 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { return object, nil } -func (c *Converter) TransformResource(resource map[string]interface{}, objType string) (*msg.Transform, error) { +func (c *Converter) TransformResource(resource map[string]any, objType string) (*msg.Transform, error) { template, err := c.cfg.GetTemplate() if err != nil { return nil, err @@ -184,20 +158,21 @@ func (c *Converter) TransformResource(resource map[string]interface{}, objType s return nil, err } - transformInput := make(map[string]interface{}) - transformInput["input"] = resource - transformInput["vars"] = vars - transformInput["objectType"] = objType + transformInput := map[string]any{ + "input": resource, + "vars": vars, + "objectType": objType, + } transformer := transform.NewGoTemplateTransform(template) return transformer.TransformObject(transformInput) } -func ProtobufStructToMap(s *structpb.Struct) (map[string]interface{}, error) { +func ProtobufStructToMap(s *structpb.Struct) (map[string]any, error) { b, err := protojson.Marshal(s) if err != nil { return nil, err } - m := make(map[string]interface{}) + m := make(map[string]any) err = json.Unmarshal(b, &m) if err != nil { return nil, err diff --git a/common/convert/converter_test.go b/common/convert/converter_test.go index 0b25132..bc3eaf9 100644 --- a/common/convert/converter_test.go +++ b/common/convert/converter_test.go @@ -32,8 +32,8 @@ var ScimUser map[string]interface{} = map[string]interface{}{ func TestTransform(t *testing.T) { assert := require.New(t) - cfg := config.SCIMConfig{ - User: &config.UserOptions{ + cfg := config.Config{ + User: &config.User{ IdentityObjectType: "identity", IdentityRelation: "identity#identitifier", ObjectType: "user", @@ -69,8 +69,8 @@ func TestTransform(t *testing.T) { func TestTransformUserIdentifier(t *testing.T) { assert := require.New(t) - cfg := config.SCIMConfig{ - User: &config.UserOptions{ + cfg := config.Config{ + User: &config.User{ IdentityObjectType: "identity", IdentityRelation: "user#identitifier", ObjectType: "user", diff --git a/common/directory/client.go b/common/directory/client.go index f78eb62..013b41a 100644 --- a/common/directory/client.go +++ b/common/directory/client.go @@ -15,6 +15,7 @@ import ( "github.com/aserto-dev/scim/common/convert" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" + "github.com/hashicorp/go-multierror" "github.com/rs/zerolog" "google.golang.org/protobuf/types/known/structpb" ) @@ -75,23 +76,23 @@ func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform } } - if relations != nil { - for _, rel := range relations.Results { - if !slices.Contains(addedIdentities, rel.ObjectId) { - logger.Trace().Str("id", rel.ObjectId).Msg("deleting identity") - _, err := s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ - ObjectType: s.cfg.User.IdentityObjectType, - ObjectId: rel.ObjectId, - WithRelations: true, - }) - if err != nil { - return result, err - } + mErr := &multierror.Error{} + for _, rel := range relations.GetResults() { + if !slices.Contains(addedIdentities, rel.ObjectId) { + logger.Trace().Str("identity", rel.ObjectId).Msg("deleting identity") + _, err := s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectType: s.cfg.User.IdentityObjectType, + ObjectId: rel.ObjectId, + WithRelations: true, + }) + if err != nil { + mErr = multierror.Append(mErr, err) + logger.Error().Err(err).Str("identity", rel.ObjectId).Msg("failed to delete identity") } } } - return result, nil + return result, mErr.ErrorOrNil() } func (s *Client) importObjects(ctx context.Context, objects []*dsc.Object, userAttributes scim.ResourceAttributes) (scim.Meta, []string, error) { @@ -101,9 +102,9 @@ func (s *Client) importObjects(ctx context.Context, objects []*dsc.Object, userA for _, object := range objects { if object.Type == s.cfg.User.ObjectType { - var userProperties map[string]interface{} + var userProperties map[string]any if object.Properties == nil { - userProperties = make(map[string]interface{}) + userProperties = make(map[string]any) } else { userProperties = object.Properties.AsMap() } diff --git a/common/handlers/groups/create.go b/common/handlers/groups/create.go index 60dc497..5d36b94 100644 --- a/common/handlers/groups/create.go +++ b/common/handlers/groups/create.go @@ -5,6 +5,7 @@ import ( dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" "github.com/aserto-dev/scim/common/convert" + "github.com/aserto-dev/scim/common/model" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" ) @@ -18,7 +19,8 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour logger.Info().Msg("create group") logger.Trace().Any("attributes", attributes).Msg("creating group") - group, err := convert.ResourceAttributesToGroup(attributes) + var group *model.Group + err := convert.Unmarshal(attributes, group) if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to group") return scim.Resource{}, serrors.ScimErrorInvalidSyntax diff --git a/common/handlers/groups/get.go b/common/handlers/groups/get.go index 8217c86..af1d87c 100644 --- a/common/handlers/groups/get.go +++ b/common/handlers/groups/get.go @@ -14,7 +14,7 @@ func (g GroupResourceHandler) Get(ctx context.Context, id string) (scim.Resource logger := g.logger.With().Str("method", "Get").Str("id", id).Logger() logger.Info().Msg("get group") - if !g.cfg.Groups() { + if !g.cfg.HasGroups() { logger.Error().Msg("groups not enabled") return scim.Resource{}, serrors.ScimErrorBadRequest("groups not enabled") } @@ -50,7 +50,7 @@ func (g GroupResourceHandler) GetAll(ctx context.Context, params scim.ListReques resources = make([]scim.Resource, 0) ) - if !g.cfg.Groups() { + if !g.cfg.HasGroups() { logger.Error().Msg("groups not enabled") return scim.Page{}, serrors.ScimErrorBadRequest("groups not enabled") } diff --git a/common/handlers/groups/patch.go b/common/handlers/groups/patch.go index 2edce28..3797678 100644 --- a/common/handlers/groups/patch.go +++ b/common/handlers/groups/patch.go @@ -19,7 +19,7 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ logger := g.logger.With().Str("method", "Patch").Str("id", id).Logger() logger.Info().Msg("patch group") - if !g.cfg.Groups() { + if !g.cfg.HasGroups() { logger.Error().Msg("groups not enabled") return scim.Resource{}, serrors.ScimErrorBadRequest("groups not enabled") } diff --git a/common/handlers/users/create.go b/common/handlers/users/create.go index 97a3f27..249f668 100644 --- a/common/handlers/users/create.go +++ b/common/handlers/users/create.go @@ -5,6 +5,7 @@ import ( dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" "github.com/aserto-dev/scim/common/convert" + "github.com/aserto-dev/scim/common/model" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" ) @@ -17,7 +18,9 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour logger := u.logger.With().Str("method", "Create").Str("userName", userName).Logger() logger.Info().Msg("create user") logger.Trace().Any("attributes", attributes).Msg("creating user") - user, err := convert.ResourceAttributesToUser(attributes) + + var user *model.User + err := convert.Unmarshal(attributes, user) if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to user") return scim.Resource{}, serrors.ScimErrorInvalidSyntax diff --git a/pkg/config/config.go b/pkg/config/config.go index 01634e1..e561858 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -27,7 +27,7 @@ type Config struct { Auth AuthConfig `json:"auth"` } `json:"server"` - SCIM config.SCIMConfig `json:"scim"` + SCIM config.Config `json:"scim"` } type AuthConfig struct { From c5cfa33aa4839058ebded10b12f3dd022f1e3ba3 Mon Sep 17 00:00:00 2001 From: florindragos Date: Mon, 14 Apr 2025 15:59:15 +0300 Subject: [PATCH 07/13] update deps and add tests --- .github/workflows/ci.yaml | 8 +- .golangci.yaml | 238 +++++++++------------- cmd/aserto-scim/main.go | 26 ++- common/config/config.go | 2 +- common/convert/convert.go | 6 +- common/go.mod | 21 +- common/go.sum | 16 ++ common/handlers/groups/create.go | 2 +- common/handlers/groups/patch.go | 8 +- common/handlers/users/create.go | 2 +- common/handlers/users/delete.go | 14 +- common/handlers/users/get.go | 8 +- common/handlers/users/patch.go | 8 +- common/patch.go | 3 + go.mod | 107 ++++++++-- go.sum | 306 ++++++++++++++++++++++++----- go.work | 2 +- makefile | 6 +- pkg/app/run.go | 63 ++++-- pkg/config/config.go | 1 + pkg/test/assets/assets.go | 41 ++++ pkg/test/assets/config/scim.yaml | 36 ++++ pkg/test/assets/config/topaz.yaml | 272 +++++++++++++++++++++++++ pkg/test/assets/data/group.json | 6 + pkg/test/assets/data/manifest.yaml | 17 ++ pkg/test/assets/data/morty.json | 23 +++ pkg/test/assets/data/patch.json | 13 ++ pkg/test/assets/data/rick.json | 18 ++ pkg/test/common/common.go | 194 ++++++++++++++++++ pkg/test/scim_test.go | 53 +++++ 30 files changed, 1254 insertions(+), 266 deletions(-) create mode 100644 pkg/test/assets/assets.go create mode 100644 pkg/test/assets/config/scim.yaml create mode 100644 pkg/test/assets/config/topaz.yaml create mode 100644 pkg/test/assets/data/group.json create mode 100644 pkg/test/assets/data/manifest.yaml create mode 100644 pkg/test/assets/data/morty.json create mode 100644 pkg/test/assets/data/patch.json create mode 100644 pkg/test/assets/data/rick.json create mode 100644 pkg/test/common/common.go create mode 100644 pkg/test/scim_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7f37c41..f7678bb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,10 +16,10 @@ on: env: VAULT_ADDR: https://vault.eng.aserto.com/ PRE_RELEASE: ${{ github.ref == 'refs/heads/main' && 'main' || '' }} - GO_VERSION: "1.23" - GO_RELEASER_VERSION: "v2.3.2" - GO_LANGCI_LINT_VERSION: "v1.64.5" - GO_TESTSUM_VERSION: "1.11.0" + GO_VERSION: "1.24" + GO_RELEASER_VERSION: "v2.8.2" + GO_LANGCI_LINT_VERSION: "v2.0.2" + GO_TESTSUM_VERSION: "1.12.1" jobs: test: diff --git a/.golangci.yaml b/.golangci.yaml index ab4b0da..4f18734 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,147 +1,105 @@ --- -# golangci.com configuration -# https://github.com/golangci/golangci/wiki/Configuration - -run: - timeout: 5m - -linters-settings: - dupl: - threshold: 100 - funlen: - lines: 100 - statements: 80 - goconst: - min-len: 2 - min-occurrences: 2 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - - octalLiteral - - whyNoLint - - wrapperFunc - gocyclo: - min-complexity: 18 - goimports: - local-prefixes: github.com/golangci/golangci-lint - govet: - settings: - printf: - funcs: - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - misspell: - locale: US - nolintlint: - allow-unused: false # report any unused nolint directives - require-explanation: false # don't require an explanation for nolint directives - require-specific: false # don't require nolint directives to be specific about which linter is being skipped +# Configuration +# https://golangci-lint.run/usage/configuration/ + +version: "2" linters: - # please, do not use `enable-all`: it's deprecated and will be removed soon. - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint - disable-all: true - enable: - - asciicheck - - bodyclose - - copyloopvar - - dogsled + # https://golangci-lint.run/usage/configuration/#linters-configuration + default: all + + # explicitly disabled linters + disable: + - containedctx + - contextcheck + - cyclop + - depguard - errcheck - - errname + - exhaustruct - exhaustive - - funlen - - gochecknoinits - - goconst - - gocritic - - gocyclo - - godot - - gosimple - - err113 + - forbidigo + - gochecknoglobals # no configuration options + - nilnil + - nlreturn # redundant with wsl + - paralleltest + - revive + - tagalign + - thelper + - varnamelen + - wrapcheck + + settings: + cyclop: + max-complexity: 12 + + errcheck: + exclude-functions: + - fmt.Fprint + - fmt.Fprintf + - fmt.Fprintln + - os.Close + + funlen: + lines: 80 + statements: 60 + ignore-comments: true + + gomoddirectives: + replace-allow-list: + - github.com/slok/go-http-metrics + + gosec: + excludes: + - G104 # Errors unhandled + - G304 # Potential file inclusion via variable (see https://github.com/golang/go/issues/67002) + + ireturn: + allow: + - error + - empty + - stdlib + - generic + - proto.Message + - plugins.Plugin + - decisionlog.DecisionLogger + - resolvers.DirectoryResolver + - resolvers.RuntimeResolver + - v3.ReaderClient + + lll: + line-length: 150 + + recvcheck: + exclusions: + - "*.Map" + + tagliatelle: + case: + rules: + json: snake + yaml: snake + + overrides: + - pkg: pkg/app/handlers + rules: + json: camel + + exclusions: + generated: lax + + # Paths to exclude + paths: + - internal/pkg/xdg/ + - pkg/cc/signals/ + - pkg/cli/editor/ + + rules: + - path: pkg/cli/cmd/ + linters: + - dupl + +formatters: + enable: - gofmt + - gofumpt - goimports - - goprintffuncname - - gosec - - gosimple - - govet - - importas - - ineffassign - - misspell - - nakedret - - noctx - - rowserrcheck - - staticcheck - - stylecheck - - testifylint - - testpackage - - typecheck - - unconvert - - unparam - - unused - - wastedassign - - # don't enable: - # - depguard - # - dupl - # - gochecknoglobals - # - gocognit - # - godox - # - gomnd - # - lll - # - nestif - # - nolintlint # conflict with 1.19 gofmt changes - # - prealloc - # - revive - # - wsl - # - whitespace - -issues: - # List of regexps of issue texts to exclude, empty list by default. - # But independently from this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. To list all - # excluded by default patterns execute `golangci-lint run --help` - exclude: - - declaration of "(err|ctx)" shadows declaration at - - shadow of imported from 'github.com/stretchr/testify/assert' package 'assert' - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - - path: _test\.go - linters: - - gomnd - # https://github.com/go-critic/go-critic/issues/926 - - text: "unnecessaryDefer:" - linters: - - gocritic - - text: "unnamedResult:" - linters: - - gocritic - - path: \.resolvers\.go - text: "typeDefFirst:" - linters: - - gocritic - - path: \.resolvers\.go - text: "paramTypeCombine:" - linters: - - gocritic - - path: \.resolvers\.go - text: "hugeParam:" - linters: - - gocritic - # integer overflow conversion - - text: "G115" - linters: - - gosec - - text: "G404" - linters: - - gosec - - text: "SA1019: \\S+ is deprecated" - linters: - - staticcheck \ No newline at end of file diff --git a/cmd/aserto-scim/main.go b/cmd/aserto-scim/main.go index b63c3c6..63e531b 100644 --- a/cmd/aserto-scim/main.go +++ b/cmd/aserto-scim/main.go @@ -1,9 +1,12 @@ package main import ( + "context" "fmt" "log" "os" + "os/signal" + "time" "github.com/aserto-dev/scim/pkg/app" "github.com/aserto-dev/scim/pkg/version" @@ -32,7 +35,28 @@ var cmdRun = &cobra.Command{ Use: "run [args]", Short: "Start SCIM service", RunE: func(cmd *cobra.Command, args []string) error { - return app.Run(flagConfigPath, os.Stdout, os.Stderr) + srv, err := app.NewSCIMServer(flagConfigPath, os.Stdout, os.Stderr) + if err != nil { + return err + } + + go func() { + if err := srv.Run(); err != nil { + log.Printf("Error running SCIM server: %v", err) + } + }() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, os.Kill) + <-stop + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + return err + } + log.Println("SCIM server stopped") + return nil }, } diff --git a/common/config/config.go b/common/config/config.go index 7192eb1..087ac1f 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -61,7 +61,7 @@ func (cfg *Config) Validate() error { return errors.Wrapf(ErrInvalidConfig, "identity relation object type [%s] doesn't match user or identity type", object) } if relation == "" { - return errors.Wrap(ErrInvalidConfig, "identity relation relation is required") + return errors.Wrap(ErrInvalidConfig, "identity relation is required") } if cfg.Group != nil { if cfg.Group.ObjectType == "" { diff --git a/common/convert/convert.go b/common/convert/convert.go index 4aeb4b9..032874e 100644 --- a/common/convert/convert.go +++ b/common/convert/convert.go @@ -58,7 +58,7 @@ func Unmarshal[S any, D any](source S, dest *D) error { } func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { - var attributes scim.ResourceAttributes + attributes := scim.ResourceAttributes{} err := Unmarshal(user, &attributes) if err != nil { return scim.Resource{}, err @@ -76,7 +76,7 @@ func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { } func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { - var attributes scim.ResourceAttributes + attributes := scim.ResourceAttributes{} err := Unmarshal(user, &attributes) if err != nil { return nil, err @@ -113,7 +113,7 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { return nil, ErrGroupsNotEnabled } - var attributes scim.ResourceAttributes + attributes := scim.ResourceAttributes{} err := Unmarshal(group, &attributes) if err != nil { return nil, err diff --git a/common/go.mod b/common/go.mod index 6f4bb97..28239d7 100644 --- a/common/go.mod +++ b/common/go.mod @@ -1,15 +1,17 @@ module github.com/aserto-dev/scim/common -go 1.23.7 +go 1.24.1 require ( - github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3 - github.com/aserto-dev/errors v0.0.15 - github.com/aserto-dev/go-aserto v0.33.7 - github.com/aserto-dev/go-directory v0.33.9 + github.com/aserto-dev/ds-load/sdk v0.0.0-20250408143332-e8965667fcc0 + github.com/aserto-dev/errors v0.0.17 + github.com/aserto-dev/go-aserto v0.33.8 + github.com/aserto-dev/go-directory v0.33.10 github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 + github.com/hashicorp/go-multierror v1.1.1 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 + github.com/samber/lo v1.49.1 github.com/scim2/filter-parser/v2 v2.2.0 github.com/stretchr/testify v1.10.0 google.golang.org/protobuf v1.36.6 @@ -20,7 +22,8 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/aserto-dev/header v0.0.10 // indirect + github.com/aserto-dev/header v0.0.11 // indirect + github.com/aserto-dev/logger v0.0.9 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/di-wu/parser v0.3.0 // indirect github.com/di-wu/xsd-datetime v1.0.0 // indirect @@ -29,7 +32,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -37,15 +39,14 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/samber/lo v1.49.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.1 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 // indirect google.golang.org/grpc v1.71.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/common/go.sum b/common/go.sum index 713b768..e99ac8d 100644 --- a/common/go.sum +++ b/common/go.sum @@ -8,14 +8,26 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3 h1:qyQXzQFa+0ocXnFeMhjOFfzZZ5opFt1m7e3Ln7Mq2E8= github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3/go.mod h1:QAQ+JlEH011NOsZ8uBGFPDoixZkfM1WQigK6PTaFSVA= +github.com/aserto-dev/ds-load/sdk v0.0.0-20250408143332-e8965667fcc0 h1:kIz/rVXDKcnl+h2rB+zZpDyD2G8DWYkdpULLdGXVpVg= +github.com/aserto-dev/ds-load/sdk v0.0.0-20250408143332-e8965667fcc0/go.mod h1:QAQ+JlEH011NOsZ8uBGFPDoixZkfM1WQigK6PTaFSVA= github.com/aserto-dev/errors v0.0.15 h1:Mx/k7HITit+Istq8YLatiydEIbff39RXf3fW/PlKSwo= github.com/aserto-dev/errors v0.0.15/go.mod h1:WntQkFRb4j41tp4ObRXTdhu/VZKIzIRTReLHjLLMWyc= +github.com/aserto-dev/errors v0.0.17 h1:Jlb38zvMAEOhbl8SSt8pz0p4/r3wgRMNcnBtnhzhD2g= +github.com/aserto-dev/errors v0.0.17/go.mod h1:42SHPNyCVOYYLgmz5KPvoc9GESAUuff0lN4S/hXLrz4= github.com/aserto-dev/go-aserto v0.33.7 h1:oLAj2nu4YKJ7q6pUFpABOQuY/70YfF5B89T0RkKSV8k= github.com/aserto-dev/go-aserto v0.33.7/go.mod h1:R4Bo3Tgn2KnyvmeyW+gID7pBqaRcnqnib36DiLjcjiw= +github.com/aserto-dev/go-aserto v0.33.8 h1:WFk0AFHoLZEH6W6az5ktg7aQ+4gS34/UmWNw2UOW70I= +github.com/aserto-dev/go-aserto v0.33.8/go.mod h1:fCDpKpXHVEf7pzaIA+oqIq1NKLywVz4GLdE3WUm+D/k= github.com/aserto-dev/go-directory v0.33.9 h1:JxsNVRfjpRr0gtpzCTn64D74gUkbj/qdXYFFM7VaP58= github.com/aserto-dev/go-directory v0.33.9/go.mod h1:jiKmqzVQ0eYnn4CrIp6bFneTOtOFcCthspo287BnWlg= +github.com/aserto-dev/go-directory v0.33.10 h1:PLevCAWc9QeLZZv5Wc+yGk4psSd3507y1+9Fps+CzdI= +github.com/aserto-dev/go-directory v0.33.10/go.mod h1:CYRXxtDtf4zSwYYBBkGqW3b7HWJWvoIEBWgah9SGMAM= github.com/aserto-dev/header v0.0.10 h1:H6sz3F4pfv53FuyGNoZlRNHpAcOonTioQMnWRowyigU= github.com/aserto-dev/header v0.0.10/go.mod h1:N3+nmX6nXmM9gI8VsGXOujPW6aW/8aEFa7dSu0FRerY= +github.com/aserto-dev/header v0.0.11 h1:Qx7lWzfq29h0OgaJQ8W9KD+/q9q14yOwKBFmD0viAfQ= +github.com/aserto-dev/header v0.0.11/go.mod h1:yTO0YPKVTlUTcP0ecQ/7qKs6l6RvDS0ac5l+S1BGWBs= +github.com/aserto-dev/logger v0.0.9 h1:QH11l8937Sw+GAe2yvgpoLg70fqQvPrEufkXAmDUk0g= +github.com/aserto-dev/logger v0.0.9/go.mod h1:mMXq/bhdIKoOVsIZ2zJOqgjcc/jR2x14FhU0St/8AVQ= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -113,8 +125,12 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= +google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 h1:AMLTAunltONNuzWgVPZXrjLWtXpsG6A3yLLPEoJ/IjU= +google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac= google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/common/handlers/groups/create.go b/common/handlers/groups/create.go index 5d36b94..0ec3c47 100644 --- a/common/handlers/groups/create.go +++ b/common/handlers/groups/create.go @@ -19,7 +19,7 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour logger.Info().Msg("create group") logger.Trace().Any("attributes", attributes).Msg("creating group") - var group *model.Group + group := &model.Group{} err := convert.Unmarshal(attributes, group) if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to group") diff --git a/common/handlers/groups/patch.go b/common/handlers/groups/patch.go index 3797678..2c86cec 100644 --- a/common/handlers/groups/patch.go +++ b/common/handlers/groups/patch.go @@ -3,15 +3,14 @@ package groups import ( "context" - cerr "github.com/aserto-dev/errors" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" "github.com/aserto-dev/scim/common" "github.com/aserto-dev/scim/common/convert" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" structpb "google.golang.org/protobuf/types/known/structpb" ) @@ -31,7 +30,8 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ }) if err != nil { logger.Error().Err(err).Msg("failed to get group") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } return scim.Resource{}, err diff --git a/common/handlers/users/create.go b/common/handlers/users/create.go index 249f668..37b88dc 100644 --- a/common/handlers/users/create.go +++ b/common/handlers/users/create.go @@ -19,7 +19,7 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour logger.Info().Msg("create user") logger.Trace().Any("attributes", attributes).Msg("creating user") - var user *model.User + user := &model.User{} err := convert.Unmarshal(attributes, user) if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to user") diff --git a/common/handlers/users/delete.go b/common/handlers/users/delete.go index c8888d4..4a8a9f6 100644 --- a/common/handlers/users/delete.go +++ b/common/handlers/users/delete.go @@ -3,12 +3,11 @@ package users import ( "context" - cerr "github.com/aserto-dev/errors" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { @@ -29,7 +28,8 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { }) if err != nil { logger.Error().Err(err).Msg("failed to get relations") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(id) } return err @@ -69,7 +69,8 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { }) if err != nil { logger.Error().Err(err).Msg("failed to delete user") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(id) } } @@ -82,7 +83,8 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { }) if err != nil { logger.Error().Err(err).Msg("failed to delete user source object") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(id) } } diff --git a/common/handlers/users/get.go b/common/handlers/users/get.go index 0199317..865afb1 100644 --- a/common/handlers/users/get.go +++ b/common/handlers/users/get.go @@ -3,14 +3,13 @@ package users import ( "context" - cerr "github.com/aserto-dev/errors" dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" - "github.com/aserto-dev/go-directory/pkg/derr" "github.com/aserto-dev/scim/common/convert" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func (u UsersResourceHandler) Get(ctx context.Context, id string) (scim.Resource, error) { @@ -26,7 +25,8 @@ func (u UsersResourceHandler) Get(ctx context.Context, id string) (scim.Resource }) if err != nil { logger.Error().Err(err).Msg("failed to get user") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } return scim.Resource{}, err diff --git a/common/handlers/users/patch.go b/common/handlers/users/patch.go index 1aefaaf..08e2bd2 100644 --- a/common/handlers/users/patch.go +++ b/common/handlers/users/patch.go @@ -3,15 +3,14 @@ package users import ( "context" - cerr "github.com/aserto-dev/errors" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" - "github.com/aserto-dev/go-directory/pkg/derr" "github.com/aserto-dev/scim/common" "github.com/aserto-dev/scim/common/convert" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" ) @@ -29,7 +28,8 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ }) if err != nil { logger.Error().Err(err).Msg("failed to get user") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } return scim.Resource{}, err diff --git a/common/patch.go b/common/patch.go index 4c36222..83631b6 100644 --- a/common/patch.go +++ b/common/patch.go @@ -180,6 +180,7 @@ func ReplaceInInterfaceArray(value []interface{}, op scim.PatchOperation) ([]int if originalValue, ok := value[index].(map[string]interface{}); ok { originalValue[*op.Path.SubAttribute] = op.Value value[index] = originalValue + return value, nil } else { return nil, serrors.ScimErrorInvalidPath @@ -222,11 +223,13 @@ func AddProperty(objectProps scim.ResourceAttributes, op scim.PatchOperation) (s if objectProps[op.Path.AttributePath.AttributeName] == nil { objectProps[op.Path.AttributePath.AttributeName] = make([]interface{}, 0) } + properties := val attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) if !ok { return nil, serrors.ScimErrorInvalidPath } + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, properties) } } diff --git a/go.mod b/go.mod index 4f873ae..db94176 100644 --- a/go.mod +++ b/go.mod @@ -1,70 +1,139 @@ module github.com/aserto-dev/scim -go 1.23.7 +go 1.24.1 replace github.com/aserto-dev/scim/common => ./common require ( - github.com/aserto-dev/go-aserto v0.33.7 - github.com/aserto-dev/logger v0.0.7 + github.com/aserto-dev/go-aserto v0.33.8 + github.com/aserto-dev/go-directory v0.33.10 + github.com/aserto-dev/logger v0.0.9 github.com/aserto-dev/scim/common v0.0.0-00010101000000-000000000000 + github.com/aserto-dev/topaz v0.32.58 + github.com/docker/go-connections v0.5.0 github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 + github.com/gavv/httpexpect/v2 v2.17.0 github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 - github.com/spf13/cobra v1.8.1 + github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.36.0 ) require ( dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3 // indirect - github.com/aserto-dev/errors v0.0.15 // indirect - github.com/aserto-dev/go-directory v0.33.9 // indirect - github.com/aserto-dev/header v0.0.10 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/aserto-dev/ds-load/sdk v0.0.0-20250408143332-e8965667fcc0 // indirect + github.com/aserto-dev/errors v0.0.17 // indirect + github.com/aserto-dev/header v0.0.11 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/di-wu/parser v0.3.0 // indirect github.com/di-wu/xsd-datetime v1.0.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.0.1+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dongri/phonenumber v0.1.12 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hpcloud/tail v1.0.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/imkira/go-interpol v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/magiconair/properties v1.8.9 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/samber/lo v1.49.1 // indirect + github.com/sanity-io/litter v1.5.5 // indirect github.com/scim2/filter-parser/v2 v2.2.0 // indirect + github.com/sergi/go-diff v1.0.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.40.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect + github.com/yudai/gojsondiff v1.0.0 // indirect + github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 // indirect google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + moul.io/http2curl/v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index d9eab45..260ac79 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,50 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3 h1:qyQXzQFa+0ocXnFeMhjOFfzZZ5opFt1m7e3Ln7Mq2E8= -github.com/aserto-dev/ds-load/sdk v0.0.0-20250313113259-eb1a3f86edc3/go.mod h1:QAQ+JlEH011NOsZ8uBGFPDoixZkfM1WQigK6PTaFSVA= -github.com/aserto-dev/errors v0.0.15 h1:Mx/k7HITit+Istq8YLatiydEIbff39RXf3fW/PlKSwo= -github.com/aserto-dev/errors v0.0.15/go.mod h1:WntQkFRb4j41tp4ObRXTdhu/VZKIzIRTReLHjLLMWyc= -github.com/aserto-dev/go-aserto v0.33.7 h1:oLAj2nu4YKJ7q6pUFpABOQuY/70YfF5B89T0RkKSV8k= -github.com/aserto-dev/go-aserto v0.33.7/go.mod h1:R4Bo3Tgn2KnyvmeyW+gID7pBqaRcnqnib36DiLjcjiw= -github.com/aserto-dev/go-directory v0.33.9 h1:JxsNVRfjpRr0gtpzCTn64D74gUkbj/qdXYFFM7VaP58= -github.com/aserto-dev/go-directory v0.33.9/go.mod h1:jiKmqzVQ0eYnn4CrIp6bFneTOtOFcCthspo287BnWlg= -github.com/aserto-dev/header v0.0.10 h1:H6sz3F4pfv53FuyGNoZlRNHpAcOonTioQMnWRowyigU= -github.com/aserto-dev/header v0.0.10/go.mod h1:N3+nmX6nXmM9gI8VsGXOujPW6aW/8aEFa7dSu0FRerY= -github.com/aserto-dev/logger v0.0.7 h1:ORvXxZDMNIcN/E3SYHj8fxmNZnOD7Gf87pOLB2XQavw= -github.com/aserto-dev/logger v0.0.7/go.mod h1:66ff7ALo68NT1HcCg5zytOnGh6I5R0HeDpN85cwHcD0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/aserto-dev/ds-load/sdk v0.0.0-20250408143332-e8965667fcc0 h1:kIz/rVXDKcnl+h2rB+zZpDyD2G8DWYkdpULLdGXVpVg= +github.com/aserto-dev/ds-load/sdk v0.0.0-20250408143332-e8965667fcc0/go.mod h1:QAQ+JlEH011NOsZ8uBGFPDoixZkfM1WQigK6PTaFSVA= +github.com/aserto-dev/errors v0.0.17 h1:Jlb38zvMAEOhbl8SSt8pz0p4/r3wgRMNcnBtnhzhD2g= +github.com/aserto-dev/errors v0.0.17/go.mod h1:42SHPNyCVOYYLgmz5KPvoc9GESAUuff0lN4S/hXLrz4= +github.com/aserto-dev/go-aserto v0.33.8 h1:WFk0AFHoLZEH6W6az5ktg7aQ+4gS34/UmWNw2UOW70I= +github.com/aserto-dev/go-aserto v0.33.8/go.mod h1:fCDpKpXHVEf7pzaIA+oqIq1NKLywVz4GLdE3WUm+D/k= +github.com/aserto-dev/go-directory v0.33.10 h1:PLevCAWc9QeLZZv5Wc+yGk4psSd3507y1+9Fps+CzdI= +github.com/aserto-dev/go-directory v0.33.10/go.mod h1:CYRXxtDtf4zSwYYBBkGqW3b7HWJWvoIEBWgah9SGMAM= +github.com/aserto-dev/header v0.0.11 h1:Qx7lWzfq29h0OgaJQ8W9KD+/q9q14yOwKBFmD0viAfQ= +github.com/aserto-dev/header v0.0.11/go.mod h1:yTO0YPKVTlUTcP0ecQ/7qKs6l6RvDS0ac5l+S1BGWBs= +github.com/aserto-dev/logger v0.0.9 h1:QH11l8937Sw+GAe2yvgpoLg70fqQvPrEufkXAmDUk0g= +github.com/aserto-dev/logger v0.0.9/go.mod h1:mMXq/bhdIKoOVsIZ2zJOqgjcc/jR2x14FhU0St/8AVQ= +github.com/aserto-dev/topaz v0.32.58 h1:ffw6MPHhfMn268z+NqmFmBR6fqrr4uHKOCiqhxWdGi4= +github.com/aserto-dev/topaz v0.32.58/go.mod h1:Llk5TuO8XkydJLiiHRVVp+oJjptr4e1+aSe/8ysOmlY= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -29,27 +54,59 @@ github.com/di-wu/parser v0.3.0 h1:NMOvy5ifswgt4gsdhySVcKOQtvjC43cHZIfViWctqQY= github.com/di-wu/parser v0.3.0/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo= github.com/di-wu/xsd-datetime v1.0.0 h1:vZoGNkbzpBNoc+JyfVLEbutNDNydYV8XwHeV7eUJoxI= github.com/di-wu/xsd-datetime v1.0.0/go.mod h1:i3iEhrP3WchwseOBeIdW/zxeoleXTOzx1WyDXgdmOww= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= +github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dongri/phonenumber v0.1.12 h1:rR/4VZzxqpocUdyM4dIdfY0TWd8FcW43oiyPaOUxNIk= github.com/dongri/phonenumber v0.1.12/go.mod h1:cuHFSstIxh6qh/Qs/SCV3Grb/JMYregBLuXELvSYmT4= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 h1:0+BTyxIYgiVAry/P5s8R4dYuLkhB9Nhso8ogFWNr4IQ= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gavv/httpexpect/v2 v2.17.0 h1:nIJqt5v5e4P7/0jODpX2gtSw+pHXUqdP28YcjqwDZmE= +github.com/gavv/httpexpect/v2 v2.17.0/go.mod h1:E8ENFlT9MZ3Si2sfM6c6ONdwXV2noBCGkhA+lkJgkP0= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -59,16 +116,29 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -78,93 +148,212 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM= github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= +github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= +github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00= +github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc= +github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= -google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 h1:AMLTAunltONNuzWgVPZXrjLWtXpsG6A3yLLPEoJ/IjU= +google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= @@ -172,8 +361,19 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= +moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= diff --git a/go.work b/go.work index 5b78e7d..6f7df19 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.23.7 +go 1.24.1 use ( ./ diff --git a/makefile b/makefile index eeb13c0..712487d 100644 --- a/makefile +++ b/makefile @@ -10,9 +10,9 @@ GOOS := $(shell go env GOOS) GOARCH := $(shell go env GOARCH) GOPRIVATE := "github.com/aserto-dev" -GOTESTSUM_VERSION := 1.11.0 -GOLANGCI-LINT_VERSION := 1.64.5 -GORELEASER_VERSION := 2.3.2 +GOTESTSUM_VERSION := 1.12.1 +GOLANGCI-LINT_VERSION := 2.0.2 +GORELEASER_VERSION := 2.8.2 RELEASE_TAG := $$(svu) diff --git a/pkg/app/run.go b/pkg/app/run.go index 7c50488..05cfcbb 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -1,8 +1,10 @@ package app import ( + "context" "crypto/sha256" "crypto/subtle" + "fmt" "net/http" "strings" "time" @@ -20,28 +22,44 @@ import ( "github.com/rs/zerolog" ) -func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) error { +type SCIMServer struct { + server *http.Server + log *zerolog.Logger + cfg *config.Config + dsClient *ds.Client +} + +func NewSCIMServer(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) (*SCIMServer, error) { cfg, err := config.NewConfig(cfgPath) if err != nil { - return err + return nil, err } scimLogger, err := logger.NewLogger(logWriter, errWriter, &cfg.Logging) if err != nil { - return err + return nil, err } - dsClient, err := directory.GetDirectoryClient(&cfg.Directory) + return &SCIMServer{ + log: scimLogger, + cfg: cfg, + }, nil +} + +func (s *SCIMServer) Run() error { + dsClient, err := directory.GetDirectoryClient(&s.cfg.Directory) if err != nil { return err } - transformCfg, err := convert.NewTransformConfig(&cfg.SCIM) + s.dsClient = dsClient + + transformCfg, err := convert.NewTransformConfig(&s.cfg.SCIM) if err != nil { return err } - userHandler, err := userHandler(scimLogger, transformCfg, dsClient) + userHandler, err := userHandler(s.log, transformCfg, dsClient) if err != nil { return err } @@ -58,7 +76,7 @@ func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) er Handler: userHandler, } - groupHandler, err := groupHandler(scimLogger, transformCfg, dsClient) + groupHandler, err := groupHandler(s.log, transformCfg, dsClient) if err != nil { return err } @@ -97,15 +115,15 @@ func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) er } app := new(application) - app.cfg = &cfg.Server.Auth + app.cfg = &s.cfg.Server.Auth - tlsServerConfig, err := cfg.Server.Certs.ServerConfig() + tlsServerConfig, err := s.cfg.Server.Certs.ServerConfig() if err != nil { return err } srv := &http.Server{ - Addr: cfg.Server.ListenAddress, + Addr: s.cfg.Server.ListenAddress, Handler: app.auth(server.ServeHTTP), TLSConfig: tlsServerConfig, IdleTimeout: time.Minute, @@ -113,13 +131,36 @@ func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) er WriteTimeout: 30 * time.Second, } - if cfg.Server.Certs.HasCert() { + s.server = srv + s.log.Info().Str("address", s.cfg.Server.ListenAddress).Msg("Starting SCIM server") + if s.cfg.Server.Certs.HasCert() { return srv.ListenAndServeTLS("", "") } + fmt.Println("Starting SCIM server without TLS") + return srv.ListenAndServe() } +func (s *SCIMServer) Shutdown(ctx context.Context) error { + if s.server != nil { + s.log.Info().Msg("Shutting down SCIM server") + return s.server.Shutdown(ctx) + } + s.server = nil + + if s.dsClient != nil { + s.log.Info().Msg("Closing directory client connection") + if err := s.dsClient.Close(); err != nil { + s.log.Error().Err(err).Msg("Failed to close directory client") + } + } + s.dsClient = nil + s.log.Info().Msg("SCIM server shutdown complete") + + return nil +} + func userHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsClient *ds.Client) (scim.ResourceHandler, error) { usersLogger := scimLogger.With().Str("component", "users").Logger() usersResourceHandler, err := users.NewUsersResourceHandler(&usersLogger, cfg, dsClient) diff --git a/pkg/config/config.go b/pkg/config/config.go index e561858..93e68dc 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -93,6 +93,7 @@ func NewConfig(configPath string) (*Config, error) { // nolint // function will return nil, errors.Wrapf(err, "failed to read config file '%s'", file) } } + v.AutomaticEnv() cfg := new(Config) diff --git a/pkg/test/assets/assets.go b/pkg/test/assets/assets.go new file mode 100644 index 0000000..44e025d --- /dev/null +++ b/pkg/test/assets/assets.go @@ -0,0 +1,41 @@ +package assets_test + +import ( + "bytes" + _ "embed" +) + +//go:embed config/topaz.yaml +var topazConfig []byte + +//go:embed data/rick.json +var rickJson []byte + +//go:embed data/morty.json +var mortyJson []byte + +//go:embed data/patch.json +var patch []byte + +//go:embed data/manifest.yaml +var manifest []byte + +func TopazConfigReader() *bytes.Reader { + return bytes.NewReader(topazConfig) +} + +func Rick() []byte { + return rickJson +} + +func Morty() []byte { + return mortyJson +} + +func Patch() []byte { + return patch +} + +func Manifest() []byte { + return manifest +} diff --git a/pkg/test/assets/config/scim.yaml b/pkg/test/assets/config/scim.yaml new file mode 100644 index 0000000..60bbf92 --- /dev/null +++ b/pkg/test/assets/config/scim.yaml @@ -0,0 +1,36 @@ +--- +logging: + log_level: trace +server: + listen_address: ":8081" + auth: + basic: + enabled: true + username: scim + password: scim +directory: + address: "" + no_tls: true +scim: + user: + object_type: user + identity_object_type: identity + identity_relation: user#identifier + property_mapping: + enabled: active + source_object_type: scim_user + manager_relation: manager + group: + object_type: group + group_member_relation: member + source_object_type: scim_group + role: + object_type: group + role_relation: member + relations: + - object_id: system + object_type: system + relation: admin + subject_id: aserto-admin + subject_type: group + subject_relation: member diff --git a/pkg/test/assets/config/topaz.yaml b/pkg/test/assets/config/topaz.yaml new file mode 100644 index 0000000..8818dd3 --- /dev/null +++ b/pkg/test/assets/config/topaz.yaml @@ -0,0 +1,272 @@ +# yaml-language-server: $schema=https://topaz.sh/schema/config.json +--- +# config schema version +version: 2 + +# logger settings. +logging: + prod: true + log_level: info + grpc_log_level: info + +# edge directory configuration. +directory: + db_path: '${TOPAZ_DB_DIR}/test-no-tls.db' + request_timeout: 5s # set as default, 5 secs. + +# remote directory is used to resolve the identity for the authorizer. +remote_directory: + address: "0.0.0.0:9292" # set as default, it should be the same as the reader as we resolve the identity from the local directory service. + insecure: false + no_tls: true + tenant_id: "" + api_key: "" + token: "" + client_cert_path: "" + client_key_path: "" + ca_cert_path: "" + timeout_in_seconds: 5 + headers: + +# default jwt validation configuration +jwt: + acceptable_time_skew_seconds: 5 # set as default, 5 secs + +# authentication configuration +auth: + keys: + # - "" + # - "" + options: + default: + enable_api_key: false + enable_anonymous: true + overrides: + paths: + - /aserto.authorizer.v2.Authorizer/Info + - /grpc.reflection.v1.ServerReflection/ServerReflectionInfo + - /grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo + override: + enable_api_key: false + enable_anonymous: true + +api: + health: + listen_address: "0.0.0.0:9494" + + metrics: + listen_address: "0.0.0.0:9696" + + services: + console: + grpc: + listen_address: "0.0.0.0:9292" + fqdn: "" + gateway: + listen_address: "0.0.0.0:9393" + fqdn: "" + allowed_headers: + - "Authorization" + - "Content-Type" + - "If-Match" + - "If-None-Match" + - "Depth" + allowed_methods: + - "GET" + - "POST" + - "HEAD" + - "DELETE" + - "PUT" + - "PATCH" + - "PROFIND" + - "MKCOL" + - "COPY" + - "MOVE" + allowed_origins: + - http://localhost + - http://localhost:* + - https://*.aserto.com + - https://*aserto-console.netlify.app + read_timeout: 2s + read_header_timeout: 2s + write_timeout: 2s + idle_timeout: 30s + + model: + grpc: + listen_address: "0.0.0.0:9292" + fqdn: "" + gateway: + listen_address: "0.0.0.0:9393" + fqdn: "" + allowed_headers: + - "Authorization" + - "Content-Type" + - "If-Match" + - "If-None-Match" + - "Depth" + allowed_methods: + - "GET" + - "POST" + - "HEAD" + - "DELETE" + - "PUT" + - "PATCH" + - "PROFIND" + - "MKCOL" + - "COPY" + - "MOVE" + allowed_origins: + - http://localhost + - http://localhost:* + - https://*.aserto.com + - https://*aserto-console.netlify.app + read_timeout: 2s + read_header_timeout: 2s + write_timeout: 2s + idle_timeout: 30s + + reader: + needs: + - model + grpc: + listen_address: "0.0.0.0:9292" + fqdn: "" + certs: + gateway: + listen_address: "0.0.0.0:9393" + fqdn: "" + allowed_headers: + - "Authorization" + - "Content-Type" + - "If-Match" + - "If-None-Match" + - "Depth" + allowed_methods: + - "GET" + - "POST" + - "HEAD" + - "DELETE" + - "PUT" + - "PATCH" + - "PROFIND" + - "MKCOL" + - "COPY" + - "MOVE" + allowed_origins: + - http://localhost + - http://localhost:* + - https://*.aserto.com + - https://*aserto-console.netlify.app + read_timeout: 2s # default 2 seconds + read_header_timeout: 2s + write_timeout: 2s + idle_timeout: 30s # default 30 seconds + + writer: + needs: + - model + grpc: + listen_address: "0.0.0.0:9292" + fqdn: "" + gateway: + listen_address: "0.0.0.0:9393" + fqdn: "" + allowed_headers: + - "Authorization" + - "Content-Type" + - "If-Match" + - "If-None-Match" + - "Depth" + allowed_methods: + - "GET" + - "POST" + - "HEAD" + - "DELETE" + - "PUT" + - "PATCH" + - "PROFIND" + - "MKCOL" + - "COPY" + - "MOVE" + allowed_origins: + - http://localhost + - http://localhost:* + - https://*.aserto.com + - https://*aserto-console.netlify.app + read_timeout: 2s + read_header_timeout: 2s + write_timeout: 2s + idle_timeout: 30s + + exporter: + grpc: + listen_address: "0.0.0.0:9292" + fqdn: "" + + importer: + needs: + - model + grpc: + listen_address: "0.0.0.0:9292" + fqdn: "" + + authorizer: + needs: + - reader + grpc: + connection_timeout_seconds: 2 + listen_address: "0.0.0.0:9292" + fqdn: "" + gateway: + listen_address: "0.0.0.0:9393" + fqdn: "" + allowed_headers: + - "Authorization" + - "Content-Type" + - "If-Match" + - "If-None-Match" + - "Depth" + allowed_methods: + - "GET" + - "POST" + - "HEAD" + - "DELETE" + - "PUT" + - "PATCH" + - "PROFIND" + - "MKCOL" + - "COPY" + - "MOVE" + allowed_origins: + - http://localhost + - http://localhost:* + - https://*.aserto.com + - https://*aserto-console.netlify.app + read_timeout: 2s + read_header_timeout: 2s + write_timeout: 2s + idle_timeout: 30s + +opa: + instance_id: "-" + graceful_shutdown_period_seconds: 2 + # max_plugin_wait_time_seconds: 30 set as default + local_bundles: + paths: [] + skip_verification: true + config: + services: + ghcr: + url: https://ghcr.io + type: "oci" + response_header_timeout_seconds: 5 + bundles: + test: + service: ghcr + resource: "ghcr.io/aserto-policies/policy-rebac:latest" + persist: false + config: + polling: + min_delay_seconds: 60 + max_delay_seconds: 120 diff --git a/pkg/test/assets/data/group.json b/pkg/test/assets/data/group.json new file mode 100644 index 0000000..d09d9d4 --- /dev/null +++ b/pkg/test/assets/data/group.json @@ -0,0 +1,6 @@ +{ + "displayName": "Evil Genius", + "members": [{ + "value": "rick@the-citadel.com" + }] +} \ No newline at end of file diff --git a/pkg/test/assets/data/manifest.yaml b/pkg/test/assets/data/manifest.yaml new file mode 100644 index 0000000..3453aea --- /dev/null +++ b/pkg/test/assets/data/manifest.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://www.topaz.sh/schema/manifest.json +--- + +model: + version: 3 + +types: + identity: {} + scim_user: {} + scim_group: {} + user: + relations: + identifier: identity + manager: user + group: + relations: + member: user diff --git a/pkg/test/assets/data/morty.json b/pkg/test/assets/data/morty.json new file mode 100644 index 0000000..2a52677 --- /dev/null +++ b/pkg/test/assets/data/morty.json @@ -0,0 +1,23 @@ +{ + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName": "morty@the-citadel.com", + "name": { + "givenName": "Morty", + "familyName": "Smith" + }, + "emails": [{ + "primary": true, + "value": "morty@the-citadel.com", + "type": "work" + }], + "displayName": "Morty Smith", + "externalId": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs", + "locale": "en-US", + "groups": [], + "active": true, + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { + "manager": { + "value": "rick@the-citadel.com" + } + } +} \ No newline at end of file diff --git a/pkg/test/assets/data/patch.json b/pkg/test/assets/data/patch.json new file mode 100644 index 0000000..6f79c97 --- /dev/null +++ b/pkg/test/assets/data/patch.json @@ -0,0 +1,13 @@ +{ + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:PatchOp" + ], + "Operations": [ + { + "op": "replace", + "value": { + "active": false + } + } + ] +} \ No newline at end of file diff --git a/pkg/test/assets/data/rick.json b/pkg/test/assets/data/rick.json new file mode 100644 index 0000000..833b3d0 --- /dev/null +++ b/pkg/test/assets/data/rick.json @@ -0,0 +1,18 @@ +{ + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName": "rick@the-citadel.com", + "name": { + "givenName": "Rick", + "familyName": "Sanchez" + }, + "emails": [{ + "primary": true, + "value": "rick@the-citadel.com", + "type": "work" + }], + "displayName": "Rick Sanchez", + "externalId": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs", + "locale": "en-US", + "groups": [], + "active": true +} \ No newline at end of file diff --git a/pkg/test/common/common.go b/pkg/test/common/common.go new file mode 100644 index 0000000..b141701 --- /dev/null +++ b/pkg/test/common/common.go @@ -0,0 +1,194 @@ +package common_test + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/aserto-dev/go-aserto" + "github.com/aserto-dev/go-aserto/ds/v3" + dsm "github.com/aserto-dev/go-directory/aserto/directory/model/v3" + dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" + "github.com/aserto-dev/logger" + "github.com/aserto-dev/scim/pkg/app" + assets_test "github.com/aserto-dev/scim/pkg/test/assets" + "github.com/aserto-dev/topaz/pkg/cli/x" + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + testcontainers "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +type TestCase struct { + Topaz testcontainers.Container + DirectoryClient *ds.Client +} + +func (tst *TestCase) ContainerLogs(ctx context.Context, t *testing.T) string { + require := require.New(t) + + logs, err := tst.Topaz.Logs(ctx) + require.NoError(err) + + t.Cleanup(func() { _ = logs.Close() }) + + logData, err := io.ReadAll(logs) + require.NoError(err) + + return string(logData) +} + +func TestSetup(t *testing.T) TestCase { + ctx, cancel := context.WithCancel(context.Background()) + + t.Logf("\nTEST CONTAINER IMAGE: %q\n", TopazImage()) + + req := testcontainers.ContainerRequest{ + Image: TopazImage(), + ExposedPorts: []string{"9292/tcp"}, + Env: map[string]string{ + x.EnvTopazCertsDir: x.DefCertsDir, + x.EnvTopazDBDir: x.DefDBDir, + x.EnvTopazDecisions: x.DefDecisionsDir, + }, + Files: []testcontainers.ContainerFile{ + { + Reader: assets_test.TopazConfigReader(), + ContainerFilePath: "/config/config.yaml", + FileMode: 0x700, + }, + }, + WaitingFor: wait.ForAll( + wait.ForExposedPort(), + wait.ForLog("Starting 0.0.0.0:9292 gRPC server"), + ).WithStartupTimeoutDefault(300 * time.Second), + } + + topaz, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: false, + }) + require.NoError(t, err) + + if err := topaz.Start(ctx); err != nil { + require.NoError(t, err) + } + + addr, err := MappedAddr(ctx, topaz, "9292") + require.NoError(t, err) + + os.Setenv("ASERTO_SCIM_DIRECTORY_ADDRESS", addr) + scimConfig, err := filepath.Abs("assets/config/scim.yaml") + require.NoError(t, err) + + srv, err := app.NewSCIMServer(scimConfig, logger.TestLogger(os.Stdout), os.Stderr) + require.NoError(t, err) + + go func() { + err := srv.Run() + require.Error(t, err) + }() + + time.Sleep(time.Second) + + dirCfg := aserto.Config{ + Address: addr, + NoTLS: true, + } + + conn, err := dirCfg.Connect() + require.NoError(t, err) + + dsClient := ds.FromConnection(conn) + stream, err := dsClient.Model.SetManifest(ctx) + assert.NoError(t, err) + err = stream.Send(&dsm.SetManifestRequest{ + Msg: &dsm.SetManifestRequest_Body{ + Body: &dsm.Body{ + Data: assets_test.Manifest(), + }, + }, + }) + assert.NoError(t, err) + _, err = stream.CloseAndRecv() + assert.NoError(t, err) + + t.Cleanup(func() { + conn.Close() + srv.Shutdown(ctx) + testcontainers.CleanupContainer(t, topaz) + cancel() + }) + + return TestCase{ + Topaz: topaz, + DirectoryClient: dsClient, + } +} + +func (tst *TestCase) UserHasIdentity(ctx context.Context, user, identity string) bool { + userResp, err := tst.DirectoryClient.Reader.GetRelation(ctx, &dsr.GetRelationRequest{ + Relation: "identifier", + ObjectType: "user", + ObjectId: user, + SubjectType: "identity", + SubjectId: identity, + }) + if err != nil { + return false + } + return userResp.Result != nil +} + +func (tst *TestCase) UserHasManager(ctx context.Context, user, manager string) bool { + userResp, err := tst.DirectoryClient.Reader.GetRelation(ctx, &dsr.GetRelationRequest{ + Relation: "manager", + ObjectType: "user", + ObjectId: user, + SubjectType: "user", + SubjectId: manager, + }) + if err != nil { + return false + } + return userResp.Result != nil +} + +func (tst *TestCase) ReadUserProperty(ctx context.Context, user, property string) any { + userResp, err := tst.DirectoryClient.Reader.GetObject(ctx, &dsr.GetObjectRequest{ + ObjectType: "user", + ObjectId: user, + }) + if err != nil || userResp.Result == nil { + return nil + } + + return userResp.Result.Properties.Fields[property].AsInterface() +} + +func TopazImage() string { + image := os.Getenv("TOPAZ_TEST_IMAGE") + if image != "" { + return image + } + return "ghcr.io/aserto-dev/topaz:latest" +} + +func MappedAddr(ctx context.Context, container testcontainers.Container, port string) (string, error) { + host, err := container.Host(ctx) + if err != nil { + return "", err + } + + mappedPort, err := container.MappedPort(ctx, nat.Port(port)) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s:%s", host, mappedPort.Port()), nil +} diff --git a/pkg/test/scim_test.go b/pkg/test/scim_test.go new file mode 100644 index 0000000..1dc8397 --- /dev/null +++ b/pkg/test/scim_test.go @@ -0,0 +1,53 @@ +package scim_test + +import ( + "encoding/json" + "fmt" + "testing" + + assets_test "github.com/aserto-dev/scim/pkg/test/assets" + common_test "github.com/aserto-dev/scim/pkg/test/common" + + "github.com/gavv/httpexpect/v2" + "github.com/stretchr/testify/require" +) + +func TestScim(t *testing.T) { + // Setup test containers + tst := common_test.TestSetup(t) + fmt.Println("Directory address:", tst.DirectoryClient) + e := httpexpect.Default(t, "http://localhost:8081") + + // Create user for Rick + rick := map[string]any{} + err := json.Unmarshal(assets_test.Rick(), &rick) + require.NoError(t, err) + e.GET("/Users").WithBasicAuth("scim", "scim").Expect().Status(200) + e.POST("/Users").WithBasicAuth("scim", "scim").WithJSON(rick).Expect().Status(201).Body().Contains("Rick Sanchez") + e.GET("/Users").WithBasicAuth("scim", "scim").Expect().Status(200).Body().Contains("Rick Sanchez") + e.GET("/Users/rick@the-citadel.com").WithBasicAuth("scim", "scim").Expect().Status(200).Body().Contains("Rick Sanchez") + + // Create user for Morty + morty := map[string]any{} + err = json.Unmarshal(assets_test.Morty(), &morty) + require.NoError(t, err) + e.POST("/Users").WithBasicAuth("scim", "scim").WithJSON(morty).Expect().Status(201).Body().Contains("Morty Smith") + e.GET("/Users/morty@the-citadel.com").WithBasicAuth("scim", "scim").Expect().Status(200).Body().Contains("Morty Smith") + + require.True(t, tst.UserHasIdentity(t.Context(), "morty@the-citadel.com", "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs")) + require.True(t, tst.UserHasManager(t.Context(), "morty@the-citadel.com", "rick@the-citadel.com")) + require.Equal(t, true, tst.ReadUserProperty(t.Context(), "morty@the-citadel.com", "enabled")) + + // Update Morty + patchMorty := map[string]any{} + err = json.Unmarshal(assets_test.Patch(), &patchMorty) + require.NoError(t, err) + e.PATCH("/Users/morty@the-citadel.com").WithBasicAuth("scim", "scim").WithJSON(patchMorty).Expect().Status(200).Body().Contains("Morty Smith") + require.Equal(t, false, tst.ReadUserProperty(t.Context(), "morty@the-citadel.com", "enabled")) + + // Delete Morty + e.DELETE("/Users/morty@the-citadel.com").WithBasicAuth("scim", "scim").Expect().Status(204) + e.GET("/Users/morty@the-citadel.com").WithBasicAuth("scim", "scim").Expect().Status(404) + + t.Logf("topaz log:\n%s", tst.ContainerLogs(t.Context(), t)) +} From 264d596fbb73fa830c3c1664e02133d32f652df9 Mon Sep 17 00:00:00 2001 From: florindragos Date: Mon, 14 Apr 2025 16:03:17 +0300 Subject: [PATCH 08/13] update actions --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f7678bb..1605892 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,7 +41,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Lint package ${{ matrix.package }} - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v7 with: version: ${{ env.GO_LANGCI_LINT_VERSION }} install-mode: binary @@ -121,7 +121,7 @@ jobs: run: exit 1 - name: Push image to GitHub Container Registry - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: ${{ env.GO_RELEASER_VERSION }} From 7132891bfdbd3bfd86149975ca795797af32b386 Mon Sep 17 00:00:00 2001 From: florindragos Date: Tue, 15 Apr 2025 18:06:03 +0300 Subject: [PATCH 09/13] more lint fixes --- cmd/aserto-scim/main.go | 11 +-- common/config/config.go | 8 ++ common/convert/config.go | 10 ++- common/convert/convert.go | 18 +++- common/convert/converter_test.go | 60 ++++++------- common/directory/client.go | 139 +++++++++++++++++++----------- common/handlers/groups/create.go | 7 +- common/handlers/groups/get.go | 16 ++-- common/handlers/groups/patch.go | 13 ++- common/handlers/groups/replace.go | 1 + common/handlers/users/create.go | 11 ++- common/handlers/users/delete.go | 34 +++++--- common/handlers/users/get.go | 21 +++-- common/handlers/users/patch.go | 13 ++- common/patch.go | 66 +++++++++----- go.mod | 3 +- go.sum | 2 - pkg/app/directory/client.go | 1 - pkg/app/run.go | 58 ++++++++----- pkg/config/config.go | 25 +++++- pkg/test/assets/config/topaz.yaml | 2 +- pkg/test/common/common.go | 52 ++++++----- pkg/test/scim_test.go | 3 +- 23 files changed, 360 insertions(+), 214 deletions(-) diff --git a/cmd/aserto-scim/main.go b/cmd/aserto-scim/main.go index 63e531b..10be0f1 100644 --- a/cmd/aserto-scim/main.go +++ b/cmd/aserto-scim/main.go @@ -6,7 +6,7 @@ import ( "log" "os" "os/signal" - "time" + "syscall" "github.com/aserto-dev/scim/pkg/app" "github.com/aserto-dev/scim/pkg/version" @@ -47,12 +47,10 @@ var cmdRun = &cobra.Command{ }() stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, os.Kill) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { + if err := srv.Shutdown(context.Background()); err != nil { return err } log.Println("SCIM server stopped") @@ -60,8 +58,7 @@ var cmdRun = &cobra.Command{ }, } -// nolint: gochecknoinits -func init() { +func init() { //nolint: gochecknoinits cmdRun.Flags().StringVarP(&flagConfigPath, "config", "c", "", "config path") rootCmd.AddCommand(cmdRun) } diff --git a/common/config/config.go b/common/config/config.go index 087ac1f..2d311a3 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -47,26 +47,34 @@ func (cfg *Config) Validate() error { if cfg.User.ObjectType == "" { return errors.Wrap(ErrInvalidConfig, "scim.user_object_type is required") } + if cfg.User.IdentityObjectType == "" { return errors.Wrap(ErrInvalidConfig, "scim.identity_object_type is required") } + if cfg.User.IdentityRelation == "" { return errors.Wrap(ErrInvalidConfig, "scim.identity_relation is required") } + object, relation, found := strings.Cut(cfg.User.IdentityRelation, "#") + if !found { return errors.Wrap(ErrInvalidConfig, "identity relation must be in the format object#relation") } + if object != cfg.User.IdentityObjectType && object != cfg.User.ObjectType { return errors.Wrapf(ErrInvalidConfig, "identity relation object type [%s] doesn't match user or identity type", object) } + if relation == "" { return errors.Wrap(ErrInvalidConfig, "identity relation is required") } + if cfg.Group != nil { if cfg.Group.ObjectType == "" { return errors.Wrap(ErrInvalidConfig, "scim.group_object_type is required") } + if cfg.Group.GroupMemberRelation == "" { return errors.Wrap(ErrInvalidConfig, "scim.group_member_relation is required") } diff --git a/common/convert/config.go b/common/convert/config.go index a68dfdf..0de8f5f 100644 --- a/common/convert/config.go +++ b/common/convert/config.go @@ -29,6 +29,7 @@ func (t TemplateName) String() string { case UsersGroupsRoles: return "users-groups-roles" } + return "unknown" } @@ -53,9 +54,11 @@ func NewTransformConfig(cfg *config.Config) (*TransformConfig, error) { if !found { return nil, errors.Wrap(ErrInvalidConfig, "identity relation must be in the format object#relation") } + if object != cfg.User.IdentityObjectType && object != cfg.User.ObjectType { return nil, errors.Wrapf(ErrInvalidConfig, "identity relation object type [%s] doesn't match user or identity type", object) } + if relation == "" { return nil, errors.Wrap(ErrInvalidConfig, "identity relation is required") } @@ -69,16 +72,17 @@ func NewTransformConfig(cfg *config.Config) (*TransformConfig, error) { } func (c *TransformConfig) HasGroups() bool { - return c.Config.Group != nil + return c.Group != nil } -func (c *TransformConfig) ToTemplateVars() (map[string]interface{}, error) { - var result map[string]interface{} +func (c *TransformConfig) ToTemplateVars() (map[string]any, error) { + var result map[string]any cfg, err := json.Marshal(c) if err != nil { return nil, errors.Wrap(err, "failed to marshal ScimConfig to json") } + if err := json.Unmarshal(cfg, &result); err != nil { return nil, errors.Wrap(err, "failed to unmarshal ScimConfig to map") } diff --git a/common/convert/convert.go b/common/convert/convert.go index 032874e..0d9a56c 100644 --- a/common/convert/convert.go +++ b/common/convert/convert.go @@ -34,7 +34,7 @@ func (c *Converter) ObjectToResource(object *dsc.Object, meta scim.Meta) scim.Re attr := c.ObjectToResourceAttributes(object) return scim.Resource{ - ID: object.Id, + ID: object.GetId(), ExternalID: eID, Attributes: attr, Meta: meta, @@ -42,7 +42,7 @@ func (c *Converter) ObjectToResource(object *dsc.Object, meta scim.Meta) scim.Re } func (c *Converter) ObjectToResourceAttributes(object *dsc.Object) scim.ResourceAttributes { - attr := object.Properties.AsMap() + attr := object.GetProperties().AsMap() delete(attr, "password") return attr @@ -60,13 +60,16 @@ func Unmarshal[S any, D any](source S, dest *D) error { func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { attributes := scim.ResourceAttributes{} err := Unmarshal(user, &attributes) + if err != nil { return scim.Resource{}, err } + eID := optional.String{} if user.ExternalID != "" { eID = optional.NewString(user.ExternalID) } + return scim.Resource{ ID: user.ID, ExternalID: eID, @@ -78,11 +81,14 @@ func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { attributes := scim.ResourceAttributes{} err := Unmarshal(user, &attributes) + if err != nil { return nil, err } + delete(attributes, "password") props, err := structpb.NewStruct(attributes) + if err != nil { return nil, err } @@ -105,6 +111,7 @@ func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { Id: userID, DisplayName: displayName, } + return object, nil } @@ -115,9 +122,11 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { attributes := scim.ResourceAttributes{} err := Unmarshal(group, &attributes) + if err != nil { return nil, err } + props, err := structpb.NewStruct(attributes) if err != nil { return nil, err @@ -144,6 +153,7 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { Id: objID, DisplayName: displayName, } + return object, nil } @@ -164,6 +174,7 @@ func (c *Converter) TransformResource(resource map[string]any, objType string) ( "objectType": objType, } transformer := transform.NewGoTemplateTransform(template) + return transformer.TransformObject(transformInput) } @@ -172,10 +183,13 @@ func ProtobufStructToMap(s *structpb.Struct) (map[string]any, error) { if err != nil { return nil, err } + m := make(map[string]any) err = json.Unmarshal(b, &m) + if err != nil { return nil, err } + return m, nil } diff --git a/common/convert/converter_test.go b/common/convert/converter_test.go index bc3eaf9..6d5aca0 100644 --- a/common/convert/converter_test.go +++ b/common/convert/converter_test.go @@ -8,14 +8,14 @@ import ( "github.com/stretchr/testify/require" ) -var ScimUser map[string]interface{} = map[string]interface{}{ +var ScimUser map[string]any = map[string]any{ "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, "userName": "foobar", - "name": map[string]interface{}{ + "name": map[string]any{ "givenName": "foo", "familyName": "bar", }, - "emails": []map[string]interface{}{ + "emails": []map[string]any{ { "primary": true, "value": "foo@bar.com", @@ -25,7 +25,7 @@ var ScimUser map[string]interface{} = map[string]interface{}{ "displayName": "foo bar", "externalId": "fooooo", "locale": "en-US", - "groups": []interface{}{}, + "groups": []any{}, "active": true, } @@ -44,26 +44,27 @@ func TestTransform(t *testing.T) { sCfg, err := convert.NewTransformConfig(&cfg) assert.NoError(err) + cvt := convert.NewConverter(sCfg) msg, err := cvt.TransformResource(ScimUser, "user") assert.NoError(err) assert.NotNil(msg) - assert.NotEmpty(msg.Objects) - assert.NotEmpty(msg.Relations) - assert.Len(msg.Relations, 3) + assert.NotEmpty(msg.GetObjects()) + assert.NotEmpty(msg.GetRelations()) + assert.Len(msg.GetRelations(), 3) - assert.Equal("foo@bar.com", msg.Relations[1].ObjectId) - assert.Equal("identity", msg.Relations[1].ObjectType) - assert.Equal("identitifier", msg.Relations[1].Relation) - assert.Equal("foobar", msg.Relations[1].SubjectId) - assert.Equal("user", msg.Relations[1].SubjectType) + assert.Equal("foo@bar.com", msg.GetRelations()[1].GetObjectId()) + assert.Equal("identity", msg.GetRelations()[1].GetObjectType()) + assert.Equal("identitifier", msg.GetRelations()[1].GetRelation()) + assert.Equal("foobar", msg.GetRelations()[1].GetSubjectId()) + assert.Equal("user", msg.GetRelations()[1].GetSubjectType()) - assert.Equal("foobar", msg.Relations[0].SubjectId) - assert.Equal("user", msg.Relations[0].SubjectType) + assert.Equal("foobar", msg.GetRelations()[0].GetSubjectId()) + assert.Equal("user", msg.GetRelations()[0].GetSubjectType()) - assert.Equal("fooooo", msg.Relations[2].ObjectId) - assert.Equal("identity", msg.Relations[2].ObjectType) + assert.Equal("fooooo", msg.GetRelations()[2].GetObjectId()) + assert.Equal("identity", msg.GetRelations()[2].GetObjectType()) } func TestTransformUserIdentifier(t *testing.T) { @@ -81,22 +82,23 @@ func TestTransformUserIdentifier(t *testing.T) { sCfg, err := convert.NewTransformConfig(&cfg) assert.NoError(err) + cvt := convert.NewConverter(sCfg) msg, err := cvt.TransformResource(ScimUser, "user") assert.NoError(err) assert.NotNil(msg) - assert.NotEmpty(msg.Objects) - assert.NotEmpty(msg.Relations) - assert.Equal("foo@bar.com", msg.Relations[1].SubjectId) - assert.Equal("identity", msg.Relations[1].SubjectType) - assert.Equal("identitifier", msg.Relations[1].Relation) - assert.Equal("foobar", msg.Relations[1].ObjectId) - assert.Equal("user", msg.Relations[1].ObjectType) - - assert.Equal("foobar", msg.Relations[0].ObjectId) - assert.Equal("user", msg.Relations[0].ObjectType) - - assert.Equal("fooooo", msg.Relations[2].SubjectId) - assert.Equal("identity", msg.Relations[2].SubjectType) + assert.NotEmpty(msg.GetObjects()) + assert.NotEmpty(msg.GetRelations()) + assert.Equal("foo@bar.com", msg.GetRelations()[1].GetSubjectId()) + assert.Equal("identity", msg.GetRelations()[1].GetSubjectType()) + assert.Equal("identitifier", msg.GetRelations()[1].GetRelation()) + assert.Equal("foobar", msg.GetRelations()[1].GetObjectId()) + assert.Equal("user", msg.GetRelations()[1].GetObjectType()) + + assert.Equal("foobar", msg.GetRelations()[0].GetObjectId()) + assert.Equal("user", msg.GetRelations()[0].GetObjectType()) + + assert.Equal("fooooo", msg.GetRelations()[2].GetSubjectId()) + assert.Equal("identity", msg.GetRelations()[2].GetSubjectType()) } diff --git a/common/directory/client.go b/common/directory/client.go index 013b41a..09711b9 100644 --- a/common/directory/client.go +++ b/common/directory/client.go @@ -17,6 +17,8 @@ import ( serrors "github.com/elimity-com/scim/errors" "github.com/hashicorp/go-multierror" "github.com/rs/zerolog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" ) @@ -41,53 +43,62 @@ func (s *Client) DS() *ds.Client { func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform, userAttributes scim.ResourceAttributes) (scim.Meta, error) { logger := s.logger.With().Str("method", "SetUser").Str("id", userID).Logger() logger.Trace().Msg("set user") + idRelation, err := s.cfg.GetIdentityRelation(userID, "") + if err != nil { return scim.Meta{}, err } relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ - ObjectType: idRelation.ObjectType, - ObjectId: idRelation.ObjectId, - Relation: idRelation.Relation, - SubjectType: idRelation.SubjectType, - SubjectId: idRelation.SubjectId, + ObjectType: idRelation.GetObjectType(), + ObjectId: idRelation.GetObjectId(), + Relation: idRelation.GetRelation(), + SubjectType: idRelation.GetSubjectType(), + SubjectId: idRelation.GetSubjectId(), WithObjects: false, WithEmptySubjectRelation: true, }) - if err != nil && !errors.Is(cerr.UnwrapAsertoError(err), derr.ErrRelationNotFound) { - return scim.Meta{}, err + if err != nil { + st, ok := status.FromError(err) + + if ok && st.Code() != codes.NotFound { + return scim.Meta{}, err + } } - result, addedIdentities, err := s.importObjects(ctx, data.Objects, userAttributes) + result, addedIdentities, err := s.importObjects(ctx, data.GetObjects(), userAttributes) if err != nil { return result, err } logger.Trace().Any("identities", addedIdentities).Msg("added identities") - for _, relation := range data.Relations { + for _, relation := range data.GetRelations() { logger.Trace().Any("relation", relation).Msg("setting relation") _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ Relation: relation, }) + if err != nil { return result, err } } mErr := &multierror.Error{} + for _, rel := range relations.GetResults() { - if !slices.Contains(addedIdentities, rel.ObjectId) { - logger.Trace().Str("identity", rel.ObjectId).Msg("deleting identity") + if !slices.Contains(addedIdentities, rel.GetObjectId()) { + logger.Trace().Str("identity", rel.GetObjectId()).Msg("deleting identity") _, err := s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectType: s.cfg.User.IdentityObjectType, - ObjectId: rel.ObjectId, + ObjectId: rel.GetObjectId(), WithRelations: true, }) + if err != nil { mErr = multierror.Append(mErr, err) - logger.Error().Err(err).Str("identity", rel.ObjectId).Msg("failed to delete identity") + logger.Error().Err(err).Str("identity", rel.GetObjectId()).Msg("failed to delete identity") } } } @@ -97,25 +108,30 @@ func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform func (s *Client) importObjects(ctx context.Context, objects []*dsc.Object, userAttributes scim.ResourceAttributes) (scim.Meta, []string, error) { var err error + result := scim.Meta{} addedIdentities := make([]string, 0) for _, object := range objects { - if object.Type == s.cfg.User.ObjectType { + if object.GetType() == s.cfg.User.ObjectType { var userProperties map[string]any - if object.Properties == nil { + if object.GetProperties() == nil { userProperties = make(map[string]any) } else { - userProperties = object.Properties.AsMap() + userProperties = object.GetProperties().AsMap() } + for key, value := range s.cfg.User.PropertyMapping { userProperties[key] = userAttributes[value] } + object.Properties, err = structpb.NewStruct(userProperties) + if err != nil { return result, addedIdentities, err } } + resp, err := s.client.Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: object, }) @@ -123,24 +139,25 @@ func (s *Client) importObjects(ctx context.Context, objects []*dsc.Object, userA if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrAlreadyExists) { return result, addedIdentities, serrors.ScimErrorUniqueness } + return result, addedIdentities, err } - if resp.Result.Type == s.cfg.User.IdentityObjectType { - addedIdentities = append(addedIdentities, resp.Result.Id) + if resp.GetResult().GetType() == s.cfg.User.IdentityObjectType { + addedIdentities = append(addedIdentities, resp.GetResult().GetId()) } - if object.Type == s.cfg.User.ObjectType { - err = s.setRelations(ctx, resp.Result.Id, resp.Result.Type) + if object.GetType() == s.cfg.User.ObjectType { + err = s.setRelations(ctx, resp.GetResult().GetId(), resp.GetResult().GetType()) if err != nil { return result, addedIdentities, err } - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() + createdAt := resp.GetResult().GetCreatedAt().AsTime() + updatedAt := resp.GetResult().GetUpdatedAt().AsTime() result.Created = &createdAt result.LastModified = &updatedAt - result.Version = resp.Result.Etag + result.Version = resp.GetResult().GetEtag() } } @@ -150,47 +167,55 @@ func (s *Client) importObjects(ctx context.Context, objects []*dsc.Object, userA func (s *Client) DeleteUser(ctx context.Context, userID string) error { logger := s.logger.With().Str("method", "DeleteUser").Str("id", userID).Logger() logger.Trace().Msg("delete user") + identityRelation, err := s.cfg.GetIdentityRelation(userID, "") + if err != nil { return err } relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ - SubjectType: identityRelation.SubjectType, - SubjectId: identityRelation.SubjectId, - ObjectType: identityRelation.ObjectType, - ObjectId: identityRelation.ObjectId, - Relation: identityRelation.Relation, + SubjectType: identityRelation.GetSubjectType(), + SubjectId: identityRelation.GetSubjectId(), + ObjectType: identityRelation.GetObjectType(), + ObjectId: identityRelation.GetObjectId(), + Relation: identityRelation.GetRelation(), }) if err != nil { - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrNotFound) { + st, ok := status.FromError(err) + + if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(userID) } } - for _, v := range relations.Results { + for _, v := range relations.GetResults() { var objectID string - switch v.ObjectType { + + switch v.GetObjectType() { case s.cfg.User.IdentityObjectType: - objectID = v.ObjectId + objectID = v.GetObjectId() case s.cfg.User.ObjectType: - objectID = v.SubjectId + objectID = v.GetSubjectId() default: return serrors.ScimErrorBadRequest("unexpected object type in identity relation") } - logger.Trace().Str("id", v.ObjectId).Msg("deleting identity") + logger.Trace().Str("id", v.GetObjectId()).Msg("deleting identity") + _, err = s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectId: objectID, ObjectType: s.cfg.User.IdentityObjectType, WithRelations: true, }) + if err != nil { return err } } logger.Trace().Msg("deleting user") + _, err = s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectType: s.cfg.User.ObjectType, ObjectId: userID, @@ -203,6 +228,7 @@ func (s *Client) DeleteUser(ctx context.Context, userID string) error { func (s *Client) SetGroup(ctx context.Context, groupID string, data *msg.Transform) (scim.Meta, error) { logger := s.logger.With().Str("method", "SetGroup").Str("id", groupID).Logger() logger.Trace().Msg("set group") + if s.cfg.Group == nil { logger.Warn().Msg("groups not enabled") return scim.Meta{}, nil @@ -215,63 +241,72 @@ func (s *Client) SetGroup(ctx context.Context, groupID string, data *msg.Transfo WithObjects: false, WithEmptySubjectRelation: true, }) - if err != nil && !errors.Is(cerr.UnwrapAsertoError(err), derr.ErrRelationNotFound) { - return scim.Meta{}, err + if err != nil { + st, ok := status.FromError(err) + + if ok && st.Code() != codes.NotFound { + return scim.Meta{}, err + } } addedMembers := make([]string, 0) - result := scim.Meta{} - for _, object := range data.Objects { + + for _, object := range data.GetObjects() { logger.Trace().Any("object", object).Msg("setting object") resp, err := s.client.Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: object, }) + if err != nil { if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrAlreadyExists) { return result, serrors.ScimErrorUniqueness } + return result, err } - if object.Type == s.cfg.Group.ObjectType { - err = s.setRelations(ctx, resp.Result.Id, resp.Result.Type) + if object.GetType() == s.cfg.Group.ObjectType { + err = s.setRelations(ctx, resp.GetResult().GetId(), resp.GetResult().GetType()) if err != nil { return result, err } - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() + createdAt := resp.GetResult().GetCreatedAt().AsTime() + updatedAt := resp.GetResult().GetUpdatedAt().AsTime() result.Created = &createdAt result.LastModified = &updatedAt - result.Version = resp.Result.Etag + result.Version = resp.GetResult().GetEtag() } } - for _, relation := range data.Relations { - if relation.Relation == s.cfg.Group.GroupMemberRelation { - addedMembers = append(addedMembers, relation.SubjectId) + for _, relation := range data.GetRelations() { + if relation.GetRelation() == s.cfg.Group.GroupMemberRelation { + addedMembers = append(addedMembers, relation.GetSubjectId()) } + logger.Trace().Any("relation", relation).Msg("setting relation") _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ Relation: relation, }) + if err != nil { return result, err } } if relations != nil { - for _, rel := range relations.Results { - if !slices.Contains(addedMembers, rel.SubjectId) { - logger.Trace().Str("id", rel.SubjectId).Msg("deleting relation") + for _, rel := range relations.GetResults() { + if !slices.Contains(addedMembers, rel.GetSubjectId()) { + logger.Trace().Str("id", rel.GetSubjectId()).Msg("deleting relation") _, err := s.client.Writer.DeleteRelation(ctx, &dsw.DeleteRelationRequest{ ObjectType: s.cfg.Group.ObjectType, ObjectId: groupID, Relation: s.cfg.Group.GroupMemberRelation, - SubjectId: rel.SubjectId, - SubjectType: rel.SubjectType, + SubjectId: rel.GetSubjectId(), + SubjectType: rel.GetSubjectType(), }) + if err != nil { return result, err } @@ -285,6 +320,7 @@ func (s *Client) SetGroup(ctx context.Context, groupID string, data *msg.Transfo func (s *Client) DeleteGroup(ctx context.Context, groupID string) error { logger := s.logger.With().Str("method", "DeleteGroup").Str("id", groupID).Logger() logger.Trace().Msg("delete group") + _, err := s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectType: s.cfg.Group.SourceObjectType, ObjectId: groupID, @@ -322,5 +358,6 @@ func (s *Client) setRelations(ctx context.Context, subjID, subjType string) erro } } } + return nil } diff --git a/common/handlers/groups/create.go b/common/handlers/groups/create.go index 0ec3c47..af42f20 100644 --- a/common/handlers/groups/create.go +++ b/common/handlers/groups/create.go @@ -15,12 +15,14 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour if !ok { return scim.Resource{}, serrors.ScimErrorInvalidSyntax } + logger := g.logger.With().Str("method", "Create").Str("name", groupName).Logger() logger.Info().Msg("create group") logger.Trace().Any("attributes", attributes).Msg("creating group") group := &model.Group{} err := convert.Unmarshal(attributes, group) + if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to group") return scim.Resource{}, serrors.ScimErrorInvalidSyntax @@ -30,6 +32,7 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour converter := convert.NewConverter(g.cfg) object, err := converter.SCIMGroupToObject(group) + if err != nil { logger.Error().Err(err).Msg("failed to convert group to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax @@ -49,13 +52,13 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour return scim.Resource{}, serrors.ScimErrorInvalidSyntax } - meta, err := g.dirClient.SetGroup(ctx, sourceGroupResp.Result.Id, transformResult) + meta, err := g.dirClient.SetGroup(ctx, sourceGroupResp.GetResult().GetId(), transformResult) if err != nil { logger.Error().Err(err).Msg("failed to sync group") return scim.Resource{}, err } - result = converter.ObjectToResource(sourceGroupResp.Result, meta) + result = converter.ObjectToResource(sourceGroupResp.GetResult(), meta) logger.Trace().Any("response", result).Msg("group created") diff --git a/common/handlers/groups/get.go b/common/handlers/groups/get.go index af1d87c..bfe5503 100644 --- a/common/handlers/groups/get.go +++ b/common/handlers/groups/get.go @@ -31,12 +31,12 @@ func (g GroupResourceHandler) Get(ctx context.Context, id string) (scim.Resource converter := convert.NewConverter(g.cfg) - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() - resource := converter.ObjectToResource(resp.Result, scim.Meta{ + createdAt := resp.GetResult().GetCreatedAt().AsTime() + updatedAt := resp.GetResult().GetUpdatedAt().AsTime() + resource := converter.ObjectToResource(resp.GetResult(), scim.Meta{ Created: &createdAt, LastModified: &updatedAt, - Version: resp.Result.Etag, + Version: resp.GetResult().GetEtag(), }) return resource, nil @@ -68,13 +68,13 @@ func (g GroupResourceHandler) GetAll(ctx context.Context, params scim.ListReques converter := convert.NewConverter(g.cfg) - for _, v := range resp.Results { - createdAt := v.CreatedAt.AsTime() - updatedAt := v.UpdatedAt.AsTime() + for _, v := range resp.GetResults() { + createdAt := v.GetCreatedAt().AsTime() + updatedAt := v.GetUpdatedAt().AsTime() resource := converter.ObjectToResource(v, scim.Meta{ Created: &createdAt, LastModified: &updatedAt, - Version: v.Etag, + Version: v.GetEtag(), }) resources = append(resources, resource) } diff --git a/common/handlers/groups/patch.go b/common/handlers/groups/patch.go index 2c86cec..d17ec15 100644 --- a/common/handlers/groups/patch.go +++ b/common/handlers/groups/patch.go @@ -31,14 +31,16 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ if err != nil { logger.Error().Err(err).Msg("failed to get group") st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } + return scim.Resource{}, err } converter := convert.NewConverter(g.cfg) - attr := converter.ObjectToResourceAttributes(getObjResp.Result) + attr := converter.ObjectToResourceAttributes(getObjResp.GetResult()) for _, op := range operations { switch op.Op { @@ -69,28 +71,31 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ return scim.Resource{}, serrors.ScimErrorInvalidSyntax } - groupObj := getObjResp.Result + groupObj := getObjResp.GetResult() props, err := structpb.NewStruct(attr) + if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to struct") return scim.Resource{}, err } + groupObj.Properties = props sourceGroupResp, err := g.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: groupObj, }) + if err != nil { logger.Error().Err(err).Msg("failed to replace group") return scim.Resource{}, err } - meta, err := g.dirClient.SetGroup(ctx, getObjResp.Result.Id, transformResult) + meta, err := g.dirClient.SetGroup(ctx, getObjResp.GetResult().GetId(), transformResult) if err != nil { logger.Error().Err(err).Msg("failed to sync group") return scim.Resource{}, err } - resource := converter.ObjectToResource(sourceGroupResp.Result, meta) + resource := converter.ObjectToResource(sourceGroupResp.GetResult(), meta) logger.Trace().Any("response", resource).Msg("group patched") diff --git a/common/handlers/groups/replace.go b/common/handlers/groups/replace.go index 4df6d1f..1187973 100644 --- a/common/handlers/groups/replace.go +++ b/common/handlers/groups/replace.go @@ -23,5 +23,6 @@ func (g GroupResourceHandler) Replace(ctx context.Context, id string, attributes } logger.Trace().Any("resource", resource).Msg("group replaced") + return resource, nil } diff --git a/common/handlers/users/create.go b/common/handlers/users/create.go index 37b88dc..67c3755 100644 --- a/common/handlers/users/create.go +++ b/common/handlers/users/create.go @@ -15,12 +15,14 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour if !ok { return scim.Resource{}, serrors.ScimErrorInvalidSyntax } + logger := u.logger.With().Str("method", "Create").Str("userName", userName).Logger() logger.Info().Msg("create user") logger.Trace().Any("attributes", attributes).Msg("creating user") user := &model.User{} err := convert.Unmarshal(attributes, user) + if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to user") return scim.Resource{}, serrors.ScimErrorInvalidSyntax @@ -30,19 +32,22 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour converter := convert.NewConverter(u.cfg) object, err := converter.SCIMUserToObject(user) + if err != nil { logger.Error().Err(err).Msg("failed to convert user to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } + sourceUserResp, err := u.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: object, }) + if err != nil { logger.Error().Err(err).Msg("failed to create user") return scim.Resource{}, err } - userMap, err := convert.ProtobufStructToMap(sourceUserResp.Result.Properties) + userMap, err := convert.ProtobufStructToMap(sourceUserResp.GetResult().GetProperties()) if err != nil { logger.Error().Err(err).Msg("failed to convert user to map") return scim.Resource{}, err @@ -54,13 +59,13 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour return scim.Resource{}, serrors.ScimErrorInvalidSyntax } - meta, err := u.dirClient.SetUser(ctx, sourceUserResp.Result.Id, transformResult, attributes) + meta, err := u.dirClient.SetUser(ctx, sourceUserResp.GetResult().GetId(), transformResult, attributes) if err != nil { logger.Error().Err(err).Msg("failed to sync user") return scim.Resource{}, err } - result = converter.ObjectToResource(sourceUserResp.Result, meta) + result = converter.ObjectToResource(sourceUserResp.GetResult(), meta) logger.Trace().Any("response", result).Msg("user created") diff --git a/common/handlers/users/delete.go b/common/handlers/users/delete.go index 4a8a9f6..e1ea2f6 100644 --- a/common/handlers/users/delete.go +++ b/common/handlers/users/delete.go @@ -20,41 +20,44 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { } resp, err := u.dirClient.DS().Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ - SubjectType: identityRelation.SubjectType, - SubjectId: identityRelation.SubjectId, - ObjectType: identityRelation.ObjectType, - ObjectId: identityRelation.ObjectId, - Relation: identityRelation.Relation, + SubjectType: identityRelation.GetSubjectType(), + SubjectId: identityRelation.GetSubjectId(), + ObjectType: identityRelation.GetObjectType(), + ObjectId: identityRelation.GetObjectId(), + Relation: identityRelation.GetRelation(), }) if err != nil { logger.Error().Err(err).Msg("failed to get relations") st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(id) } + return err } - identities := resp.Results - - for _, v := range identities { + for _, v := range resp.GetResults() { var objectID string - switch v.ObjectType { + + switch v.GetObjectType() { case u.cfg.User.IdentityObjectType: - objectID = v.ObjectId + objectID = v.GetObjectId() case u.cfg.User.ObjectType: - objectID = v.SubjectId + objectID = v.GetSubjectId() default: - logger.Error().Str("object_type", v.ObjectType).Msg("unexpected object type") + logger.Error().Str("object_type", v.GetObjectType()).Msg("unexpected object type") return serrors.ScimErrorBadRequest("unexpected object type in identity relation") } - logger.Trace().Str("id", v.ObjectId).Msg("deleting identity") + logger.Trace().Str("id", v.GetObjectId()).Msg("deleting identity") + _, err = u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectId: objectID, ObjectType: u.cfg.User.IdentityObjectType, WithRelations: true, }) + if err != nil { logger.Error().Err(err).Msg("failed to delete identity") return err @@ -62,6 +65,7 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { } logger.Trace().Msg("deleting user") + _, err = u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectType: u.cfg.User.ObjectType, ObjectId: id, @@ -70,12 +74,14 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { if err != nil { logger.Error().Err(err).Msg("failed to delete user") st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(id) } } logger.Trace().Msg("deleting user source object") + _, err = u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectType: u.cfg.User.SourceObjectType, ObjectId: id, @@ -84,11 +90,13 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { if err != nil { logger.Error().Err(err).Msg("failed to delete user source object") st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(id) } } logger.Trace().Msg("user deleted") + return err } diff --git a/common/handlers/users/get.go b/common/handlers/users/get.go index 865afb1..ec795ef 100644 --- a/common/handlers/users/get.go +++ b/common/handlers/users/get.go @@ -26,18 +26,20 @@ func (u UsersResourceHandler) Get(ctx context.Context, id string) (scim.Resource if err != nil { logger.Error().Err(err).Msg("failed to get user") st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } + return scim.Resource{}, err } - createdAt := resp.Result.CreatedAt.AsTime() - updatedAt := resp.Result.UpdatedAt.AsTime() - resource := converter.ObjectToResource(resp.Result, scim.Meta{ + createdAt := resp.GetResult().GetCreatedAt().AsTime() + updatedAt := resp.GetResult().GetUpdatedAt().AsTime() + resource := converter.ObjectToResource(resp.GetResult(), scim.Meta{ Created: &createdAt, LastModified: &updatedAt, - Version: resp.Result.Etag, + Version: resp.GetResult().GetEtag(), }) logger.Trace().Any("user", resource).Msg("user retrieved") @@ -69,15 +71,15 @@ func (u UsersResourceHandler) GetAll(ctx context.Context, params scim.ListReques return scim.Page{}, err } - pageToken = resp.Page.NextToken + pageToken = resp.GetPage().GetNextToken() - for _, v := range resp.Results { - createdAt := v.CreatedAt.AsTime() - updatedAt := v.UpdatedAt.AsTime() + for _, v := range resp.GetResults() { + createdAt := v.GetCreatedAt().AsTime() + updatedAt := v.GetUpdatedAt().AsTime() resource := converter.ObjectToResource(v, scim.Meta{ Created: &createdAt, LastModified: &updatedAt, - Version: v.Etag, + Version: v.GetEtag(), }) if params.FilterValidator == nil || params.FilterValidator.PassesFilter(resource.Attributes) == nil { @@ -85,6 +87,7 @@ func (u UsersResourceHandler) GetAll(ctx context.Context, params scim.ListReques skipIndex++ continue } + resources = append(resources, resource) } diff --git a/common/handlers/users/patch.go b/common/handlers/users/patch.go index 08e2bd2..1b601f2 100644 --- a/common/handlers/users/patch.go +++ b/common/handlers/users/patch.go @@ -29,13 +29,15 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ if err != nil { logger.Error().Err(err).Msg("failed to get user") st, ok := status.FromError(err) + if ok && st.Code() == codes.NotFound { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } + return scim.Resource{}, err } - attr := converter.ObjectToResourceAttributes(getObjResp.Result) + attr := converter.ObjectToResourceAttributes(getObjResp.GetResult()) for _, op := range operations { switch op.Op { @@ -71,28 +73,31 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ return scim.Resource{}, serrors.ScimErrorInvalidSyntax } - userObj := getObjResp.Result + userObj := getObjResp.GetResult() props, err := structpb.NewStruct(attr) + if err != nil { logger.Error().Err(err).Msg("failed to convert resource attributes to struct") return scim.Resource{}, err } + userObj.Properties = props sourceUserResp, err := u.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: userObj, }) + if err != nil { logger.Error().Err(err).Msg("failed to replace user") return scim.Resource{}, err } - meta, err := u.dirClient.SetUser(ctx, getObjResp.Result.Id, transformResult, attr) + meta, err := u.dirClient.SetUser(ctx, getObjResp.GetResult().GetId(), transformResult, attr) if err != nil { logger.Error().Err(err).Msg("failed to sync user") return scim.Resource{}, err } - resource := converter.ObjectToResource(sourceUserResp.Result, meta) + resource := converter.ObjectToResource(sourceUserResp.GetResult(), meta) logger.Trace().Any("response", resource).Msg("user patched") diff --git a/common/patch.go b/common/patch.go index 83631b6..630b53b 100644 --- a/common/patch.go +++ b/common/patch.go @@ -23,7 +23,7 @@ func HandlePatchOPAdd(objectProps scim.ResourceAttributes, op scim.PatchOperatio return nil, err } - properties := make(map[string]interface{}) + properties := make(map[string]any) if op.Path.ValueExpression == nil { properties[*op.Path.SubAttribute] = op.Value @@ -31,15 +31,17 @@ func HandlePatchOPAdd(objectProps scim.ResourceAttributes, op scim.PatchOperatio } if objectProps[op.Path.AttributePath.AttributeName] != nil { - attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) if !ok { return nil, serrors.ScimErrorInvalidPath } + for _, v := range attrProps { - originalValue, ok := v.(map[string]interface{}) + originalValue, ok := v.(map[string]any) if !ok { return nil, serrors.ScimErrorInvalidPath } + switch fltr.Operator { case filter.EQ: value, ok := originalValue[fltr.AttributePath.AttributeName].(string) @@ -47,6 +49,7 @@ func HandlePatchOPAdd(objectProps scim.ResourceAttributes, op scim.PatchOperatio if originalValue[*op.Path.SubAttribute] != nil { return nil, serrors.ScimErrorUniqueness } + properties = originalValue } case filter.PR, filter.NE, filter.CO, filter.SW, filter.EW, filter.GT, filter.GE, filter.LT, filter.LE: @@ -54,15 +57,18 @@ func HandlePatchOPAdd(objectProps scim.ResourceAttributes, op scim.PatchOperatio } } } else { - objectProps[op.Path.AttributePath.AttributeName] = make([]interface{}, 0) + objectProps[op.Path.AttributePath.AttributeName] = make([]any, 0) } + if len(properties) == 0 { properties[fltr.AttributePath.AttributeName] = fltr.CompareValue properties[*op.Path.SubAttribute] = op.Value - attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) + if !ok { return nil, serrors.ScimErrorInvalidPath } + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, properties) } @@ -75,35 +81,43 @@ func HandlePatchOPRemove(objectProps scim.ResourceAttributes, op scim.PatchOpera switch value := objectProps[op.Path.AttributePath.AttributeName].(type) { case string: delete(objectProps, op.Path.AttributePath.AttributeName) - case []interface{}: + case []any: attrExpr, ok := op.Path.ValueExpression.(*filter.AttributeExpression) if !ok { return nil, serrors.ScimErrorInvalidPath } + ftr, err := filter.ParseAttrExp([]byte(attrExpr.String())) + if err != nil { return nil, err } index := -1 + if ftr.Operator == filter.EQ { for i, v := range value { - originalValue, ok := v.(map[string]interface{}) + originalValue, ok := v.(map[string]any) if !ok { return nil, serrors.ScimErrorInvalidPath } + value, ok := originalValue[ftr.AttributePath.AttributeName].(string) if ok && value == ftr.CompareValue { index = i } } + if index == -1 { return nil, serrors.ScimErrorMutability } - attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) + + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) + if !ok { return nil, serrors.ScimErrorInvalidPath } + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps[:index], attrProps[index+1:]...) } } @@ -115,7 +129,7 @@ func HandlePatchOPReplace(objectProps scim.ResourceAttributes, op scim.PatchOper var err error if op.Path == nil { - value, ok := op.Value.(map[string]interface{}) + value, ok := op.Value.(map[string]any) if ok { for k, v := range value { objectProps[k] = v @@ -128,13 +142,13 @@ func HandlePatchOPReplace(objectProps scim.ResourceAttributes, op scim.PatchOper switch value := objectProps[op.Path.AttributePath.AttributeName].(type) { case string: objectProps[op.Path.AttributePath.AttributeName] = op.Value - case map[string]interface{}: + case map[string]any: if op.Path.AttributePath.SubAttribute != nil { value[*op.Path.AttributePath.SubAttribute] = op.Value } else { objectProps[op.Path.AttributePath.AttributeName] = op.Value } - case []interface{}: + case []any: if op.Path.ValueExpression == nil { objectProps[op.Path.AttributePath.AttributeName] = op.Value break @@ -144,40 +158,48 @@ func HandlePatchOPReplace(objectProps scim.ResourceAttributes, op scim.PatchOper if err != nil { return nil, err } + objectProps[op.Path.AttributePath.AttributeName] = value } return objectProps, err } -func ReplaceInInterfaceArray(value []interface{}, op scim.PatchOperation) ([]interface{}, error) { +func ReplaceInInterfaceArray(value []any, op scim.PatchOperation) ([]any, error) { attrExpr, ok := op.Path.ValueExpression.(*filter.AttributeExpression) if !ok { return nil, serrors.ScimErrorInvalidPath } + ftr, err := filter.ParseAttrExp([]byte(attrExpr.String())) + if err != nil { return nil, err } index := -1 + switch ftr.Operator { case filter.EQ: for i, v := range value { - originalValue, ok := v.(map[string]interface{}) + originalValue, ok := v.(map[string]any) + if !ok { return nil, serrors.ScimErrorInvalidPath } + value, ok := originalValue[ftr.AttributePath.AttributeName].(string) + if ok && value == ftr.CompareValue { index = i } } + if index == -1 { return nil, serrors.ScimErrorMutability } - if originalValue, ok := value[index].(map[string]interface{}); ok { + if originalValue, ok := value[index].(map[string]any); ok { originalValue[*op.Path.SubAttribute] = op.Value value[index] = originalValue @@ -199,33 +221,37 @@ func AddProperty(objectProps scim.ResourceAttributes, op scim.PatchOperation) (s if objectProps[op.Path.AttributePath.AttributeName] != nil { return nil, serrors.ScimErrorUniqueness } + objectProps[op.Path.AttributePath.AttributeName] = op.Value - case map[string]interface{}: + case map[string]any: for k, v := range value { if objectProps[k] != nil { return nil, serrors.ScimErrorUniqueness } + objectProps[k] = v } - case []interface{}: + case []any: for _, v := range value { switch val := v.(type) { case string: if objectProps[op.Path.AttributePath.AttributeName] == nil { objectProps[op.Path.AttributePath.AttributeName] = make([]string, 0) } - if attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}); ok { + + if attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any); ok { objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, v) } else { return nil, serrors.ScimErrorInvalidPath } - case map[string]interface{}: + case map[string]any: if objectProps[op.Path.AttributePath.AttributeName] == nil { - objectProps[op.Path.AttributePath.AttributeName] = make([]interface{}, 0) + objectProps[op.Path.AttributePath.AttributeName] = make([]any, 0) } properties := val - attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]interface{}) + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) + if !ok { return nil, serrors.ScimErrorInvalidPath } diff --git a/go.mod b/go.mod index db94176..95633a3 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/aserto-dev/go-directory v0.33.10 github.com/aserto-dev/logger v0.0.9 github.com/aserto-dev/scim/common v0.0.0-00010101000000-000000000000 - github.com/aserto-dev/topaz v0.32.58 github.com/docker/go-connections v0.5.0 github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 github.com/gavv/httpexpect/v2 v2.17.0 @@ -24,6 +23,7 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect @@ -127,6 +127,7 @@ require ( golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 // indirect google.golang.org/grpc v1.71.0 // indirect diff --git a/go.sum b/go.sum index 260ac79..bc856e0 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,6 @@ github.com/aserto-dev/header v0.0.11 h1:Qx7lWzfq29h0OgaJQ8W9KD+/q9q14yOwKBFmD0vi github.com/aserto-dev/header v0.0.11/go.mod h1:yTO0YPKVTlUTcP0ecQ/7qKs6l6RvDS0ac5l+S1BGWBs= github.com/aserto-dev/logger v0.0.9 h1:QH11l8937Sw+GAe2yvgpoLg70fqQvPrEufkXAmDUk0g= github.com/aserto-dev/logger v0.0.9/go.mod h1:mMXq/bhdIKoOVsIZ2zJOqgjcc/jR2x14FhU0St/8AVQ= -github.com/aserto-dev/topaz v0.32.58 h1:ffw6MPHhfMn268z+NqmFmBR6fqrr4uHKOCiqhxWdGi4= -github.com/aserto-dev/topaz v0.32.58/go.mod h1:Llk5TuO8XkydJLiiHRVVp+oJjptr4e1+aSe/8ysOmlY= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= diff --git a/pkg/app/directory/client.go b/pkg/app/directory/client.go index c9f2508..acb7aa1 100644 --- a/pkg/app/directory/client.go +++ b/pkg/app/directory/client.go @@ -12,5 +12,4 @@ func GetDirectoryClient(cfg *client.Config) (*ds.Client, error) { } return ds.FromConnection(conn), nil - } diff --git a/pkg/app/run.go b/pkg/app/run.go index 05cfcbb..089e0b5 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "strings" - "time" "github.com/aserto-dev/go-aserto/ds/v3" "github.com/aserto-dev/logger" @@ -123,16 +122,18 @@ func (s *SCIMServer) Run() error { } srv := &http.Server{ - Addr: s.cfg.Server.ListenAddress, - Handler: app.auth(server.ServeHTTP), - TLSConfig: tlsServerConfig, - IdleTimeout: time.Minute, - ReadTimeout: 10 * time.Second, - WriteTimeout: 30 * time.Second, + Addr: s.cfg.Server.ListenAddress, + Handler: app.auth(server.ServeHTTP), + TLSConfig: tlsServerConfig, + IdleTimeout: s.cfg.Server.IdleTimeout, + ReadTimeout: s.cfg.Server.ReadTimeout, + WriteTimeout: s.cfg.Server.WriteTimeout, + ReadHeaderTimeout: s.cfg.Server.ReadHeaderTimeout, } s.server = srv s.log.Info().Str("address", s.cfg.Server.ListenAddress).Msg("Starting SCIM server") + if s.cfg.Server.Certs.HasCert() { return srv.ListenAndServeTLS("", "") } @@ -147,14 +148,17 @@ func (s *SCIMServer) Shutdown(ctx context.Context) error { s.log.Info().Msg("Shutting down SCIM server") return s.server.Shutdown(ctx) } + s.server = nil if s.dsClient != nil { s.log.Info().Msg("Closing directory client connection") + if err := s.dsClient.Close(); err != nil { s.log.Error().Err(err).Msg("Failed to close directory client") } } + s.dsClient = nil s.log.Info().Msg("SCIM server shutdown complete") @@ -164,6 +168,7 @@ func (s *SCIMServer) Shutdown(ctx context.Context) error { func userHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsClient *ds.Client) (scim.ResourceHandler, error) { usersLogger := scimLogger.With().Str("component", "users").Logger() usersResourceHandler, err := users.NewUsersResourceHandler(&usersLogger, cfg, dsClient) + if err != nil { return nil, err } @@ -174,6 +179,7 @@ func userHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsCli func groupHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsClient *ds.Client) (scim.ResourceHandler, error) { groupsLogger := scimLogger.With().Str("component", "groups").Logger() groupsResourceHandler, err := groups.NewGroupResourceHandler(&groupsLogger, cfg, dsClient) + if err != nil { return nil, err } @@ -181,6 +187,8 @@ func groupHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsCl return NewGroupResourceHandler(groupsResourceHandler) } +const authzHeaderParts = 2 + type application struct { cfg *config.AuthConfig } @@ -193,23 +201,14 @@ func (app *application) auth(next http.HandlerFunc) http.HandlerFunc { } username, password, ok := r.BasicAuth() - if ok && app.cfg.Basic.Enabled { - usernameHash := sha256.Sum256([]byte(username)) - passwordHash := sha256.Sum256([]byte(password)) - expectedUsernameHash := sha256.Sum256([]byte(app.cfg.Basic.Username)) - expectedPasswordHash := sha256.Sum256([]byte(app.cfg.Basic.Password)) - - usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) - passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) - - if usernameMatch && passwordMatch { - next.ServeHTTP(w, r) - return - } + if ok && app.cfg.Basic.Enabled && app.checkBasicAuth(username, password) { + next.ServeHTTP(w, r) + return } else if app.cfg.Bearer.Enabled { reqToken := r.Header.Get("Authorization") splitToken := strings.Split(reqToken, "Bearer ") - if len(splitToken) == 2 { + + if len(splitToken) == authzHeaderParts { if subtle.ConstantTimeCompare([]byte(app.cfg.Bearer.Token), []byte(splitToken[1])) == 1 { next.ServeHTTP(w, r) return @@ -221,3 +220,20 @@ func (app *application) auth(next http.HandlerFunc) http.HandlerFunc { http.Error(w, "Unauthorized", http.StatusUnauthorized) }) } + +func (app *application) checkBasicAuth(username, password string) bool { + if username == "" || password == "" { + return false + } + + usernameHash := sha256.Sum256([]byte(username)) + passwordHash := sha256.Sum256([]byte(password)) + + expectedUsernameHash := sha256.Sum256([]byte(app.cfg.Basic.Username)) + expectedPasswordHash := sha256.Sum256([]byte(app.cfg.Basic.Password)) + + usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) + passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) + + return usernameMatch && passwordMatch +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 93e68dc..9fe3335 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,6 +3,7 @@ package config import ( "os" "strings" + "time" client "github.com/aserto-dev/go-aserto" "github.com/aserto-dev/logger" @@ -13,6 +14,13 @@ import ( "github.com/spf13/viper" ) +const ( + DefaultReadTimeout = 5 * time.Second + DefaultReadHeaderTimeout = 2 * time.Second + DefaultWriteTimeout = 10 * time.Second + DefaultIdleTimeout = 30 * time.Second +) + var ( DefaultTLSGenDir = os.ExpandEnv("$HOME/.config/aserto/scim/certs") ErrInvalidConfig = errors.New("invalid config") @@ -22,9 +30,13 @@ type Config struct { Logging logger.Config `json:"logging"` Directory client.Config `json:"directory"` Server struct { - ListenAddress string `json:"listen_address"` - Certs client.TLSConfig `json:"certs"` - Auth AuthConfig `json:"auth"` + ListenAddress string `json:"listen_address"` + Certs client.TLSConfig `json:"certs"` + Auth AuthConfig `json:"auth"` + ReadTimeout time.Duration `json:"read_timeout"` + ReadHeaderTimeout time.Duration `json:"read_header_timeout"` + WriteTimeout time.Duration `json:"write_timeout"` + IdleTimeout time.Duration `json:"idle_timeout"` } `json:"server"` SCIM config.Config `json:"scim"` @@ -42,7 +54,7 @@ type AuthConfig struct { } `json:"bearer"` } -func NewConfig(configPath string) (*Config, error) { // nolint // function will contain repeating statements for defaults +func NewConfig(configPath string) (*Config, error) { file := "config.yaml" v := viper.New() @@ -70,6 +82,11 @@ func NewConfig(configPath string) (*Config, error) { // nolint // function will v.SetDefault("server.auth.basic.enabled", "false") v.SetDefault("server.auth.bearer.enabled", "false") + v.SetDefault("server.read_timeout", DefaultReadTimeout) + v.SetDefault("server.read_header_timeout", DefaultReadHeaderTimeout) + v.SetDefault("server.write_timeout", DefaultWriteTimeout) + v.SetDefault("server.idle_timeout", DefaultIdleTimeout) + v.SetDefault("scim.user.object_type", "user") v.SetDefault("scim.user.identity_object_type", "identity") v.SetDefault("scim.user.identity_relation", "user#identifier") diff --git a/pkg/test/assets/config/topaz.yaml b/pkg/test/assets/config/topaz.yaml index 8818dd3..040cf7a 100644 --- a/pkg/test/assets/config/topaz.yaml +++ b/pkg/test/assets/config/topaz.yaml @@ -11,7 +11,7 @@ logging: # edge directory configuration. directory: - db_path: '${TOPAZ_DB_DIR}/test-no-tls.db' + db_path: '/data/db.db' request_timeout: 5s # set as default, 5 secs. # remote directory is used to resolve the identity for the authorizer. diff --git a/pkg/test/common/common.go b/pkg/test/common/common.go index b141701..e2403cf 100644 --- a/pkg/test/common/common.go +++ b/pkg/test/common/common.go @@ -16,14 +16,15 @@ import ( "github.com/aserto-dev/logger" "github.com/aserto-dev/scim/pkg/app" assets_test "github.com/aserto-dev/scim/pkg/test/assets" - "github.com/aserto-dev/topaz/pkg/cli/x" "github.com/docker/go-connections/nat" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" testcontainers "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) +const containerStartTimeout = 300 * time.Second +const topazConfigFileMode = 0o700 + type TestCase struct { Topaz testcontainers.Container DirectoryClient *ds.Client @@ -44,45 +45,41 @@ func (tst *TestCase) ContainerLogs(ctx context.Context, t *testing.T) string { } func TestSetup(t *testing.T) TestCase { - ctx, cancel := context.WithCancel(context.Background()) - t.Logf("\nTEST CONTAINER IMAGE: %q\n", TopazImage()) req := testcontainers.ContainerRequest{ + Name: "scim-topaz", Image: TopazImage(), ExposedPorts: []string{"9292/tcp"}, - Env: map[string]string{ - x.EnvTopazCertsDir: x.DefCertsDir, - x.EnvTopazDBDir: x.DefDBDir, - x.EnvTopazDecisions: x.DefDecisionsDir, - }, Files: []testcontainers.ContainerFile{ { Reader: assets_test.TopazConfigReader(), ContainerFilePath: "/config/config.yaml", - FileMode: 0x700, + FileMode: topazConfigFileMode, }, }, WaitingFor: wait.ForAll( wait.ForExposedPort(), wait.ForLog("Starting 0.0.0.0:9292 gRPC server"), - ).WithStartupTimeoutDefault(300 * time.Second), + ).WithStartupTimeoutDefault(containerStartTimeout), } - topaz, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + topaz, err := testcontainers.GenericContainer(t.Context(), testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: false, + Reuse: true, }) require.NoError(t, err) - if err := topaz.Start(ctx); err != nil { + if err := topaz.Start(t.Context()); err != nil { require.NoError(t, err) } - addr, err := MappedAddr(ctx, topaz, "9292") + addr, err := MappedAddr(t.Context(), topaz, "9292") require.NoError(t, err) - os.Setenv("ASERTO_SCIM_DIRECTORY_ADDRESS", addr) + t.Setenv("ASERTO_SCIM_DIRECTORY_ADDRESS", addr) + scimConfig, err := filepath.Abs("assets/config/scim.yaml") require.NoError(t, err) @@ -90,8 +87,7 @@ func TestSetup(t *testing.T) TestCase { require.NoError(t, err) go func() { - err := srv.Run() - require.Error(t, err) + srv.Run() }() time.Sleep(time.Second) @@ -105,8 +101,8 @@ func TestSetup(t *testing.T) TestCase { require.NoError(t, err) dsClient := ds.FromConnection(conn) - stream, err := dsClient.Model.SetManifest(ctx) - assert.NoError(t, err) + stream, err := dsClient.Model.SetManifest(t.Context()) + require.NoError(t, err) err = stream.Send(&dsm.SetManifestRequest{ Msg: &dsm.SetManifestRequest_Body{ Body: &dsm.Body{ @@ -114,15 +110,14 @@ func TestSetup(t *testing.T) TestCase { }, }, }) - assert.NoError(t, err) + require.NoError(t, err) _, err = stream.CloseAndRecv() - assert.NoError(t, err) + require.NoError(t, err) t.Cleanup(func() { conn.Close() - srv.Shutdown(ctx) + srv.Shutdown(t.Context()) testcontainers.CleanupContainer(t, topaz) - cancel() }) return TestCase{ @@ -142,7 +137,8 @@ func (tst *TestCase) UserHasIdentity(ctx context.Context, user, identity string) if err != nil { return false } - return userResp.Result != nil + + return userResp.GetResult() != nil } func (tst *TestCase) UserHasManager(ctx context.Context, user, manager string) bool { @@ -156,7 +152,8 @@ func (tst *TestCase) UserHasManager(ctx context.Context, user, manager string) b if err != nil { return false } - return userResp.Result != nil + + return userResp.GetResult() != nil } func (tst *TestCase) ReadUserProperty(ctx context.Context, user, property string) any { @@ -164,11 +161,11 @@ func (tst *TestCase) ReadUserProperty(ctx context.Context, user, property string ObjectType: "user", ObjectId: user, }) - if err != nil || userResp.Result == nil { + if err != nil || userResp.GetResult() == nil { return nil } - return userResp.Result.Properties.Fields[property].AsInterface() + return userResp.GetResult().GetProperties().GetFields()[property].AsInterface() } func TopazImage() string { @@ -176,6 +173,7 @@ func TopazImage() string { if image != "" { return image } + return "ghcr.io/aserto-dev/topaz:latest" } diff --git a/pkg/test/scim_test.go b/pkg/test/scim_test.go index 1dc8397..ed52f4b 100644 --- a/pkg/test/scim_test.go +++ b/pkg/test/scim_test.go @@ -2,7 +2,6 @@ package scim_test import ( "encoding/json" - "fmt" "testing" assets_test "github.com/aserto-dev/scim/pkg/test/assets" @@ -15,7 +14,7 @@ import ( func TestScim(t *testing.T) { // Setup test containers tst := common_test.TestSetup(t) - fmt.Println("Directory address:", tst.DirectoryClient) + e := httpexpect.Default(t, "http://localhost:8081") // Create user for Rick From 6a70fdb7a05e68fb5765f3164f5da14c47bcfe52 Mon Sep 17 00:00:00 2001 From: florindragos Date: Tue, 15 Apr 2025 18:27:02 +0300 Subject: [PATCH 10/13] use latest topaz --- pkg/test/common/common.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/test/common/common.go b/pkg/test/common/common.go index e2403cf..4b86fae 100644 --- a/pkg/test/common/common.go +++ b/pkg/test/common/common.go @@ -48,9 +48,9 @@ func TestSetup(t *testing.T) TestCase { t.Logf("\nTEST CONTAINER IMAGE: %q\n", TopazImage()) req := testcontainers.ContainerRequest{ - Name: "scim-topaz", - Image: TopazImage(), - ExposedPorts: []string{"9292/tcp"}, + AlwaysPullImage: true, + Image: TopazImage(), + ExposedPorts: []string{"9292/tcp"}, Files: []testcontainers.ContainerFile{ { Reader: assets_test.TopazConfigReader(), @@ -67,7 +67,6 @@ func TestSetup(t *testing.T) TestCase { topaz, err := testcontainers.GenericContainer(t.Context(), testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: false, - Reuse: true, }) require.NoError(t, err) From 154844e024d43dfc5f9300ba102f88e70415b7f6 Mon Sep 17 00:00:00 2001 From: florindragos Date: Wed, 16 Apr 2025 16:41:46 +0300 Subject: [PATCH 11/13] more lint fixes --- .golangci.yaml | 12 +-- cmd/aserto-scim/main.go | 4 +- common/convert/convert.go | 10 +-- common/directory/client.go | 131 +++++++++++++++++++---------- common/handlers/groups/create.go | 4 +- common/handlers/groups/get.go | 4 +- common/handlers/groups/patch.go | 31 +++++-- common/handlers/users/create.go | 5 +- common/handlers/users/delete.go | 22 +++-- common/handlers/users/patch.go | 28 ++++--- common/patch.go | 135 +++++++++++++++++++++--------- pkg/app/groups.go | 42 ---------- pkg/app/handler.go | 42 ++++++++++ pkg/app/run.go | 97 +++++++++++---------- pkg/app/users.go | 42 ---------- pkg/test/assets/config/topaz.yaml | 94 --------------------- pkg/test/common/common.go | 6 +- 17 files changed, 354 insertions(+), 355 deletions(-) delete mode 100644 pkg/app/groups.go create mode 100644 pkg/app/handler.go delete mode 100644 pkg/app/users.go diff --git a/.golangci.yaml b/.golangci.yaml index 4f18734..6216a9c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -45,8 +45,7 @@ linters: ignore-comments: true gomoddirectives: - replace-allow-list: - - github.com/slok/go-http-metrics + replace-local: true gosec: excludes: @@ -57,14 +56,7 @@ linters: allow: - error - empty - - stdlib - - generic - - proto.Message - - plugins.Plugin - - decisionlog.DecisionLogger - - resolvers.DirectoryResolver - - resolvers.RuntimeResolver - - v3.ReaderClient + - scim.ResourceHandler lll: line-length: 150 diff --git a/cmd/aserto-scim/main.go b/cmd/aserto-scim/main.go index 10be0f1..be8d1a3 100644 --- a/cmd/aserto-scim/main.go +++ b/cmd/aserto-scim/main.go @@ -13,9 +13,7 @@ import ( "github.com/spf13/cobra" ) -var ( - flagConfigPath string -) +var flagConfigPath string var rootCmd = &cobra.Command{ Use: "aserto-scim [flags]", diff --git a/common/convert/convert.go b/common/convert/convert.go index 0d9a56c..12f96ee 100644 --- a/common/convert/convert.go +++ b/common/convert/convert.go @@ -59,8 +59,8 @@ func Unmarshal[S any, D any](source S, dest *D) error { func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { attributes := scim.ResourceAttributes{} - err := Unmarshal(user, &attributes) + err := Unmarshal(user, &attributes) if err != nil { return scim.Resource{}, err } @@ -80,15 +80,15 @@ func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { attributes := scim.ResourceAttributes{} - err := Unmarshal(user, &attributes) + err := Unmarshal(user, &attributes) if err != nil { return nil, err } delete(attributes, "password") - props, err := structpb.NewStruct(attributes) + props, err := structpb.NewStruct(attributes) if err != nil { return nil, err } @@ -121,8 +121,8 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { } attributes := scim.ResourceAttributes{} - err := Unmarshal(group, &attributes) + err := Unmarshal(group, &attributes) if err != nil { return nil, err } @@ -185,8 +185,8 @@ func ProtobufStructToMap(s *structpb.Struct) (map[string]any, error) { } m := make(map[string]any) - err = json.Unmarshal(b, &m) + err = json.Unmarshal(b, &m) if err != nil { return nil, err } diff --git a/common/directory/client.go b/common/directory/client.go index 09711b9..32427f9 100644 --- a/common/directory/client.go +++ b/common/directory/client.go @@ -45,7 +45,6 @@ func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform logger.Trace().Msg("set user") idRelation, err := s.cfg.GetIdentityRelation(userID, "") - if err != nil { return scim.Meta{}, err } @@ -76,10 +75,10 @@ func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform for _, relation := range data.GetRelations() { logger.Trace().Any("relation", relation).Msg("setting relation") + _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ Relation: relation, }) - if err != nil { return result, err } @@ -90,12 +89,12 @@ func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform for _, rel := range relations.GetResults() { if !slices.Contains(addedIdentities, rel.GetObjectId()) { logger.Trace().Str("identity", rel.GetObjectId()).Msg("deleting identity") + _, err := s.client.Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectType: s.cfg.User.IdentityObjectType, ObjectId: rel.GetObjectId(), WithRelations: true, }) - if err != nil { mErr = multierror.Append(mErr, err) logger.Error().Err(err).Str("identity", rel.GetObjectId()).Msg("failed to delete identity") @@ -126,7 +125,6 @@ func (s *Client) importObjects(ctx context.Context, objects []*dsc.Object, userA } object.Properties, err = structpb.NewStruct(userProperties) - if err != nil { return result, addedIdentities, err } @@ -169,7 +167,6 @@ func (s *Client) DeleteUser(ctx context.Context, userID string) error { logger.Trace().Msg("delete user") identityRelation, err := s.cfg.GetIdentityRelation(userID, "") - if err != nil { return err } @@ -208,7 +205,6 @@ func (s *Client) DeleteUser(ctx context.Context, userID string) error { ObjectType: s.cfg.User.IdentityObjectType, WithRelations: true, }) - if err != nil { return err } @@ -234,6 +230,29 @@ func (s *Client) SetGroup(ctx context.Context, groupID string, data *msg.Transfo return scim.Meta{}, nil } + existingRelations, err := s.getGroupRelations(ctx, groupID) + if err != nil { + return scim.Meta{}, err + } + + result, err := s.processGroupObjects(ctx, data.GetObjects(), logger) + if err != nil { + return result, err + } + + addedMembers, err := s.processGroupRelations(ctx, data.GetRelations(), logger) + if err != nil { + return result, err + } + + if err := s.removeStaleRelations(ctx, existingRelations, addedMembers, groupID, logger); err != nil { + return result, err + } + + return result, nil +} + +func (s *Client) getGroupRelations(ctx context.Context, groupID string) (*dsr.GetRelationsResponse, error) { relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ ObjectType: s.cfg.Group.ObjectType, ObjectId: groupID, @@ -243,21 +262,23 @@ func (s *Client) SetGroup(ctx context.Context, groupID string, data *msg.Transfo }) if err != nil { st, ok := status.FromError(err) - if ok && st.Code() != codes.NotFound { - return scim.Meta{}, err + return nil, err } } - addedMembers := make([]string, 0) - result := scim.Meta{} + return relations, nil +} - for _, object := range data.GetObjects() { +func (s *Client) processGroupObjects(ctx context.Context, objects []*dsc.Object, logger zerolog.Logger) (scim.Meta, error) { + var result scim.Meta + + for _, object := range objects { logger.Trace().Any("object", object).Msg("setting object") + resp, err := s.client.Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: object, }) - if err != nil { if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrAlreadyExists) { return result, serrors.ScimErrorUniqueness @@ -267,54 +288,81 @@ func (s *Client) SetGroup(ctx context.Context, groupID string, data *msg.Transfo } if object.GetType() == s.cfg.Group.ObjectType { - err = s.setRelations(ctx, resp.GetResult().GetId(), resp.GetResult().GetType()) - if err != nil { + if err := s.setRelations(ctx, resp.GetResult().GetId(), resp.GetResult().GetType()); err != nil { return result, err } - createdAt := resp.GetResult().GetCreatedAt().AsTime() - updatedAt := resp.GetResult().GetUpdatedAt().AsTime() - result.Created = &createdAt - result.LastModified = &updatedAt - result.Version = resp.GetResult().GetEtag() + result = s.updateMetaFromResponse(resp.GetResult()) } } - for _, relation := range data.GetRelations() { + return result, nil +} + +func (s *Client) processGroupRelations(ctx context.Context, relations []*dsc.Relation, logger zerolog.Logger) ([]string, error) { + addedMembers := make([]string, 0) + + for _, relation := range relations { if relation.GetRelation() == s.cfg.Group.GroupMemberRelation { addedMembers = append(addedMembers, relation.GetSubjectId()) } logger.Trace().Any("relation", relation).Msg("setting relation") - _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ - Relation: relation, - }) - if err != nil { - return result, err + if _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ + Relation: relation, + }); err != nil { + return nil, err } } - if relations != nil { - for _, rel := range relations.GetResults() { - if !slices.Contains(addedMembers, rel.GetSubjectId()) { - logger.Trace().Str("id", rel.GetSubjectId()).Msg("deleting relation") - _, err := s.client.Writer.DeleteRelation(ctx, &dsw.DeleteRelationRequest{ - ObjectType: s.cfg.Group.ObjectType, - ObjectId: groupID, - Relation: s.cfg.Group.GroupMemberRelation, - SubjectId: rel.GetSubjectId(), - SubjectType: rel.GetSubjectType(), - }) - - if err != nil { - return result, err - } + return addedMembers, nil +} + +func (s *Client) removeStaleRelations(ctx context.Context, + relations *dsr.GetRelationsResponse, + addedMembers []string, + groupID string, + logger zerolog.Logger, +) error { + if relations == nil { + return nil + } + + for _, rel := range relations.GetResults() { + if !slices.Contains(addedMembers, rel.GetSubjectId()) { + logger.Trace().Str("id", rel.GetSubjectId()).Msg("deleting relation") + + if err := s.deleteGroupRelation(ctx, groupID, rel); err != nil { + return err } } } - return result, nil + return nil +} + +func (s *Client) deleteGroupRelation(ctx context.Context, groupID string, rel *dsc.Relation) error { + _, err := s.client.Writer.DeleteRelation(ctx, &dsw.DeleteRelationRequest{ + ObjectType: s.cfg.Group.ObjectType, + ObjectId: groupID, + Relation: s.cfg.Group.GroupMemberRelation, + SubjectId: rel.GetSubjectId(), + SubjectType: rel.GetSubjectType(), + }) + + return err +} + +func (s *Client) updateMetaFromResponse(result *dsc.Object) scim.Meta { + createdAt := result.GetCreatedAt().AsTime() + updatedAt := result.GetUpdatedAt().AsTime() + + return scim.Meta{ + Created: &createdAt, + LastModified: &updatedAt, + Version: result.GetEtag(), + } } func (s *Client) DeleteGroup(ctx context.Context, groupID string) error { @@ -326,7 +374,6 @@ func (s *Client) DeleteGroup(ctx context.Context, groupID string) error { ObjectId: groupID, WithRelations: true, }) - if err != nil { return err } diff --git a/common/handlers/groups/create.go b/common/handlers/groups/create.go index af42f20..a3bea2b 100644 --- a/common/handlers/groups/create.go +++ b/common/handlers/groups/create.go @@ -21,8 +21,8 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour logger.Trace().Any("attributes", attributes).Msg("creating group") group := &model.Group{} - err := convert.Unmarshal(attributes, group) + err := convert.Unmarshal(attributes, group) if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to group") return scim.Resource{}, serrors.ScimErrorInvalidSyntax @@ -31,8 +31,8 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour var result scim.Resource converter := convert.NewConverter(g.cfg) - object, err := converter.SCIMGroupToObject(group) + object, err := converter.SCIMGroupToObject(group) if err != nil { logger.Error().Err(err).Msg("failed to convert group to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax diff --git a/common/handlers/groups/get.go b/common/handlers/groups/get.go index bfe5503..f81f6a5 100644 --- a/common/handlers/groups/get.go +++ b/common/handlers/groups/get.go @@ -46,9 +46,7 @@ func (g GroupResourceHandler) GetAll(ctx context.Context, params scim.ListReques logger := g.logger.With().Str("method", "GetAll").Logger() logger.Info().Msg("getting all groups") - var ( - resources = make([]scim.Resource, 0) - ) + resources := make([]scim.Resource, 0) if !g.cfg.HasGroups() { logger.Error().Msg("groups not enabled") diff --git a/common/handlers/groups/patch.go b/common/handlers/groups/patch.go index d17ec15..9c58884 100644 --- a/common/handlers/groups/patch.go +++ b/common/handlers/groups/patch.go @@ -3,12 +3,14 @@ package groups import ( "context" + dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" "github.com/aserto-dev/scim/common" "github.com/aserto-dev/scim/common/convert" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" + "github.com/rs/zerolog" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" structpb "google.golang.org/protobuf/types/known/structpb" @@ -65,39 +67,50 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ } } + resource, err := g.updateGroup(ctx, attr, getObjResp.GetResult(), converter, logger) + if err != nil { + return scim.Resource{}, err + } + + logger.Trace().Any("response", resource).Msg("group patched") + + return resource, nil +} + +func (g GroupResourceHandler) updateGroup( + ctx context.Context, + attr map[string]interface{}, + groupObj *dsc.Object, + converter *convert.Converter, + logger zerolog.Logger, +) (scim.Resource, error) { transformResult, err := converter.TransformResource(attr, "group") if err != nil { logger.Error().Err(err).Msg("failed to convert group to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } - groupObj := getObjResp.GetResult() props, err := structpb.NewStruct(attr) - if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to struct") return scim.Resource{}, err } groupObj.Properties = props + sourceGroupResp, err := g.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: groupObj, }) - if err != nil { logger.Error().Err(err).Msg("failed to replace group") return scim.Resource{}, err } - meta, err := g.dirClient.SetGroup(ctx, getObjResp.GetResult().GetId(), transformResult) + meta, err := g.dirClient.SetGroup(ctx, groupObj.GetId(), transformResult) if err != nil { logger.Error().Err(err).Msg("failed to sync group") return scim.Resource{}, err } - resource := converter.ObjectToResource(sourceGroupResp.GetResult(), meta) - - logger.Trace().Any("response", resource).Msg("group patched") - - return resource, nil + return converter.ObjectToResource(sourceGroupResp.GetResult(), meta), nil } diff --git a/common/handlers/users/create.go b/common/handlers/users/create.go index 67c3755..7c092fb 100644 --- a/common/handlers/users/create.go +++ b/common/handlers/users/create.go @@ -21,8 +21,8 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour logger.Trace().Any("attributes", attributes).Msg("creating user") user := &model.User{} - err := convert.Unmarshal(attributes, user) + err := convert.Unmarshal(attributes, user) if err != nil { logger.Error().Err(err).Msg("failed to convert attributes to user") return scim.Resource{}, serrors.ScimErrorInvalidSyntax @@ -31,8 +31,8 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour var result scim.Resource converter := convert.NewConverter(u.cfg) - object, err := converter.SCIMUserToObject(user) + object, err := converter.SCIMUserToObject(user) if err != nil { logger.Error().Err(err).Msg("failed to convert user to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax @@ -41,7 +41,6 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour sourceUserResp, err := u.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: object, }) - if err != nil { logger.Error().Err(err).Msg("failed to create user") return scim.Resource{}, err diff --git a/common/handlers/users/delete.go b/common/handlers/users/delete.go index e1ea2f6..60b8567 100644 --- a/common/handlers/users/delete.go +++ b/common/handlers/users/delete.go @@ -6,6 +6,7 @@ import ( dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" serrors "github.com/elimity-com/scim/errors" + "github.com/rs/zerolog" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -57,7 +58,6 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { ObjectType: u.cfg.User.IdentityObjectType, WithRelations: true, }) - if err != nil { logger.Error().Err(err).Msg("failed to delete identity") return err @@ -66,7 +66,17 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { logger.Trace().Msg("deleting user") - _, err = u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + if err := u.deleteUserObjects(ctx, id, logger); err != nil { + return err + } + + logger.Trace().Msg("user deleted") + + return nil +} + +func (u UsersResourceHandler) deleteUserObjects(ctx context.Context, id string, logger zerolog.Logger) error { + _, err := u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ ObjectType: u.cfg.User.ObjectType, ObjectId: id, WithRelations: true, @@ -78,6 +88,8 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(id) } + + return err } logger.Trace().Msg("deleting user source object") @@ -94,9 +106,9 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { if ok && st.Code() == codes.NotFound { return serrors.ScimErrorResourceNotFound(id) } - } - logger.Trace().Msg("user deleted") + return err + } - return err + return nil } diff --git a/common/handlers/users/patch.go b/common/handlers/users/patch.go index 1b601f2..357fc1d 100644 --- a/common/handlers/users/patch.go +++ b/common/handlers/users/patch.go @@ -3,12 +3,14 @@ package users import ( "context" + dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" "github.com/aserto-dev/scim/common" "github.com/aserto-dev/scim/common/convert" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" + "github.com/rs/zerolog" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" @@ -62,44 +64,50 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ } } + resource, err := u.updateUser(ctx, attr, getObjResp.GetResult(), converter, logger) if err != nil { - logger.Error().Err(err).Msg("error handling patch operation") return scim.Resource{}, err } + logger.Trace().Any("response", resource).Msg("user patched") + + return resource, nil +} + +func (u UsersResourceHandler) updateUser( + ctx context.Context, + attr map[string]interface{}, + userObj *dsc.Object, + converter *convert.Converter, + logger zerolog.Logger, +) (scim.Resource, error) { transformResult, err := converter.TransformResource(attr, "user") if err != nil { logger.Error().Err(err).Msg("failed to convert user to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } - userObj := getObjResp.GetResult() props, err := structpb.NewStruct(attr) - if err != nil { logger.Error().Err(err).Msg("failed to convert resource attributes to struct") return scim.Resource{}, err } userObj.Properties = props + sourceUserResp, err := u.dirClient.DS().Writer.SetObject(ctx, &dsw.SetObjectRequest{ Object: userObj, }) - if err != nil { logger.Error().Err(err).Msg("failed to replace user") return scim.Resource{}, err } - meta, err := u.dirClient.SetUser(ctx, getObjResp.GetResult().GetId(), transformResult, attr) + meta, err := u.dirClient.SetUser(ctx, userObj.GetId(), transformResult, attr) if err != nil { logger.Error().Err(err).Msg("failed to sync user") return scim.Resource{}, err } - resource := converter.ObjectToResource(sourceUserResp.GetResult(), meta) - - logger.Trace().Any("response", resource).Msg("user patched") - - return resource, nil + return converter.ObjectToResource(sourceUserResp.GetResult(), meta), nil } diff --git a/common/patch.go b/common/patch.go index 630b53b..808534d 100644 --- a/common/patch.go +++ b/common/patch.go @@ -7,13 +7,24 @@ import ( ) func HandlePatchOPAdd(objectProps scim.ResourceAttributes, op scim.PatchOperation) (scim.ResourceAttributes, error) { - var err error - if op.Path == nil || op.Path.ValueExpression == nil { return AddProperty(objectProps, op) } - valueExpression, ok := op.Path.ValueExpression.(*filter.AttributeExpression) + fltr, err := parseValueExpression(op.Path.ValueExpression) + if err != nil { + return nil, err + } + + if op.Path.ValueExpression == nil { + return handleSimpleAdd(objectProps, op) + } + + return handleComplexAdd(objectProps, op, fltr) +} + +func parseValueExpression(expr any) (*filter.AttributeExpression, error) { + valueExpression, ok := expr.(*filter.AttributeExpression) if !ok { return nil, serrors.ScimErrorInvalidPath } @@ -23,56 +34,106 @@ func HandlePatchOPAdd(objectProps scim.ResourceAttributes, op scim.PatchOperatio return nil, err } + return &fltr, nil +} + +func handleSimpleAdd(objectProps scim.ResourceAttributes, op scim.PatchOperation) (scim.ResourceAttributes, error) { properties := make(map[string]any) - if op.Path.ValueExpression == nil { - properties[*op.Path.SubAttribute] = op.Value + properties[*op.Path.SubAttribute] = op.Value - return objectProps, nil - } + return objectProps, nil +} - if objectProps[op.Path.AttributePath.AttributeName] != nil { - attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) - if !ok { - return nil, serrors.ScimErrorInvalidPath - } +func handleComplexAdd(objectProps scim.ResourceAttributes, + op scim.PatchOperation, + fltr *filter.AttributeExpression, +) (scim.ResourceAttributes, error) { + attrName := op.Path.AttributePath.AttributeName + properties := make(map[string]any) - for _, v := range attrProps { - originalValue, ok := v.(map[string]any) - if !ok { - return nil, serrors.ScimErrorInvalidPath - } + if objectProps[attrName] == nil { + objectProps[attrName] = make([]any, 0) + } - switch fltr.Operator { - case filter.EQ: - value, ok := originalValue[fltr.AttributePath.AttributeName].(string) - if ok && value == fltr.CompareValue { - if originalValue[*op.Path.SubAttribute] != nil { - return nil, serrors.ScimErrorUniqueness - } + if objectProps[attrName] != nil { + var err error - properties = originalValue - } - case filter.PR, filter.NE, filter.CO, filter.SW, filter.EW, filter.GT, filter.GE, filter.LT, filter.LE: - return nil, serrors.ScimErrorBadRequest("operand not supported") - } + properties, err = processExistingAttributes(objectProps[attrName], op, fltr) + if err != nil { + return nil, err } - } else { - objectProps[op.Path.AttributePath.AttributeName] = make([]any, 0) } if len(properties) == 0 { - properties[fltr.AttributePath.AttributeName] = fltr.CompareValue - properties[*op.Path.SubAttribute] = op.Value - attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) + return appendNewProperty(objectProps, op, fltr) + } + return objectProps, nil +} + +func processExistingAttributes(attr any, op scim.PatchOperation, fltr *filter.AttributeExpression) (map[string]any, error) { + attrProps, ok := attr.([]any) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + + for _, v := range attrProps { + originalValue, ok := v.(map[string]any) if !ok { return nil, serrors.ScimErrorInvalidPath } - objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, properties) + if result, err := processAttribute(originalValue, op, fltr); err != nil { + return nil, err + } else if result != nil { + return result, nil + } } - return objectProps, err + return make(map[string]any), nil +} + +func processAttribute(value map[string]any, op scim.PatchOperation, fltr *filter.AttributeExpression) (map[string]any, error) { + switch fltr.Operator { + case filter.EQ: + return processEqualityOperator(value, op, fltr) + case filter.PR, filter.NE, filter.CO, filter.SW, filter.EW, filter.GT, filter.GE, filter.LT, filter.LE: + return nil, serrors.ScimErrorBadRequest("operand not supported") + default: + return nil, nil + } +} + +func processEqualityOperator(value map[string]any, op scim.PatchOperation, fltr *filter.AttributeExpression) (map[string]any, error) { + attrValue, ok := value[fltr.AttributePath.AttributeName].(string) + if !ok || attrValue != fltr.CompareValue { + return nil, nil + } + + if value[*op.Path.SubAttribute] != nil { + return nil, serrors.ScimErrorUniqueness + } + + return value, nil +} + +func appendNewProperty(objectProps scim.ResourceAttributes, + op scim.PatchOperation, + fltr *filter.AttributeExpression, +) (scim.ResourceAttributes, error) { + properties := map[string]any{ + fltr.AttributePath.AttributeName: fltr.CompareValue, + *op.Path.SubAttribute: op.Value, + } + + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, properties) + + return objectProps, nil } func HandlePatchOPRemove(objectProps scim.ResourceAttributes, op scim.PatchOperation) (scim.ResourceAttributes, error) { @@ -88,7 +149,6 @@ func HandlePatchOPRemove(objectProps scim.ResourceAttributes, op scim.PatchOpera } ftr, err := filter.ParseAttrExp([]byte(attrExpr.String())) - if err != nil { return nil, err } @@ -172,7 +232,6 @@ func ReplaceInInterfaceArray(value []any, op scim.PatchOperation) ([]any, error) } ftr, err := filter.ParseAttrExp([]byte(attrExpr.String())) - if err != nil { return nil, err } diff --git a/pkg/app/groups.go b/pkg/app/groups.go deleted file mode 100644 index eb3e5ac..0000000 --- a/pkg/app/groups.go +++ /dev/null @@ -1,42 +0,0 @@ -package app - -import ( - "net/http" - - "github.com/aserto-dev/scim/common/handlers/groups" - "github.com/elimity-com/scim" -) - -type GroupResourceHandler struct { - handler *groups.GroupResourceHandler -} - -func NewGroupResourceHandler(handler *groups.GroupResourceHandler) (*GroupResourceHandler, error) { - return &GroupResourceHandler{ - handler: handler, - }, nil -} - -func (g GroupResourceHandler) Create(r *http.Request, attributes scim.ResourceAttributes) (scim.Resource, error) { - return g.handler.Create(r.Context(), attributes) -} - -func (g GroupResourceHandler) Delete(r *http.Request, id string) error { - return g.handler.Delete(r.Context(), id) -} - -func (g GroupResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) { - return g.handler.Get(r.Context(), id) -} - -func (g GroupResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { - return g.handler.GetAll(r.Context(), params) -} - -func (g GroupResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) { - return g.handler.Patch(r.Context(), id, operations) -} - -func (g GroupResourceHandler) Replace(r *http.Request, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { - return g.handler.Replace(r.Context(), id, attributes) -} diff --git a/pkg/app/handler.go b/pkg/app/handler.go new file mode 100644 index 0000000..a6e1c28 --- /dev/null +++ b/pkg/app/handler.go @@ -0,0 +1,42 @@ +package app + +import ( + "net/http" + + "github.com/aserto-dev/scim/common/handlers" + "github.com/elimity-com/scim" +) + +type ResourceHandler struct { + handler handlers.ResourceHandler +} + +func NewResourceHandler(handler handlers.ResourceHandler) (scim.ResourceHandler, error) { + return &ResourceHandler{ + handler: handler, + }, nil +} + +func (g ResourceHandler) Create(r *http.Request, attributes scim.ResourceAttributes) (scim.Resource, error) { + return g.handler.Create(r.Context(), attributes) +} + +func (g ResourceHandler) Delete(r *http.Request, id string) error { + return g.handler.Delete(r.Context(), id) +} + +func (g ResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) { + return g.handler.Get(r.Context(), id) +} + +func (g ResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { + return g.handler.GetAll(r.Context(), params) +} + +func (g ResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) { + return g.handler.Patch(r.Context(), id, operations) +} + +func (g ResourceHandler) Replace(r *http.Request, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { + return g.handler.Replace(r.Context(), id, attributes) +} diff --git a/pkg/app/run.go b/pkg/app/run.go index 089e0b5..cd7f70f 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -53,42 +53,11 @@ func (s *SCIMServer) Run() error { s.dsClient = dsClient - transformCfg, err := convert.NewTransformConfig(&s.cfg.SCIM) - if err != nil { - return err - } - - userHandler, err := userHandler(s.log, transformCfg, dsClient) - if err != nil { - return err - } - - userType := scim.ResourceType{ - ID: optional.NewString("User"), - Name: "User", - Endpoint: "/Users", - Description: optional.NewString("User Account"), - Schema: schema.CoreUserSchema(), - SchemaExtensions: []scim.SchemaExtension{ - {Schema: schema.ExtensionEnterpriseUser()}, - }, - Handler: userHandler, - } - - groupHandler, err := groupHandler(s.log, transformCfg, dsClient) + resourceTypes, err := s.resourceTypes() if err != nil { return err } - groupType := scim.ResourceType{ - ID: optional.NewString("Group"), - Name: "Group", - Endpoint: "/Groups", - Description: optional.NewString("Group"), - Schema: schema.CoreGroupSchema(), - Handler: groupHandler, - } - serverArgs := &scim.ServerArgs{ ServiceProviderConfig: &scim.ServiceProviderConfig{ DocumentationURI: optional.NewString("https://aserto.com/docs/scim"), @@ -100,12 +69,10 @@ func (s *SCIMServer) Run() error { Name: "HTTP Basic", Description: "Authentication scheme using the HTTP Basic Standard", SpecURI: optional.NewString("https://tools.ietf.org/html/rfc7617"), - }}, - }, - ResourceTypes: []scim.ResourceType{ - userType, - groupType, + }, + }, }, + ResourceTypes: resourceTypes, } server, err := scim.NewServer(serverArgs) @@ -165,26 +132,66 @@ func (s *SCIMServer) Shutdown(ctx context.Context) error { return nil } -func userHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsClient *ds.Client) (scim.ResourceHandler, error) { - usersLogger := scimLogger.With().Str("component", "users").Logger() - usersResourceHandler, err := users.NewUsersResourceHandler(&usersLogger, cfg, dsClient) +func (s *SCIMServer) userHandler(cfg *convert.TransformConfig) (scim.ResourceHandler, error) { + usersLogger := s.log.With().Str("component", "users").Logger() + usersResourceHandler, err := users.NewUsersResourceHandler(&usersLogger, cfg, s.dsClient) if err != nil { return nil, err } - return NewUsersResourceHandler(usersResourceHandler) + return NewResourceHandler(usersResourceHandler) } -func groupHandler(scimLogger *zerolog.Logger, cfg *convert.TransformConfig, dsClient *ds.Client) (scim.ResourceHandler, error) { - groupsLogger := scimLogger.With().Str("component", "groups").Logger() - groupsResourceHandler, err := groups.NewGroupResourceHandler(&groupsLogger, cfg, dsClient) +func (s *SCIMServer) groupHandler(cfg *convert.TransformConfig) (scim.ResourceHandler, error) { + groupsLogger := s.log.With().Str("component", "groups").Logger() + groupsResourceHandler, err := groups.NewGroupResourceHandler(&groupsLogger, cfg, s.dsClient) if err != nil { return nil, err } - return NewGroupResourceHandler(groupsResourceHandler) + return NewResourceHandler(groupsResourceHandler) +} + +func (s *SCIMServer) resourceTypes() ([]scim.ResourceType, error) { + transformCfg, err := convert.NewTransformConfig(&s.cfg.SCIM) + if err != nil { + return nil, err + } + + userHandler, err := s.userHandler(transformCfg) + if err != nil { + return nil, err + } + + userType := scim.ResourceType{ + ID: optional.NewString("User"), + Name: "User", + Endpoint: "/Users", + Description: optional.NewString("User Account"), + Schema: schema.CoreUserSchema(), + SchemaExtensions: []scim.SchemaExtension{ + {Schema: schema.ExtensionEnterpriseUser()}, + }, + Handler: userHandler, + } + + groupHandler, err := s.groupHandler(transformCfg) + if err != nil { + return nil, err + } + + groupType := scim.ResourceType{ + ID: optional.NewString("Group"), + Name: "Group", + Endpoint: "/Groups", + Description: optional.NewString("Group"), + Schema: schema.CoreGroupSchema(), + Handler: groupHandler, + } + + return []scim.ResourceType{userType, groupType}, nil } const authzHeaderParts = 2 diff --git a/pkg/app/users.go b/pkg/app/users.go deleted file mode 100644 index 913753a..0000000 --- a/pkg/app/users.go +++ /dev/null @@ -1,42 +0,0 @@ -package app - -import ( - "net/http" - - "github.com/aserto-dev/scim/common/handlers/users" - "github.com/elimity-com/scim" -) - -type UsersResourceHandler struct { - handler *users.UsersResourceHandler -} - -func NewUsersResourceHandler(handler *users.UsersResourceHandler) (*UsersResourceHandler, error) { - return &UsersResourceHandler{ - handler: handler, - }, nil -} - -func (u UsersResourceHandler) Create(r *http.Request, attributes scim.ResourceAttributes) (scim.Resource, error) { - return u.handler.Create(r.Context(), attributes) -} - -func (u UsersResourceHandler) Delete(r *http.Request, id string) error { - return u.handler.Delete(r.Context(), id) -} - -func (u UsersResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) { - return u.handler.Get(r.Context(), id) -} - -func (u UsersResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { - return u.handler.GetAll(r.Context(), params) -} - -func (u UsersResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) { - return u.handler.Patch(r.Context(), id, operations) -} - -func (u UsersResourceHandler) Replace(r *http.Request, id string, attributes scim.ResourceAttributes) (scim.Resource, error) { - return u.handler.Replace(r.Context(), id, attributes) -} diff --git a/pkg/test/assets/config/topaz.yaml b/pkg/test/assets/config/topaz.yaml index 040cf7a..c177caa 100644 --- a/pkg/test/assets/config/topaz.yaml +++ b/pkg/test/assets/config/topaz.yaml @@ -58,40 +58,6 @@ api: listen_address: "0.0.0.0:9696" services: - console: - grpc: - listen_address: "0.0.0.0:9292" - fqdn: "" - gateway: - listen_address: "0.0.0.0:9393" - fqdn: "" - allowed_headers: - - "Authorization" - - "Content-Type" - - "If-Match" - - "If-None-Match" - - "Depth" - allowed_methods: - - "GET" - - "POST" - - "HEAD" - - "DELETE" - - "PUT" - - "PATCH" - - "PROFIND" - - "MKCOL" - - "COPY" - - "MOVE" - allowed_origins: - - http://localhost - - http://localhost:* - - https://*.aserto.com - - https://*aserto-console.netlify.app - read_timeout: 2s - read_header_timeout: 2s - write_timeout: 2s - idle_timeout: 30s - model: grpc: listen_address: "0.0.0.0:9292" @@ -210,63 +176,3 @@ api: grpc: listen_address: "0.0.0.0:9292" fqdn: "" - - authorizer: - needs: - - reader - grpc: - connection_timeout_seconds: 2 - listen_address: "0.0.0.0:9292" - fqdn: "" - gateway: - listen_address: "0.0.0.0:9393" - fqdn: "" - allowed_headers: - - "Authorization" - - "Content-Type" - - "If-Match" - - "If-None-Match" - - "Depth" - allowed_methods: - - "GET" - - "POST" - - "HEAD" - - "DELETE" - - "PUT" - - "PATCH" - - "PROFIND" - - "MKCOL" - - "COPY" - - "MOVE" - allowed_origins: - - http://localhost - - http://localhost:* - - https://*.aserto.com - - https://*aserto-console.netlify.app - read_timeout: 2s - read_header_timeout: 2s - write_timeout: 2s - idle_timeout: 30s - -opa: - instance_id: "-" - graceful_shutdown_period_seconds: 2 - # max_plugin_wait_time_seconds: 30 set as default - local_bundles: - paths: [] - skip_verification: true - config: - services: - ghcr: - url: https://ghcr.io - type: "oci" - response_header_timeout_seconds: 5 - bundles: - test: - service: ghcr - resource: "ghcr.io/aserto-policies/policy-rebac:latest" - persist: false - config: - polling: - min_delay_seconds: 60 - max_delay_seconds: 120 diff --git a/pkg/test/common/common.go b/pkg/test/common/common.go index 4b86fae..fac96a5 100644 --- a/pkg/test/common/common.go +++ b/pkg/test/common/common.go @@ -22,8 +22,10 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -const containerStartTimeout = 300 * time.Second -const topazConfigFileMode = 0o700 +const ( + containerStartTimeout = 300 * time.Second + topazConfigFileMode = 0o700 +) type TestCase struct { Topaz testcontainers.Container From 670f19fadd2bf86e40284e7cea1f909af5aaeabf Mon Sep 17 00:00:00 2001 From: florindragos Date: Thu, 17 Apr 2025 16:04:33 +0300 Subject: [PATCH 12/13] update mapstructure --- go.mod | 8 ++------ go.sum | 16 ++++------------ pkg/config/config.go | 2 +- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 95633a3..379907a 100644 --- a/go.mod +++ b/go.mod @@ -12,11 +12,11 @@ require ( github.com/docker/go-connections v0.5.0 github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 github.com/gavv/httpexpect/v2 v2.17.0 - github.com/mitchellh/mapstructure v1.5.0 + github.com/go-viper/mapstructure/v2 v2.2.1 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.19.0 + github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.36.0 ) @@ -63,7 +63,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/hpcloud/tail v1.0.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect @@ -90,7 +89,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/samber/lo v1.49.1 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/scim2/filter-parser/v2 v2.2.0 // indirect @@ -123,7 +121,6 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect @@ -133,7 +130,6 @@ require ( google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect moul.io/http2curl/v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index bc856e0..3e31f76 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -112,8 +114,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -148,8 +148,6 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -195,8 +193,6 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= @@ -221,8 +217,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= @@ -293,8 +289,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -361,8 +355,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/config/config.go b/pkg/config/config.go index 9fe3335..9ac2506 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -8,7 +8,7 @@ import ( client "github.com/aserto-dev/go-aserto" "github.com/aserto-dev/logger" config "github.com/aserto-dev/scim/common/config" - "github.com/mitchellh/mapstructure" + "github.com/go-viper/mapstructure/v2" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/spf13/viper" From 30d8946bc49bfc012c1d87d0321b4a1d6939a564 Mon Sep 17 00:00:00 2001 From: florindragos Date: Fri, 25 Apr 2025 17:47:58 +0300 Subject: [PATCH 13/13] address comments --- cmd/aserto-scim/main.go | 21 +++-- common/assets/users-groups-roles.tmpl | 118 +++++++++++++------------- common/config/config.go | 12 +++ common/convert/config.go | 22 ++--- common/convert/convert.go | 42 ++------- common/directory/client.go | 6 +- common/handlers/groups/create.go | 13 ++- common/handlers/groups/delete.go | 2 +- common/handlers/groups/get.go | 4 +- common/handlers/groups/patch.go | 18 ++-- common/handlers/groups/replace.go | 4 +- common/handlers/users/create.go | 61 +++++++++---- common/handlers/users/delete.go | 103 +++++++++++++++------- common/handlers/users/get.go | 4 +- common/handlers/users/patch.go | 53 ++++++------ common/handlers/users/replace.go | 4 +- common/patch.go | 80 +++++++++-------- go.mod | 2 + go.sum | 18 +++- pkg/app/run.go | 2 +- 20 files changed, 334 insertions(+), 255 deletions(-) diff --git a/cmd/aserto-scim/main.go b/cmd/aserto-scim/main.go index be8d1a3..95f707a 100644 --- a/cmd/aserto-scim/main.go +++ b/cmd/aserto-scim/main.go @@ -5,12 +5,12 @@ import ( "fmt" "log" "os" - "os/signal" - "syscall" "github.com/aserto-dev/scim/pkg/app" "github.com/aserto-dev/scim/pkg/version" "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" + "sigs.k8s.io/controller-runtime/pkg/manager/signals" ) var flagConfigPath string @@ -38,19 +38,18 @@ var cmdRun = &cobra.Command{ return err } - go func() { - if err := srv.Run(); err != nil { - log.Printf("Error running SCIM server: %v", err) - } - }() - - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - <-stop + errGroup, ctx := errgroup.WithContext(signals.SetupSignalHandler()) + errGroup.Go(srv.Run) + <-ctx.Done() if err := srv.Shutdown(context.Background()); err != nil { return err } + + if err := errGroup.Wait(); err != nil { + log.Printf("Error: %v", err) + } + log.Println("SCIM server stopped") return nil }, diff --git a/common/assets/users-groups-roles.tmpl b/common/assets/users-groups-roles.tmpl index ff73f43..c4ab3df 100644 --- a/common/assets/users-groups-roles.tmpl +++ b/common/assets/users-groups-roles.tmpl @@ -1,6 +1,6 @@ { "objects": [ - {{ if eq .objectType "user" }} + {{- if eq .objectType "user" }} { "id": "{{ $.input.userName }}", "type": "{{ $.vars.user.object_type }}", @@ -13,7 +13,7 @@ "verified": true } }, - {{ range $i, $element := $.input.emails }} + {{- range $i, $element := $.input.emails }} {{ if $i }},{{ end }} { "id": "{{ $element.value }}", @@ -23,8 +23,8 @@ "verified": true } } - {{ end }} - {{ if and ($.input.externalId) (ne $.input.externalId "") }} + {{- end }} + {{- if $.input.externalId }} , { "id": "{{ $.input.externalId }}", @@ -33,9 +33,9 @@ "verified": true } } - {{ end }} - {{ if $.input.roles }} - {{ range $i, $element := $.input.roles }} + {{- end }} + {{- if $.input.roles }} + {{- range $i, $element := $.input.roles }} , { "id": "{{ $element.value }}", @@ -46,29 +46,29 @@ "primary": {{ $element.primary }} } } - {{ end }} - {{ end }} - {{ else }} + {{- end }} + {{- end }} + {{- else }} { "id": "{{ $.input.displayName }}", "type": "{{ $.vars.group.object_type }}", "displayName": "{{ $.input.displayName }}" } - {{ end }} + {{- end }} ], "relations":[ - {{ if eq .objectType "user" }} - {{ $idRelationMap := splitn "#" 2 $.vars.user.identity_relation }} - {{ $idObjType := $idRelationMap._0 }} - {{ $idRelation := $idRelationMap._1 }} - {{ $idSubjType := $.vars.user.object_type }} - {{ $objId := $.input.userName }} - {{ $subjId := $.input.userName }} + {{- if eq .objectType "user" }} + {{- $idRelationMap := splitn "#" 2 $.vars.user.identity_relation }} + {{- $idObjType := $idRelationMap._0 }} + {{- $idRelation := $idRelationMap._1 }} + {{- $idSubjType := $.vars.user.object_type }} + {{- $objId := $.input.userName }} + {{- $subjId := $.input.userName }} - {{ if eq $idObjType $.vars.user.object_type }} - {{ $idSubjType = $.vars.user.identity_object_type }} - {{ $subjId = $.input.userName }} - {{ end }} + {{- if eq $idObjType $.vars.user.object_type }} + {{- $idSubjType = $.vars.user.identity_object_type }} + {{- $subjId = $.input.userName }} + {{- end }} { "object_type": "{{ $idObjType }}", "object_id": "{{ $objId }}", @@ -76,15 +76,15 @@ "subject_type": "{{ $idSubjType }}", "subject_id": "{{ $subjId }}" }, - {{ range $i, $element := $.input.emails }} - {{ if $i }},{{ end }} - {{ if eq $idObjType $.vars.user.object_type }} - {{ $subjId = $element.value }} - {{ $objId := $.input.userName }} - {{ else }} - {{ $subjId := $.input.userName }} - {{ $objId = $element.value }} - {{ end }} + {{- range $i, $element := $.input.emails }} + {{- if $i }},{{ end }} + {{- if eq $idObjType $.vars.user.object_type }} + {{- $subjId = $element.value }} + {{- $objId := $.input.userName }} + {{- else }} + {{- $subjId := $.input.userName }} + {{- $objId = $element.value }} + {{- end }} { "object_type": "{{ $idObjType }}", "object_id": "{{ $objId }}", @@ -92,16 +92,16 @@ "subject_type": "{{ $idSubjType }}", "subject_id": "{{ $subjId }}" } - {{ end }} - {{ if and ($.input.externalId) (ne $.input.externalId "") }} + {{- end }} + {{- if $.input.externalId }} , - {{ if eq $idObjType $.vars.user.object_type }} - {{ $objId := $.input.userName }} - {{ $subjId = $.input.externalId }} - {{ else }} - {{ $objId = $.input.externalId }} - {{ $subjId = $.input.userName }} - {{ end }} + {{- if eq $idObjType $.vars.user.object_type }} + {{- $objId := $.input.userName }} + {{- $subjId = $.input.externalId }} + {{- else }} + {{- $objId = $.input.externalId }} + {{- $subjId = $.input.userName }} + {{- end }} { "object_type": "{{ $idObjType }}", "object_id": "{{ $objId }}", @@ -109,11 +109,11 @@ "subject_type": "{{ $idSubjType }}", "subject_id": "{{ $subjId }}" } - {{ end }} - {{ if and ($.vars.user.manager_relation) (ne $.vars.user.manager_relation "") }} - {{ $manager := index .input "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }} - {{ if $manager }} - {{ if and ($manager.manager.value) (ne $manager.manager.value "") }} + {{- end }} + {{- if $.vars.user.manager_relation }} + {{- $manager := index .input "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }} + {{- if $manager }} + {{- if and ($manager.manager.value) (ne $manager.manager.value "") }} , { "object_type": "{{ $.vars.user.object_type }}", @@ -122,11 +122,11 @@ "subject_type": "{{ $.vars.user.object_type }}", "subject_id": "{{ $manager.manager.value }}" } - {{ end }} - {{ end }} - {{ end }} - {{ if $.input.roles }} - {{ range $i, $element := $.input.roles }} + {{- end }} + {{- end }} + {{- end }} + {{- if $.input.roles }} + {{- range $i, $element := $.input.roles }} , { "object_type": "{{ $.vars.role.object_type }}", @@ -135,12 +135,12 @@ "subject_type": "{{ $.vars.user.object_type }}", "subject_id": "{{ $.input.userName }}" } - {{ end }} - {{ end }} - {{ else }} - {{ $members := index .input "members" }} - {{ if $members }} - {{ range $i, $member := $members }} + {{- end }} + {{- end }} + {{- else }} + {{- $members := index .input "members" }} + {{- if $members }} + {{- range $i, $member := $members }} {{ if $i }},{{ end }} { "object_type": "{{ $.vars.group.object_type }}", @@ -149,8 +149,8 @@ "subject_type": "{{ $.vars.user.object_type }}", "subject_id": "{{ $member.value }}" } - {{ end }} - {{ end }} - {{ end }} + {{- end }} + {{- end }} + {{- end }} ] } diff --git a/common/config/config.go b/common/config/config.go index 2d311a3..5311100 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -48,6 +48,10 @@ func (cfg *Config) Validate() error { return errors.Wrap(ErrInvalidConfig, "scim.user_object_type is required") } + if cfg.User.SourceObjectType == "" { + return errors.Wrap(ErrInvalidConfig, "scim.source_object_type is required") + } + if cfg.User.IdentityObjectType == "" { return errors.Wrap(ErrInvalidConfig, "scim.identity_object_type is required") } @@ -75,6 +79,10 @@ func (cfg *Config) Validate() error { return errors.Wrap(ErrInvalidConfig, "scim.group_object_type is required") } + if cfg.Group.SourceObjectType == "" { + return errors.Wrap(ErrInvalidConfig, "scim.source_object_type is required") + } + if cfg.Group.GroupMemberRelation == "" { return errors.Wrap(ErrInvalidConfig, "scim.group_member_relation is required") } @@ -82,3 +90,7 @@ func (cfg *Config) Validate() error { return nil } + +func (c *Config) HasGroups() bool { + return c.Group != nil +} diff --git a/common/convert/config.go b/common/convert/config.go index 0de8f5f..faf489e 100644 --- a/common/convert/config.go +++ b/common/convert/config.go @@ -10,17 +10,17 @@ import ( "github.com/pkg/errors" ) -type TemplateName int +type TemplateKind int const ( - Users TemplateName = iota + Users TemplateKind = iota UsersGroups UsersGroupsRoles ) var ErrInvalidConfig = errors.New("invalid config") -func (t TemplateName) String() string { +func (t TemplateKind) String() string { switch t { case Users: return "users" @@ -35,7 +35,7 @@ func (t TemplateName) String() string { type TransformConfig struct { *config.Config - template TemplateName + template TemplateKind IdentityObjectType string `json:"identity_object_type,omitempty"` IdentityRelation string `json:"identity_relation,omitempty"` } @@ -43,7 +43,7 @@ type TransformConfig struct { func NewTransformConfig(cfg *config.Config) (*TransformConfig, error) { template := Users - if cfg.Group != nil { + if cfg.HasGroups() { template = UsersGroups if cfg.Role != nil { template = UsersGroupsRoles @@ -71,18 +71,14 @@ func NewTransformConfig(cfg *config.Config) (*TransformConfig, error) { }, nil } -func (c *TransformConfig) HasGroups() bool { - return c.Group != nil -} - func (c *TransformConfig) ToTemplateVars() (map[string]any, error) { - var result map[string]any - cfg, err := json.Marshal(c) if err != nil { return nil, errors.Wrap(err, "failed to marshal ScimConfig to json") } + var result map[string]any + if err := json.Unmarshal(cfg, &result); err != nil { return nil, errors.Wrap(err, "failed to unmarshal ScimConfig to map") } @@ -90,11 +86,11 @@ func (c *TransformConfig) ToTemplateVars() (map[string]any, error) { return result, nil } -func (c *TransformConfig) GetTemplate() ([]byte, error) { +func (c *TransformConfig) Template() ([]byte, error) { return common.LoadTemplate(c.template.String()) } -func (c *TransformConfig) GetIdentityRelation(userID, identity string) (*dsc.Relation, error) { +func (c *TransformConfig) ParseIdentityRelation(userID, identity string) (*dsc.Relation, error) { switch c.IdentityObjectType { case c.User.IdentityObjectType: return &dsc.Relation{ diff --git a/common/convert/convert.go b/common/convert/convert.go index 12f96ee..ea08673 100644 --- a/common/convert/convert.go +++ b/common/convert/convert.go @@ -60,8 +60,7 @@ func Unmarshal[S any, D any](source S, dest *D) error { func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { attributes := scim.ResourceAttributes{} - err := Unmarshal(user, &attributes) - if err != nil { + if err := Unmarshal(user, &attributes); err != nil { return scim.Resource{}, err } @@ -81,8 +80,7 @@ func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { attributes := scim.ResourceAttributes{} - err := Unmarshal(user, &attributes) - if err != nil { + if err := Unmarshal(user, &attributes); err != nil { return nil, err } @@ -94,19 +92,10 @@ func (c *Converter) SCIMUserToObject(user *model.User) (*dsc.Object, error) { } userID := lo.Ternary(user.ID != "", user.ID, user.UserName) - - displayName := user.DisplayName - if displayName == "" { - displayName = userID - } - - sourceUserType := c.cfg.User.SourceObjectType - if sourceUserType == "" { - return nil, ErrSourceUserTypeNotSet - } + displayName := lo.Ternary(user.DisplayName != "", user.DisplayName, userID) object := &dsc.Object{ - Type: sourceUserType, + Type: c.cfg.User.SourceObjectType, Properties: props, Id: userID, DisplayName: displayName, @@ -122,8 +111,7 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { attributes := scim.ResourceAttributes{} - err := Unmarshal(group, &attributes) - if err != nil { + if err := Unmarshal(group, &attributes); err != nil { return nil, err } @@ -132,23 +120,11 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { return nil, err } - objID := group.ID - if objID == "" { - objID = group.DisplayName - } - - displayName := group.DisplayName - if displayName == "" { - displayName = objID - } - - sourceGroupType := c.cfg.Group.SourceObjectType - if sourceGroupType == "" { - return nil, ErrSourceGroupTypeNotSet - } + objID := lo.Ternary(group.ID != "", group.ID, group.DisplayName) + displayName := lo.Ternary(group.DisplayName != "", group.DisplayName, objID) object := &dsc.Object{ - Type: sourceGroupType, + Type: c.cfg.Group.SourceObjectType, Properties: props, Id: objID, DisplayName: displayName, @@ -158,7 +134,7 @@ func (c *Converter) SCIMGroupToObject(group *model.Group) (*dsc.Object, error) { } func (c *Converter) TransformResource(resource map[string]any, objType string) (*msg.Transform, error) { - template, err := c.cfg.GetTemplate() + template, err := c.cfg.Template() if err != nil { return nil, err } diff --git a/common/directory/client.go b/common/directory/client.go index 32427f9..205b219 100644 --- a/common/directory/client.go +++ b/common/directory/client.go @@ -44,7 +44,7 @@ func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform logger := s.logger.With().Str("method", "SetUser").Str("id", userID).Logger() logger.Trace().Msg("set user") - idRelation, err := s.cfg.GetIdentityRelation(userID, "") + idRelation, err := s.cfg.ParseIdentityRelation(userID, "") if err != nil { return scim.Meta{}, err } @@ -97,7 +97,7 @@ func (s *Client) SetUser(ctx context.Context, userID string, data *msg.Transform }) if err != nil { mErr = multierror.Append(mErr, err) - logger.Error().Err(err).Str("identity", rel.GetObjectId()).Msg("failed to delete identity") + logger.Err(err).Str("identity", rel.GetObjectId()).Msg("failed to delete identity") } } } @@ -166,7 +166,7 @@ func (s *Client) DeleteUser(ctx context.Context, userID string) error { logger := s.logger.With().Str("method", "DeleteUser").Str("id", userID).Logger() logger.Trace().Msg("delete user") - identityRelation, err := s.cfg.GetIdentityRelation(userID, "") + identityRelation, err := s.cfg.ParseIdentityRelation(userID, "") if err != nil { return err } diff --git a/common/handlers/groups/create.go b/common/handlers/groups/create.go index a3bea2b..11c04a0 100644 --- a/common/handlers/groups/create.go +++ b/common/handlers/groups/create.go @@ -22,9 +22,8 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour group := &model.Group{} - err := convert.Unmarshal(attributes, group) - if err != nil { - logger.Error().Err(err).Msg("failed to convert attributes to group") + if err := convert.Unmarshal(attributes, group); err != nil { + logger.Err(err).Msg("failed to convert attributes to group") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } @@ -34,7 +33,7 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour object, err := converter.SCIMGroupToObject(group) if err != nil { - logger.Error().Err(err).Msg("failed to convert group to object") + logger.Err(err).Msg("failed to convert group to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } @@ -42,19 +41,19 @@ func (g GroupResourceHandler) Create(ctx context.Context, attributes scim.Resour Object: object, }) if err != nil { - logger.Error().Err(err).Msg("failed to create group") + logger.Err(err).Msg("failed to create group") return scim.Resource{}, err } transformResult, err := converter.TransformResource(attributes, "group") if err != nil { - logger.Error().Err(err).Msg("failed to transform group") + logger.Err(err).Msg("failed to transform group") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } meta, err := g.dirClient.SetGroup(ctx, sourceGroupResp.GetResult().GetId(), transformResult) if err != nil { - logger.Error().Err(err).Msg("failed to sync group") + logger.Err(err).Msg("failed to sync group") return scim.Resource{}, err } diff --git a/common/handlers/groups/delete.go b/common/handlers/groups/delete.go index b1219f6..3040d95 100644 --- a/common/handlers/groups/delete.go +++ b/common/handlers/groups/delete.go @@ -8,7 +8,7 @@ func (g GroupResourceHandler) Delete(ctx context.Context, id string) error { err := g.dirClient.DeleteGroup(ctx, id) if err != nil { - logger.Error().Err(err).Msg("failed to delete group") + logger.Err(err).Msg("failed to delete group") return err } diff --git a/common/handlers/groups/get.go b/common/handlers/groups/get.go index f81f6a5..17ddab7 100644 --- a/common/handlers/groups/get.go +++ b/common/handlers/groups/get.go @@ -25,7 +25,7 @@ func (g GroupResourceHandler) Get(ctx context.Context, id string) (scim.Resource WithRelations: false, }) if err != nil { - logger.Error().Err(err).Msg("failed to get group") + logger.Err(err).Msg("failed to get group") return scim.Resource{}, err } @@ -60,7 +60,7 @@ func (g GroupResourceHandler) GetAll(ctx context.Context, params scim.ListReques }, }) if err != nil { - logger.Error().Err(err).Msg("failed to read groups") + logger.Err(err).Msg("failed to read groups") return scim.Page{}, err } diff --git a/common/handlers/groups/patch.go b/common/handlers/groups/patch.go index 9c58884..2916ff1 100644 --- a/common/handlers/groups/patch.go +++ b/common/handlers/groups/patch.go @@ -31,9 +31,9 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ WithRelations: false, }) if err != nil { - logger.Error().Err(err).Msg("failed to get group") - st, ok := status.FromError(err) + logger.Err(err).Msg("failed to get group") + st, ok := status.FromError(err) if ok && st.Code() == codes.NotFound { return scim.Resource{}, serrors.ScimErrorResourceNotFound(id) } @@ -49,19 +49,19 @@ func (g GroupResourceHandler) Patch(ctx context.Context, id string, operations [ case scim.PatchOperationAdd: attr, err = common.HandlePatchOPAdd(attr, op) if err != nil { - logger.Error().Err(err).Msg("error adding property") + logger.Err(err).Msg("error adding property") return scim.Resource{}, err } case scim.PatchOperationRemove: attr, err = common.HandlePatchOPRemove(attr, op) if err != nil { - logger.Error().Err(err).Msg("error removing property") + logger.Err(err).Msg("error removing property") return scim.Resource{}, err } case scim.PatchOperationReplace: attr, err = common.HandlePatchOPReplace(attr, op) if err != nil { - logger.Error().Err(err).Msg("error replacing property") + logger.Err(err).Msg("error replacing property") return scim.Resource{}, err } } @@ -86,13 +86,13 @@ func (g GroupResourceHandler) updateGroup( ) (scim.Resource, error) { transformResult, err := converter.TransformResource(attr, "group") if err != nil { - logger.Error().Err(err).Msg("failed to convert group to object") + logger.Err(err).Msg("failed to convert group to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } props, err := structpb.NewStruct(attr) if err != nil { - logger.Error().Err(err).Msg("failed to convert attributes to struct") + logger.Err(err).Msg("failed to convert attributes to struct") return scim.Resource{}, err } @@ -102,13 +102,13 @@ func (g GroupResourceHandler) updateGroup( Object: groupObj, }) if err != nil { - logger.Error().Err(err).Msg("failed to replace group") + logger.Err(err).Msg("failed to replace group") return scim.Resource{}, err } meta, err := g.dirClient.SetGroup(ctx, groupObj.GetId(), transformResult) if err != nil { - logger.Error().Err(err).Msg("failed to sync group") + logger.Err(err).Msg("failed to sync group") return scim.Resource{}, err } diff --git a/common/handlers/groups/replace.go b/common/handlers/groups/replace.go index 1187973..b0101d8 100644 --- a/common/handlers/groups/replace.go +++ b/common/handlers/groups/replace.go @@ -12,13 +12,13 @@ func (g GroupResourceHandler) Replace(ctx context.Context, id string, attributes err := g.Delete(ctx, id) if err != nil { - logger.Error().Err(err).Msg("failed to delete group") + logger.Err(err).Msg("failed to delete group") return scim.Resource{}, err } resource, err := g.Create(ctx, attributes) if err != nil { - logger.Error().Err(err).Msg("failed to create group") + logger.Err(err).Msg("failed to create group") return scim.Resource{}, err } diff --git a/common/handlers/users/create.go b/common/handlers/users/create.go index 7c092fb..ed7b672 100644 --- a/common/handlers/users/create.go +++ b/common/handlers/users/create.go @@ -8,6 +8,7 @@ import ( "github.com/aserto-dev/scim/common/model" "github.com/elimity-com/scim" serrors "github.com/elimity-com/scim/errors" + "github.com/rs/zerolog" ) func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.ResourceAttributes) (scim.Resource, error) { @@ -20,21 +21,43 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour logger.Info().Msg("create user") logger.Trace().Any("attributes", attributes).Msg("creating user") - user := &model.User{} + user, err := u.convertAttributesToUser(attributes, logger) + if err != nil { + return scim.Resource{}, err + } + + converter := convert.NewConverter(u.cfg) - err := convert.Unmarshal(attributes, user) + result, err := u.createUserObject(ctx, user, attributes, converter, logger) if err != nil { - logger.Error().Err(err).Msg("failed to convert attributes to user") - return scim.Resource{}, serrors.ScimErrorInvalidSyntax + return scim.Resource{}, err } - var result scim.Resource + logger.Trace().Any("response", result).Msg("user created") - converter := convert.NewConverter(u.cfg) + return result, nil +} +func (u UsersResourceHandler) convertAttributesToUser(attributes scim.ResourceAttributes, logger zerolog.Logger) (*model.User, error) { + user := &model.User{} + if err := convert.Unmarshal(attributes, user); err != nil { + logger.Err(err).Msg("failed to convert attributes to user") + return nil, serrors.ScimErrorInvalidSyntax + } + + return user, nil +} + +func (u UsersResourceHandler) createUserObject( + ctx context.Context, + user *model.User, + attributes scim.ResourceAttributes, + converter *convert.Converter, + logger zerolog.Logger, +) (scim.Resource, error) { object, err := converter.SCIMUserToObject(user) if err != nil { - logger.Error().Err(err).Msg("failed to convert user to object") + logger.Err(err).Msg("failed to convert user to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } @@ -42,31 +65,37 @@ func (u UsersResourceHandler) Create(ctx context.Context, attributes scim.Resour Object: object, }) if err != nil { - logger.Error().Err(err).Msg("failed to create user") + logger.Err(err).Msg("failed to create user") return scim.Resource{}, err } + return u.processUserResponse(ctx, sourceUserResp, attributes, converter, logger) +} + +func (u UsersResourceHandler) processUserResponse( + ctx context.Context, + sourceUserResp *dsw.SetObjectResponse, + attributes scim.ResourceAttributes, + converter *convert.Converter, + logger zerolog.Logger, +) (scim.Resource, error) { userMap, err := convert.ProtobufStructToMap(sourceUserResp.GetResult().GetProperties()) if err != nil { - logger.Error().Err(err).Msg("failed to convert user to map") + logger.Err(err).Msg("failed to convert user to map") return scim.Resource{}, err } transformResult, err := converter.TransformResource(userMap, "user") if err != nil { - logger.Error().Err(err).Msg("failed to convert user to object") + logger.Err(err).Msg("failed to convert user to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } meta, err := u.dirClient.SetUser(ctx, sourceUserResp.GetResult().GetId(), transformResult, attributes) if err != nil { - logger.Error().Err(err).Msg("failed to sync user") + logger.Err(err).Msg("failed to sync user") return scim.Resource{}, err } - result = converter.ObjectToResource(sourceUserResp.GetResult(), meta) - - logger.Trace().Any("response", result).Msg("user created") - - return result, nil + return converter.ObjectToResource(sourceUserResp.GetResult(), meta), nil } diff --git a/common/handlers/users/delete.go b/common/handlers/users/delete.go index 60b8567..b65c64e 100644 --- a/common/handlers/users/delete.go +++ b/common/handlers/users/delete.go @@ -3,6 +3,7 @@ package users import ( "context" + dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3" dsr "github.com/aserto-dev/go-directory/aserto/directory/reader/v3" dsw "github.com/aserto-dev/go-directory/aserto/directory/writer/v3" serrors "github.com/elimity-com/scim/errors" @@ -15,11 +16,42 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { logger := u.logger.With().Str("method", "Delete").Str("id", id).Logger() logger.Info().Msg("delete user") - identityRelation, err := u.cfg.GetIdentityRelation(id, "") + if err := u.deleteUserIdentities(ctx, id, logger); err != nil { + return err + } + + logger.Trace().Msg("deleting user") + + if err := u.deleteUserObjects(ctx, id, logger); err != nil { + return err + } + + logger.Trace().Msg("user deleted") + + return nil +} + +func (u UsersResourceHandler) deleteUserIdentities(ctx context.Context, id string, logger zerolog.Logger) error { + identityRelation, err := u.cfg.ParseIdentityRelation(id, "") + if err != nil { + logger.Err(err).Msg("failed to get identity relation") + return err + } + + relations, err := u.getUserIdentityRelations(ctx, identityRelation, id, logger) if err != nil { - logger.Error().Err(err).Msg("failed to get identity relation") + return err } + return u.deleteIdentityObjects(ctx, relations, logger) +} + +func (u UsersResourceHandler) getUserIdentityRelations( + ctx context.Context, + identityRelation *dsc.Relation, + id string, + logger zerolog.Logger, +) (*dsr.GetRelationsResponse, error) { resp, err := u.dirClient.DS().Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ SubjectType: identityRelation.GetSubjectType(), SubjectId: identityRelation.GetSubjectId(), @@ -28,49 +60,62 @@ func (u UsersResourceHandler) Delete(ctx context.Context, id string) error { Relation: identityRelation.GetRelation(), }) if err != nil { - logger.Error().Err(err).Msg("failed to get relations") + logger.Err(err).Msg("failed to get relations") st, ok := status.FromError(err) if ok && st.Code() == codes.NotFound { - return serrors.ScimErrorResourceNotFound(id) + return nil, serrors.ScimErrorResourceNotFound(id) } - return err + return nil, err } + return resp, nil +} + +func (u UsersResourceHandler) deleteIdentityObjects( + ctx context.Context, + resp *dsr.GetRelationsResponse, + logger zerolog.Logger, +) error { for _, v := range resp.GetResults() { - var objectID string - - switch v.GetObjectType() { - case u.cfg.User.IdentityObjectType: - objectID = v.GetObjectId() - case u.cfg.User.ObjectType: - objectID = v.GetSubjectId() - default: - logger.Error().Str("object_type", v.GetObjectType()).Msg("unexpected object type") - return serrors.ScimErrorBadRequest("unexpected object type in identity relation") + objectID, err := u.getIdentityObjectID(v, logger) + if err != nil { + return err } - logger.Trace().Str("id", v.GetObjectId()).Msg("deleting identity") + logger.Trace().Str("id", objectID).Msg("deleting identity") - _, err = u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ - ObjectId: objectID, - ObjectType: u.cfg.User.IdentityObjectType, - WithRelations: true, - }) - if err != nil { - logger.Error().Err(err).Msg("failed to delete identity") + if err := u.deleteIdentityObject(ctx, objectID, logger); err != nil { return err } } - logger.Trace().Msg("deleting user") + return nil +} - if err := u.deleteUserObjects(ctx, id, logger); err != nil { - return err +func (u UsersResourceHandler) getIdentityObjectID(relation *dsc.Relation, logger zerolog.Logger) (string, error) { + switch relation.GetObjectType() { + case u.cfg.User.IdentityObjectType: + return relation.GetObjectId(), nil + case u.cfg.User.ObjectType: + return relation.GetSubjectId(), nil + default: + logger.Error().Str("object_type", relation.GetObjectType()).Msg("unexpected object type") + return "", serrors.ScimErrorBadRequest("unexpected object type in identity relation") } +} - logger.Trace().Msg("user deleted") +func (u UsersResourceHandler) deleteIdentityObject(ctx context.Context, objectID string, logger zerolog.Logger) error { + _, err := u.dirClient.DS().Writer.DeleteObject(ctx, &dsw.DeleteObjectRequest{ + ObjectId: objectID, + ObjectType: u.cfg.User.IdentityObjectType, + WithRelations: true, + }) + if err != nil { + logger.Err(err).Msg("failed to delete identity") + return err + } return nil } @@ -82,7 +127,7 @@ func (u UsersResourceHandler) deleteUserObjects(ctx context.Context, id string, WithRelations: true, }) if err != nil { - logger.Error().Err(err).Msg("failed to delete user") + logger.Err(err).Msg("failed to delete user") st, ok := status.FromError(err) if ok && st.Code() == codes.NotFound { @@ -100,7 +145,7 @@ func (u UsersResourceHandler) deleteUserObjects(ctx context.Context, id string, WithRelations: true, }) if err != nil { - logger.Error().Err(err).Msg("failed to delete user source object") + logger.Err(err).Msg("failed to delete user source object") st, ok := status.FromError(err) if ok && st.Code() == codes.NotFound { diff --git a/common/handlers/users/get.go b/common/handlers/users/get.go index ec795ef..bcdb30c 100644 --- a/common/handlers/users/get.go +++ b/common/handlers/users/get.go @@ -24,7 +24,7 @@ func (u UsersResourceHandler) Get(ctx context.Context, id string) (scim.Resource WithRelations: false, }) if err != nil { - logger.Error().Err(err).Msg("failed to get user") + logger.Err(err).Msg("failed to get user") st, ok := status.FromError(err) if ok && st.Code() == codes.NotFound { @@ -67,7 +67,7 @@ func (u UsersResourceHandler) GetAll(ctx context.Context, params scim.ListReques for { resp, err := u.getUsers(ctx, pageSize, pageToken) if err != nil { - logger.Error().Err(err).Msg("failed to get users") + logger.Err(err).Msg("failed to get users") return scim.Page{}, err } diff --git a/common/handlers/users/patch.go b/common/handlers/users/patch.go index 357fc1d..ae0f9d8 100644 --- a/common/handlers/users/patch.go +++ b/common/handlers/users/patch.go @@ -29,7 +29,7 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ WithRelations: false, }) if err != nil { - logger.Error().Err(err).Msg("failed to get user") + logger.Err(err).Msg("failed to get user") st, ok := status.FromError(err) if ok && st.Code() == codes.NotFound { @@ -41,27 +41,10 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ attr := converter.ObjectToResourceAttributes(getObjResp.GetResult()) - for _, op := range operations { - switch op.Op { - case scim.PatchOperationAdd: - attr, err = common.HandlePatchOPAdd(attr, op) - if err != nil { - logger.Error().Err(err).Msg("error adding property") - return scim.Resource{}, err - } - case scim.PatchOperationRemove: - attr, err = common.HandlePatchOPRemove(attr, op) - if err != nil { - logger.Error().Err(err).Msg("error removing property") - return scim.Resource{}, err - } - case scim.PatchOperationReplace: - attr, err = common.HandlePatchOPReplace(attr, op) - if err != nil { - logger.Error().Err(err).Msg("error replacing property") - return scim.Resource{}, err - } - } + attr, err = u.doOperations(operations, attr) + if err != nil { + logger.Err(err).Msg("failed to apply operations") + return scim.Resource{}, err } resource, err := u.updateUser(ctx, attr, getObjResp.GetResult(), converter, logger) @@ -74,6 +57,24 @@ func (u UsersResourceHandler) Patch(ctx context.Context, id string, operations [ return resource, nil } +func (u UsersResourceHandler) doOperations( + operations []scim.PatchOperation, + attr scim.ResourceAttributes, +) (scim.ResourceAttributes, error) { + for _, op := range operations { + switch op.Op { + case scim.PatchOperationAdd: + return common.HandlePatchOPAdd(attr, op) + case scim.PatchOperationRemove: + return common.HandlePatchOPRemove(attr, op) + case scim.PatchOperationReplace: + return common.HandlePatchOPReplace(attr, op) + } + } + + return attr, nil +} + func (u UsersResourceHandler) updateUser( ctx context.Context, attr map[string]interface{}, @@ -83,13 +84,13 @@ func (u UsersResourceHandler) updateUser( ) (scim.Resource, error) { transformResult, err := converter.TransformResource(attr, "user") if err != nil { - logger.Error().Err(err).Msg("failed to convert user to object") + logger.Err(err).Msg("failed to convert user to object") return scim.Resource{}, serrors.ScimErrorInvalidSyntax } props, err := structpb.NewStruct(attr) if err != nil { - logger.Error().Err(err).Msg("failed to convert resource attributes to struct") + logger.Err(err).Msg("failed to convert resource attributes to struct") return scim.Resource{}, err } @@ -99,13 +100,13 @@ func (u UsersResourceHandler) updateUser( Object: userObj, }) if err != nil { - logger.Error().Err(err).Msg("failed to replace user") + logger.Err(err).Msg("failed to replace user") return scim.Resource{}, err } meta, err := u.dirClient.SetUser(ctx, userObj.GetId(), transformResult, attr) if err != nil { - logger.Error().Err(err).Msg("failed to sync user") + logger.Err(err).Msg("failed to sync user") return scim.Resource{}, err } diff --git a/common/handlers/users/replace.go b/common/handlers/users/replace.go index f443eff..7f888ac 100644 --- a/common/handlers/users/replace.go +++ b/common/handlers/users/replace.go @@ -13,13 +13,13 @@ func (u UsersResourceHandler) Replace(ctx context.Context, id string, attributes err := u.Delete(ctx, id) if err != nil { - logger.Error().Err(err).Msg("failed to delete user") + logger.Err(err).Msg("failed to delete user") return scim.Resource{}, err } resource, err := u.Create(ctx, attributes) if err != nil { - logger.Error().Err(err).Msg("failed to create user") + logger.Err(err).Msg("failed to create user") return scim.Resource{}, err } diff --git a/common/patch.go b/common/patch.go index 808534d..99bedf1 100644 --- a/common/patch.go +++ b/common/patch.go @@ -49,12 +49,13 @@ func handleComplexAdd(objectProps scim.ResourceAttributes, fltr *filter.AttributeExpression, ) (scim.ResourceAttributes, error) { attrName := op.Path.AttributePath.AttributeName - properties := make(map[string]any) if objectProps[attrName] == nil { objectProps[attrName] = make([]any, 0) } + properties := make(map[string]any) + if objectProps[attrName] != nil { var err error @@ -143,43 +144,10 @@ func HandlePatchOPRemove(objectProps scim.ResourceAttributes, op scim.PatchOpera case string: delete(objectProps, op.Path.AttributePath.AttributeName) case []any: - attrExpr, ok := op.Path.ValueExpression.(*filter.AttributeExpression) - if !ok { - return nil, serrors.ScimErrorInvalidPath - } - - ftr, err := filter.ParseAttrExp([]byte(attrExpr.String())) + objectProps, err = patchOrRemoveSlice(value, op, objectProps) if err != nil { return nil, err } - - index := -1 - - if ftr.Operator == filter.EQ { - for i, v := range value { - originalValue, ok := v.(map[string]any) - if !ok { - return nil, serrors.ScimErrorInvalidPath - } - - value, ok := originalValue[ftr.AttributePath.AttributeName].(string) - if ok && value == ftr.CompareValue { - index = i - } - } - - if index == -1 { - return nil, serrors.ScimErrorMutability - } - - attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) - - if !ok { - return nil, serrors.ScimErrorInvalidPath - } - - objectProps[op.Path.AttributePath.AttributeName] = append(attrProps[:index], attrProps[index+1:]...) - } } return objectProps, err @@ -322,3 +290,45 @@ func AddProperty(objectProps scim.ResourceAttributes, op scim.PatchOperation) (s return objectProps, nil } + +func patchOrRemoveSlice(value []any, op scim.PatchOperation, objectProps scim.ResourceAttributes) (scim.ResourceAttributes, error) { + attrExpr, ok := op.Path.ValueExpression.(*filter.AttributeExpression) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + + ftr, err := filter.ParseAttrExp([]byte(attrExpr.String())) + if err != nil { + return nil, err + } + + index := -1 + + if ftr.Operator == filter.EQ { + for i, v := range value { + originalValue, ok := v.(map[string]any) + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + + value, ok := originalValue[ftr.AttributePath.AttributeName].(string) + if ok && value == ftr.CompareValue { + index = i + } + } + + if index == -1 { + return nil, serrors.ScimErrorMutability + } + + attrProps, ok := objectProps[op.Path.AttributePath.AttributeName].([]any) + + if !ok { + return nil, serrors.ScimErrorInvalidPath + } + + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps[:index], attrProps[index+1:]...) + } + + return objectProps, nil +} diff --git a/go.mod b/go.mod index 379907a..f11ce27 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,8 @@ require ( github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.36.0 + golang.org/x/sync v0.12.0 + sigs.k8s.io/controller-runtime v0.20.4 ) require ( diff --git a/go.sum b/go.sum index 3e31f76..5b295b0 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -103,6 +105,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -166,8 +170,10 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -303,6 +309,8 @@ golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -338,6 +346,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -358,8 +368,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -367,3 +375,5 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= diff --git a/pkg/app/run.go b/pkg/app/run.go index cd7f70f..18b8c39 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -122,7 +122,7 @@ func (s *SCIMServer) Shutdown(ctx context.Context) error { s.log.Info().Msg("Closing directory client connection") if err := s.dsClient.Close(); err != nil { - s.log.Error().Err(err).Msg("Failed to close directory client") + s.log.Err(err).Msg("Failed to close directory client") } }