diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bad9c40..1605892 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,14 +16,21 @@ 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.61.0" - 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: 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 }} + name: Lint package ${{ matrix.package }} + uses: golangci/golangci-lint-action@v7 with: - distribution: goreleaser - version: ${{ env.GO_RELEASER_VERSION }} - args: build --clean --snapshot --single-target + version: ${{ env.GO_LANGCI_LINT_VERSION }} + install-mode: binary + working-directory: ${{ matrix.package}} + args: --config="${{ github.workspace }}/.golangci.yaml" - - name: Lint - uses: golangci/golangci-lint-action@v6 + name: Test Setup + uses: gertd/action-gotestsum@v3.0.0 with: - version: ${{ env.GO_LANGCI_LINT_VERSION }} - verify: false - args: --timeout=30m + 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 @@ -114,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 }} diff --git a/.golangci.yaml b/.golangci.yaml index e78086a..6216a9c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,150 +1,97 @@ --- -# golangci.com configuration -# https://github.com/golangci/golangci/wiki/Configuration - -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: - 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 - golint: - min-confidence: 0 - gomnd: - checks: - - argument - - case - - condition - - return - govet: - shadow: true - 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 - maligned: - suggest-new: true - 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 - - dogsled + # https://golangci-lint.run/usage/configuration/#linters-configuration + default: all + + # explicitly disabled linters + disable: + - containedctx + - contextcheck + - cyclop + - depguard - errcheck - - copyloopvar + - exhaustruct - exhaustive - - funlen - - gochecknoinits - - goconst - - gocritic - - gocyclo - - godot - - godox - - 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-local: true + + gosec: + excludes: + - G104 # Errors unhandled + - G304 # Potential file inclusion via variable (see https://github.com/golang/go/issues/67002) + + ireturn: + allow: + - error + - empty + - scim.ResourceHandler + + 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 - - ineffassign - - misspell - - nakedret - - noctx - - rowserrcheck - - staticcheck - - stylecheck - - testpackage - - typecheck - - unconvert - - unparam - - unused - - # don't enable: - # - depguard - # - dupl - # - gochecknoglobals - # - gocognit - # - 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 - - text: "G404" - linters: - - gosec \ No newline at end of file diff --git a/cmd/aserto-scim/main.go b/cmd/aserto-scim/main.go index b63c3c6..95f707a 100644 --- a/cmd/aserto-scim/main.go +++ b/cmd/aserto-scim/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "log" "os" @@ -8,11 +9,11 @@ import ( "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 -) +var flagConfigPath string var rootCmd = &cobra.Command{ Use: "aserto-scim [flags]", @@ -32,12 +33,29 @@ 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 + } + + 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 }, } -// nolint: gochecknoinits -func init() { +func init() { //nolint: gochecknoinits cmdRun.Flags().StringVarP(&flagConfigPath, "config", "c", "", "config path") rootCmd.AddCommand(cmdRun) } diff --git a/common/assets.go b/common/assets.go new file mode 100644 index 0000000..6943707 --- /dev/null +++ b/common/assets.go @@ -0,0 +1,14 @@ +package common + +import ( + "embed" + "fmt" +) + +//go:embed assets/* +var staticAssets embed.FS + +func LoadTemplate(templateName string) ([]byte, error) { + templateFile := fmt.Sprintf("assets/%s.tmpl", templateName) + return staticAssets.ReadFile(templateFile) +} diff --git a/common/assets/users-groups-roles.tmpl b/common/assets/users-groups-roles.tmpl new file mode 100644 index 0000000..c4ab3df --- /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 $.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 $.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 $.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 }}", + "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..feb0acc --- /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 ($.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 "") }} + , + { + "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..42016b0 --- /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 ($.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 "") }} + , + { + "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..5311100 --- /dev/null +++ b/common/config/config.go @@ -0,0 +1,96 @@ +package config + +import ( + "strings" + + "github.com/pkg/errors" +) + +var ErrInvalidConfig = errors.New("invalid config") + +type Config struct { + User *User `json:"user"` + Group *Group `json:"group"` + Role *Role `json:"role"` + Relations []*Relation `json:"relations"` +} + +type User 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 Group struct { + ObjectType string `json:"object_type"` + GroupMemberRelation string `json:"group_member_relation"` + SourceObjectType string `json:"source_object_type"` +} +type Role 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 *Config) Validate() error { + if cfg.User.ObjectType == "" { + 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") + } + + 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.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") + } + } + + return nil +} + +func (c *Config) HasGroups() bool { + return c.Group != nil +} diff --git a/common/convert/config.go b/common/convert/config.go new file mode 100644 index 0000000..faf489e --- /dev/null +++ b/common/convert/config.go @@ -0,0 +1,114 @@ +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 TemplateKind int + +const ( + Users TemplateKind = iota + UsersGroups + UsersGroupsRoles +) + +var ErrInvalidConfig = errors.New("invalid config") + +func (t TemplateKind) String() string { + switch t { + case Users: + return "users" + case UsersGroups: + return "users-groups" + case UsersGroupsRoles: + return "users-groups-roles" + } + + return "unknown" +} + +type TransformConfig struct { + *config.Config + template TemplateKind + IdentityObjectType string `json:"identity_object_type,omitempty"` + IdentityRelation string `json:"identity_relation,omitempty"` +} + +func NewTransformConfig(cfg *config.Config) (*TransformConfig, error) { + template := Users + + if cfg.HasGroups() { + 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{ + Config: cfg, + template: template, + IdentityObjectType: object, + IdentityRelation: relation, + }, nil +} + +func (c *TransformConfig) ToTemplateVars() (map[string]any, error) { + 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") + } + + return result, nil +} + +func (c *TransformConfig) Template() ([]byte, error) { + return common.LoadTemplate(c.template.String()) +} + +func (c *TransformConfig) ParseIdentityRelation(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..ea08673 --- /dev/null +++ b/common/convert/convert.go @@ -0,0 +1,171 @@ +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" + "github.com/samber/lo" + "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.GetId(), + ExternalID: eID, + Attributes: attr, + Meta: meta, + } +} + +func (c *Converter) ObjectToResourceAttributes(object *dsc.Object) scim.ResourceAttributes { + attr := object.GetProperties().AsMap() + delete(attr, "password") + + return attr +} + +func Unmarshal[S any, D any](source S, dest *D) error { + data, err := json.Marshal(source) + if err != nil { + return err + } + + return json.Unmarshal(data, dest) +} + +func UserToResource(meta scim.Meta, user *model.User) (scim.Resource, error) { + attributes := scim.ResourceAttributes{} + + if err := Unmarshal(user, &attributes); 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 := scim.ResourceAttributes{} + + if err := Unmarshal(user, &attributes); err != nil { + return nil, err + } + + delete(attributes, "password") + + props, err := structpb.NewStruct(attributes) + if err != nil { + return nil, err + } + + userID := lo.Ternary(user.ID != "", user.ID, user.UserName) + displayName := lo.Ternary(user.DisplayName != "", user.DisplayName, userID) + + object := &dsc.Object{ + Type: c.cfg.User.SourceObjectType, + 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 := scim.ResourceAttributes{} + + if err := Unmarshal(group, &attributes); err != nil { + return nil, err + } + + props, err := structpb.NewStruct(attributes) + if err != nil { + return nil, err + } + + objID := lo.Ternary(group.ID != "", group.ID, group.DisplayName) + displayName := lo.Ternary(group.DisplayName != "", group.DisplayName, objID) + + object := &dsc.Object{ + Type: c.cfg.Group.SourceObjectType, + Properties: props, + Id: objID, + DisplayName: displayName, + } + + return object, nil +} + +func (c *Converter) TransformResource(resource map[string]any, objType string) (*msg.Transform, error) { + template, err := c.cfg.Template() + if err != nil { + return nil, err + } + + vars, err := c.cfg.ToTemplateVars() + if err != nil { + return nil, err + } + + 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]any, error) { + b, err := protojson.Marshal(s) + 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 new file mode 100644 index 0000000..6d5aca0 --- /dev/null +++ b/common/convert/converter_test.go @@ -0,0 +1,104 @@ +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]any = map[string]any{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "foobar", + "name": map[string]any{ + "givenName": "foo", + "familyName": "bar", + }, + "emails": []map[string]any{ + { + "primary": true, + "value": "foo@bar.com", + "type": "work", + }, + }, + "displayName": "foo bar", + "externalId": "fooooo", + "locale": "en-US", + "groups": []any{}, + "active": true, +} + +func TestTransform(t *testing.T) { + assert := require.New(t) + + cfg := config.Config{ + User: &config.User{ + 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.GetObjects()) + assert.NotEmpty(msg.GetRelations()) + assert.Len(msg.GetRelations(), 3) + + 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.GetRelations()[0].GetSubjectId()) + assert.Equal("user", msg.GetRelations()[0].GetSubjectType()) + + assert.Equal("fooooo", msg.GetRelations()[2].GetObjectId()) + assert.Equal("identity", msg.GetRelations()[2].GetObjectType()) +} + +func TestTransformUserIdentifier(t *testing.T) { + assert := require.New(t) + + cfg := config.Config{ + User: &config.User{ + 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.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 new file mode 100644 index 0000000..205b219 --- /dev/null +++ b/common/directory/client.go @@ -0,0 +1,410 @@ +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/hashicorp/go-multierror" + "github.com/rs/zerolog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "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") + + idRelation, err := s.cfg.ParseIdentityRelation(userID, "") + if err != nil { + return scim.Meta{}, err + } + + relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ + ObjectType: idRelation.GetObjectType(), + ObjectId: idRelation.GetObjectId(), + Relation: idRelation.GetRelation(), + SubjectType: idRelation.GetSubjectType(), + SubjectId: idRelation.GetSubjectId(), + WithObjects: false, + WithEmptySubjectRelation: true, + }) + 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.GetObjects(), userAttributes) + if err != nil { + return result, err + } + + logger.Trace().Any("identities", addedIdentities).Msg("added identities") + + 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.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.Err(err).Str("identity", rel.GetObjectId()).Msg("failed to delete identity") + } + } + } + + return result, mErr.ErrorOrNil() +} + +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.GetType() == s.cfg.User.ObjectType { + var userProperties map[string]any + if object.GetProperties() == nil { + userProperties = make(map[string]any) + } else { + 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, + }) + if err != nil { + if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrAlreadyExists) { + return result, addedIdentities, serrors.ScimErrorUniqueness + } + + return result, addedIdentities, err + } + + if resp.GetResult().GetType() == s.cfg.User.IdentityObjectType { + addedIdentities = append(addedIdentities, resp.GetResult().GetId()) + } + + 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.GetResult().GetCreatedAt().AsTime() + updatedAt := resp.GetResult().GetUpdatedAt().AsTime() + result.Created = &createdAt + result.LastModified = &updatedAt + result.Version = resp.GetResult().GetEtag() + } + } + + 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.ParseIdentityRelation(userID, "") + if err != nil { + return err + } + + relations, err := s.client.Reader.GetRelations(ctx, &dsr.GetRelationsRequest{ + SubjectType: identityRelation.GetSubjectType(), + SubjectId: identityRelation.GetSubjectId(), + ObjectType: identityRelation.GetObjectType(), + ObjectId: identityRelation.GetObjectId(), + Relation: identityRelation.GetRelation(), + }) + if err != nil { + st, ok := status.FromError(err) + + if ok && st.Code() == codes.NotFound { + return serrors.ScimErrorResourceNotFound(userID) + } + } + + for _, v := range relations.GetResults() { + var objectID string + + switch v.GetObjectType() { + case s.cfg.User.IdentityObjectType: + objectID = v.GetObjectId() + case s.cfg.User.ObjectType: + objectID = v.GetSubjectId() + default: + return serrors.ScimErrorBadRequest("unexpected object type in identity relation") + } + + 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, + 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 + } + + 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, + Relation: s.cfg.Group.GroupMemberRelation, + WithObjects: false, + WithEmptySubjectRelation: true, + }) + if err != nil { + st, ok := status.FromError(err) + if ok && st.Code() != codes.NotFound { + return nil, err + } + } + + return relations, nil +} + +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 + } + + return result, err + } + + if object.GetType() == s.cfg.Group.ObjectType { + if err := s.setRelations(ctx, resp.GetResult().GetId(), resp.GetResult().GetType()); err != nil { + return result, err + } + + result = s.updateMetaFromResponse(resp.GetResult()) + } + } + + 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") + + if _, err := s.client.Writer.SetRelation(ctx, &dsw.SetRelationRequest{ + Relation: relation, + }); err != nil { + return nil, 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 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 { + 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..28239d7 --- /dev/null +++ b/common/go.mod @@ -0,0 +1,52 @@ +module github.com/aserto-dev/scim/common + +go 1.24.1 + +require ( + 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 +) + +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.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 + 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/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/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-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 new file mode 100644 index 0000000..e99ac8d --- /dev/null +++ b/common/go.sum @@ -0,0 +1,142 @@ +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/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= +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/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= +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..11c04a0 --- /dev/null +++ b/common/handlers/groups/create.go @@ -0,0 +1,65 @@ +package groups + +import ( + "context" + + 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" +) + +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 := &model.Group{} + + if err := convert.Unmarshal(attributes, group); err != nil { + logger.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.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.Err(err).Msg("failed to create group") + return scim.Resource{}, err + } + + transformResult, err := converter.TransformResource(attributes, "group") + if err != nil { + 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.Err(err).Msg("failed to sync group") + return scim.Resource{}, err + } + + result = converter.ObjectToResource(sourceGroupResp.GetResult(), 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..17ddab7 --- /dev/null +++ b/common/handlers/groups/get.go @@ -0,0 +1,86 @@ +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.HasGroups() { + 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.GetResult().GetCreatedAt().AsTime() + updatedAt := resp.GetResult().GetUpdatedAt().AsTime() + resource := converter.ObjectToResource(resp.GetResult(), scim.Meta{ + Created: &createdAt, + LastModified: &updatedAt, + Version: resp.GetResult().GetEtag(), + }) + + 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") + + resources := make([]scim.Resource, 0) + + if !g.cfg.HasGroups() { + 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.GetResults() { + createdAt := v.GetCreatedAt().AsTime() + updatedAt := v.GetUpdatedAt().AsTime() + resource := converter.ObjectToResource(v, scim.Meta{ + Created: &createdAt, + LastModified: &updatedAt, + Version: v.GetEtag(), + }) + 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..2916ff1 --- /dev/null +++ b/common/handlers/groups/patch.go @@ -0,0 +1,116 @@ +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" +) + +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.HasGroups() { + 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") + + 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.GetResult()) + + for _, op := range operations { + switch op.Op { + case scim.PatchOperationAdd: + 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(attr, op) + if err != nil { + logger.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") + return scim.Resource{}, err + } + } + } + + 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.Err(err).Msg("failed to convert group to object") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + 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, groupObj.GetId(), transformResult) + if err != nil { + logger.Err(err).Msg("failed to sync group") + return scim.Resource{}, err + } + + return converter.ObjectToResource(sourceGroupResp.GetResult(), meta), nil +} diff --git a/common/handlers/groups/replace.go b/common/handlers/groups/replace.go new file mode 100644 index 0000000..b0101d8 --- /dev/null +++ b/common/handlers/groups/replace.go @@ -0,0 +1,28 @@ +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.Err(err).Msg("failed to delete group") + return scim.Resource{}, err + } + + resource, err := g.Create(ctx, attributes) + if err != nil { + logger.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..ed7b672 --- /dev/null +++ b/common/handlers/users/create.go @@ -0,0 +1,101 @@ +package users + +import ( + "context" + + 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" + "github.com/rs/zerolog" +) + +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 := u.convertAttributesToUser(attributes, logger) + if err != nil { + return scim.Resource{}, err + } + + converter := convert.NewConverter(u.cfg) + + result, err := u.createUserObject(ctx, user, attributes, converter, logger) + if err != nil { + return scim.Resource{}, err + } + + logger.Trace().Any("response", result).Msg("user created") + + 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.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 + } + + 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.Err(err).Msg("failed to convert user to map") + return scim.Resource{}, err + } + + transformResult, err := converter.TransformResource(userMap, "user") + if err != nil { + 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.Err(err).Msg("failed to sync user") + return scim.Resource{}, err + } + + return converter.ObjectToResource(sourceUserResp.GetResult(), meta), nil +} diff --git a/common/handlers/users/delete.go b/common/handlers/users/delete.go new file mode 100644 index 0000000..b65c64e --- /dev/null +++ b/common/handlers/users/delete.go @@ -0,0 +1,159 @@ +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" + "github.com/rs/zerolog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +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") + + 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 { + 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(), + ObjectType: identityRelation.GetObjectType(), + ObjectId: identityRelation.GetObjectId(), + Relation: identityRelation.GetRelation(), + }) + if err != nil { + logger.Err(err).Msg("failed to get relations") + st, ok := status.FromError(err) + + if ok && st.Code() == codes.NotFound { + return nil, serrors.ScimErrorResourceNotFound(id) + } + + 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() { + objectID, err := u.getIdentityObjectID(v, logger) + if err != nil { + return err + } + + logger.Trace().Str("id", objectID).Msg("deleting identity") + + if err := u.deleteIdentityObject(ctx, objectID, logger); err != nil { + return err + } + } + + return nil +} + +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") + } +} + +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 +} + +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, + }) + if err != nil { + logger.Err(err).Msg("failed to delete user") + st, ok := status.FromError(err) + + if ok && st.Code() == codes.NotFound { + return serrors.ScimErrorResourceNotFound(id) + } + + return err + } + + 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") + st, ok := status.FromError(err) + + if ok && st.Code() == codes.NotFound { + return serrors.ScimErrorResourceNotFound(id) + } + + return err + } + + return nil +} diff --git a/pkg/app/handlers/users/get.go b/common/handlers/users/get.go similarity index 58% rename from pkg/app/handlers/users/get.go rename to common/handlers/users/get.go index 5d2d80d..bcdb30c 100644 --- a/pkg/app/handlers/users/get.go +++ b/common/handlers/users/get.go @@ -2,40 +2,44 @@ 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" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) -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") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { + logger.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 := common.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") @@ -43,9 +47,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,22 +62,24 @@ 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 } - pageToken = resp.Page.NextToken + pageToken = resp.GetPage().GetNextToken() - for _, v := range resp.Results { - createdAt := v.CreatedAt.AsTime() - updatedAt := v.UpdatedAt.AsTime() - resource := common.ObjectToResource(v, scim.Meta{ + 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 { @@ -81,6 +87,7 @@ func (u UsersResourceHandler) GetAll(r *http.Request, params scim.ListRequestPar skipIndex++ continue } + resources = append(resources, resource) } @@ -103,8 +110,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..ae0f9d8 --- /dev/null +++ b/common/handlers/users/patch.go @@ -0,0 +1,114 @@ +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" +) + +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.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.GetResult()) + + 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) + if err != nil { + return scim.Resource{}, err + } + + logger.Trace().Any("response", resource).Msg("user patched") + + 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{}, + userObj *dsc.Object, + converter *convert.Converter, + logger zerolog.Logger, +) (scim.Resource, error) { + transformResult, err := converter.TransformResource(attr, "user") + if err != nil { + logger.Err(err).Msg("failed to convert user to object") + return scim.Resource{}, serrors.ScimErrorInvalidSyntax + } + + 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, userObj.GetId(), transformResult, attr) + if err != nil { + logger.Err(err).Msg("failed to sync user") + return scim.Resource{}, err + } + + return converter.ObjectToResource(sourceUserResp.GetResult(), meta), 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..99bedf1 --- /dev/null +++ b/common/patch.go @@ -0,0 +1,334 @@ +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) { + if op.Path == nil || op.Path.ValueExpression == nil { + return AddProperty(objectProps, op) + } + + 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 + } + + fltr, err := filter.ParseAttrExp([]byte(valueExpression.String())) + if err != nil { + return nil, err + } + + return &fltr, nil +} + +func handleSimpleAdd(objectProps scim.ResourceAttributes, op scim.PatchOperation) (scim.ResourceAttributes, error) { + properties := make(map[string]any) + properties[*op.Path.SubAttribute] = op.Value + + return objectProps, nil +} + +func handleComplexAdd(objectProps scim.ResourceAttributes, + op scim.PatchOperation, + fltr *filter.AttributeExpression, +) (scim.ResourceAttributes, error) { + attrName := op.Path.AttributePath.AttributeName + + if objectProps[attrName] == nil { + objectProps[attrName] = make([]any, 0) + } + + properties := make(map[string]any) + + if objectProps[attrName] != nil { + var err error + + properties, err = processExistingAttributes(objectProps[attrName], op, fltr) + if err != nil { + return nil, err + } + } + + if len(properties) == 0 { + 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 + } + + if result, err := processAttribute(originalValue, op, fltr); err != nil { + return nil, err + } else if result != nil { + return result, nil + } + } + + 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) { + var err error + + switch value := objectProps[op.Path.AttributePath.AttributeName].(type) { + case string: + delete(objectProps, op.Path.AttributePath.AttributeName) + case []any: + objectProps, err = patchOrRemoveSlice(value, op, objectProps) + if err != nil { + return nil, err + } + } + + 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]any) + 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]any: + if op.Path.AttributePath.SubAttribute != nil { + value[*op.Path.AttributePath.SubAttribute] = op.Value + } else { + objectProps[op.Path.AttributePath.AttributeName] = op.Value + } + case []any: + 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 []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]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]any); 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]any: + for k, v := range value { + if objectProps[k] != nil { + return nil, serrors.ScimErrorUniqueness + } + + objectProps[k] = v + } + 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].([]any); ok { + objectProps[op.Path.AttributePath.AttributeName] = append(attrProps, v) + } else { + return nil, serrors.ScimErrorInvalidPath + } + case map[string]any: + if objectProps[op.Path.AttributePath.AttributeName] == nil { + objectProps[op.Path.AttributePath.AttributeName] = make([]any, 0) + } + + properties := val + 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 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 b0c5e11..f11ce27 100644 --- a/go.mod +++ b/go.mod @@ -1,59 +1,138 @@ module github.com/aserto-dev/scim -go 1.22.11 +go 1.24.1 -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.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/docker/go-connections v0.5.0 github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 - github.com/mitchellh/mapstructure v1.5.0 + github.com/gavv/httpexpect/v2 v2.17.0 + github.com/go-viper/mapstructure/v2 v2.2.1 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/spf13/cobra v1.8.1 - github.com/spf13/viper v1.19.0 - google.golang.org/protobuf v1.36.3 + github.com/rs/zerolog v1.34.0 + github.com/spf13/cobra v1.9.1 + 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 ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.3-20241127180247-a33202765966.1 // indirect - github.com/aserto-dev/header v0.0.10 // indirect + 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 + github.com/Masterminds/sprig/v3 v3.3.0 // 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/fsnotify/fsnotify v1.7.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/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/grpc-ecosystem/grpc-gateway/v2 v2.26.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/pelletier/go-toml/v2 v2.2.2 // 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/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/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/samber/lo v1.47.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/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/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // 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 - 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 - gopkg.in/ini.v1 v1.67.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/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 + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/fsnotify.v1 v1.4.7 // 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 7c8fecf..5b295b0 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,48 @@ -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= -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= +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/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/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= @@ -21,42 +52,95 @@ 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/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= +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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +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/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/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/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= 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/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= @@ -64,94 +148,232 @@ 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/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -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/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/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= +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/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= +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/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/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/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/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +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/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/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/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +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.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.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.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= -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.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/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= +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/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/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/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= +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.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/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= +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/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= +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= +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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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.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= +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/go.work b/go.work new file mode 100644 index 0000000..6f7df19 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.24.1 + +use ( + ./ + ./common +) diff --git a/makefile b/makefile index 72de7ba..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.61.0 -GORELEASER_VERSION := 2.3.2 +GOTESTSUM_VERSION := 1.12.1 +GOLANGCI-LINT_VERSION := 2.0.2 +GORELEASER_VERSION := 2.8.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 100% rename from pkg/directory/client.go rename to pkg/app/directory/client.go 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/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/delete.go b/pkg/app/handlers/users/delete.go deleted file mode 100644 index 06f4d68..0000000 --- a/pkg/app/handlers/users/delete.go +++ /dev/null @@ -1,82 +0,0 @@ -package users - -import ( - "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" - serrors "github.com/elimity-com/scim/errors" - "github.com/pkg/errors" -) - -func (u UsersResourceHandler) Delete(r *http.Request, id string) error { - logger := u.logger.With().Str("method", "Delete").Str("id", id).Logger() - logger.Info().Msg("delete user") - - identityRelation, err := u.getIdentityRelation(id, "") - if err != nil { - u.logger.Err(err).Msg("failed to get identity relation") - return err - } - - var identities []*dsc.Relation - - resp, err := u.dirClient.Reader.GetRelations(r.Context(), &dsr.GetRelationsRequest{ - SubjectType: identityRelation.SubjectType, - SubjectId: identityRelation.SubjectId, - Relation: identityRelation.Relation, - ObjectId: identityRelation.ObjectId, - ObjectType: identityRelation.ObjectType, - }) - if err != nil { - logger.Err(err).Msg("failed to get identities") - if errors.Is(cerr.UnwrapAsertoError(err), derr.ErrObjectNotFound) { - return serrors.ScimErrorResourceNotFound(id) - } - return err - } - identities = resp.Results - - for _, v := range identities { - var objectID string - switch v.ObjectType { - case u.cfg.SCIM.IdentityObjectType: - objectID = v.ObjectId - case u.cfg.SCIM.UserObjectType: - 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{ - ObjectId: objectID, - ObjectType: u.cfg.SCIM.IdentityObjectType, - WithRelations: true, - }) - if err != nil { - logger.Err(err).Msg("failed to delete identity") - return err - } - } - - _, err = u.dirClient.Writer.DeleteObject(r.Context(), &dsw.DeleteObjectRequest{ - ObjectType: u.cfg.SCIM.UserObjectType, - ObjectId: id, - WithRelations: true, - }) - if err != nil { - logger.Err(err).Msg("failed to delete user") - 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/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..18b8c39 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -1,63 +1,63 @@ package app import ( + "context" "crypto/sha256" "crypto/subtle" + "fmt" "net/http" "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 { +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 } - userHandler, err := users.NewUsersResourceHandler(cfg, scimLogger) + return &SCIMServer{ + log: scimLogger, + cfg: cfg, + }, nil +} + +func (s *SCIMServer) Run() error { + dsClient, err := directory.GetDirectoryClient(&s.cfg.Directory) 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, - } + s.dsClient = dsClient - groupHandler, err := groups.NewGroupResourceHandler(cfg, scimLogger) + 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"), @@ -69,12 +69,10 @@ func Run(cfgPath string, logWriter logger.Writer, errWriter logger.ErrWriter) er 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) @@ -83,29 +81,121 @@ 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, - 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, } - 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.Err(err).Msg("Failed to close directory client") + } + } + + s.dsClient = nil + s.log.Info().Msg("SCIM server shutdown complete") + + return nil +} + +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 NewResourceHandler(usersResourceHandler) +} + +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 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 + type application struct { cfg *config.AuthConfig } @@ -118,23 +208,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 @@ -146,3 +227,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/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..9ac2506 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,15 +3,24 @@ package config import ( "os" "strings" + "time" client "github.com/aserto-dev/go-aserto" "github.com/aserto-dev/logger" - "github.com/mitchellh/mapstructure" + config "github.com/aserto-dev/scim/common/config" + "github.com/go-viper/mapstructure/v2" "github.com/pkg/errors" "github.com/rs/zerolog" "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") @@ -21,34 +30,16 @@ 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 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.Config `json:"scim"` } type AuthConfig struct { @@ -63,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() @@ -91,12 +82,18 @@ 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("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") + 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", "") @@ -113,6 +110,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) @@ -142,37 +140,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) { 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..c177caa --- /dev/null +++ b/pkg/test/assets/config/topaz.yaml @@ -0,0 +1,178 @@ +# 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: '/data/db.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: + 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: "" 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..fac96a5 --- /dev/null +++ b/pkg/test/common/common.go @@ -0,0 +1,193 @@ +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/docker/go-connections/nat" + "github.com/stretchr/testify/require" + testcontainers "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + containerStartTimeout = 300 * time.Second + topazConfigFileMode = 0o700 +) + +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 { + t.Logf("\nTEST CONTAINER IMAGE: %q\n", TopazImage()) + + req := testcontainers.ContainerRequest{ + AlwaysPullImage: true, + Image: TopazImage(), + ExposedPorts: []string{"9292/tcp"}, + Files: []testcontainers.ContainerFile{ + { + Reader: assets_test.TopazConfigReader(), + ContainerFilePath: "/config/config.yaml", + FileMode: topazConfigFileMode, + }, + }, + WaitingFor: wait.ForAll( + wait.ForExposedPort(), + wait.ForLog("Starting 0.0.0.0:9292 gRPC server"), + ).WithStartupTimeoutDefault(containerStartTimeout), + } + + topaz, err := testcontainers.GenericContainer(t.Context(), testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: false, + }) + require.NoError(t, err) + + if err := topaz.Start(t.Context()); err != nil { + require.NoError(t, err) + } + + addr, err := MappedAddr(t.Context(), topaz, "9292") + require.NoError(t, err) + + t.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() { + srv.Run() + }() + + 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(t.Context()) + require.NoError(t, err) + err = stream.Send(&dsm.SetManifestRequest{ + Msg: &dsm.SetManifestRequest_Body{ + Body: &dsm.Body{ + Data: assets_test.Manifest(), + }, + }, + }) + require.NoError(t, err) + _, err = stream.CloseAndRecv() + require.NoError(t, err) + + t.Cleanup(func() { + conn.Close() + srv.Shutdown(t.Context()) + testcontainers.CleanupContainer(t, topaz) + }) + + 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.GetResult() != 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.GetResult() != 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.GetResult() == nil { + return nil + } + + return userResp.GetResult().GetProperties().GetFields()[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..ed52f4b --- /dev/null +++ b/pkg/test/scim_test.go @@ -0,0 +1,52 @@ +package scim_test + +import ( + "encoding/json" + "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) + + 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)) +}