snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
90
cmd/identities/definitions.go
Normal file
90
cmd/identities/definitions.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
|
||||
"github.com/ory/x/cmdx"
|
||||
)
|
||||
|
||||
type (
|
||||
outputIdentity kratos.Identity
|
||||
outputIdentityCollection struct {
|
||||
Identities []kratos.Identity `json:"identities"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
includePageToken bool
|
||||
}
|
||||
)
|
||||
|
||||
func (outputIdentity) Header() []string {
|
||||
return []string{"ID", "EXTERNAL ID", "VERIFIED ADDRESSES", "RECOVERY ADDRESSES", "SCHEMA ID", "SCHEMA URL"}
|
||||
}
|
||||
|
||||
func (i outputIdentity) Columns() []string {
|
||||
data := [6]string{
|
||||
i.Id,
|
||||
cmdx.None,
|
||||
cmdx.None,
|
||||
cmdx.None,
|
||||
cmdx.None,
|
||||
cmdx.None,
|
||||
}
|
||||
|
||||
if i.ExternalId != nil && len(*i.ExternalId) > 0 {
|
||||
data[1] = *i.ExternalId
|
||||
}
|
||||
|
||||
addresses := make([]string, 0, len(i.VerifiableAddresses))
|
||||
for _, a := range i.VerifiableAddresses {
|
||||
if len(a.Value) > 0 {
|
||||
addresses = append(addresses, a.Value)
|
||||
}
|
||||
}
|
||||
data[2] = strings.Join(addresses, ", ")
|
||||
|
||||
addresses = addresses[:0]
|
||||
for _, a := range i.RecoveryAddresses {
|
||||
if len(a.Value) > 0 {
|
||||
addresses = append(addresses, a.Value)
|
||||
}
|
||||
}
|
||||
data[3] = strings.Join(addresses, ", ")
|
||||
data[4] = i.SchemaId
|
||||
data[5] = i.SchemaUrl
|
||||
|
||||
return data[:]
|
||||
}
|
||||
|
||||
func (i outputIdentity) Interface() interface{} {
|
||||
return i
|
||||
}
|
||||
|
||||
func (outputIdentityCollection) Header() []string {
|
||||
return outputIdentity{}.Header()
|
||||
}
|
||||
|
||||
func (c outputIdentityCollection) Table() [][]string {
|
||||
rows := make([][]string, len(c.Identities))
|
||||
for i, ident := range c.Identities {
|
||||
rows[i] = outputIdentity(ident).Columns()
|
||||
}
|
||||
return append(rows,
|
||||
[]string{""},
|
||||
[]string{"NEXT PAGE TOKEN", c.NextPageToken},
|
||||
)
|
||||
}
|
||||
|
||||
func (c outputIdentityCollection) Interface() interface{} {
|
||||
if c.includePageToken {
|
||||
return c
|
||||
}
|
||||
return c.Identities
|
||||
}
|
||||
|
||||
func (c *outputIdentityCollection) Len() int {
|
||||
return len(c.Identities)
|
||||
}
|
||||
72
cmd/identities/delete.go
Normal file
72
cmd/identities/delete.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/ory/kratos/cmd/cliclient"
|
||||
"github.com/ory/kratos/pkg/clihelpers"
|
||||
"github.com/ory/x/cmdx"
|
||||
)
|
||||
|
||||
func NewDeleteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete resources",
|
||||
}
|
||||
cmd.AddCommand(NewDeleteIdentityCmd())
|
||||
cliclient.RegisterClientFlags(cmd.PersistentFlags())
|
||||
cmdx.RegisterFormatFlags(cmd.PersistentFlags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewDeleteIdentityCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "identity id-0 [id-1] [id-2] [id-n]",
|
||||
Short: "Delete one or more identities by their ID(s)",
|
||||
Long: fmt.Sprintf(`This command deletes one or more identities by ID. To delete an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.
|
||||
|
||||
%s`, clihelpers.WarningJQIsComplicated),
|
||||
Example: `To delete the identity with the recovery email address "foo@bar.com", run:
|
||||
|
||||
{{ .CommandPath }} $({{ .Root.Name }} list identities --format json | jq -r 'map(select(.recovery_addresses[].value == "foo@bar.com")) | .[].id')`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := cliclient.NewClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
deleted = make([]cmdx.OutputIder, 0, len(args))
|
||||
failed = make(map[string]error)
|
||||
)
|
||||
|
||||
for _, a := range args {
|
||||
_, err := c.IdentityAPI.DeleteIdentity(cmd.Context(), a).Execute()
|
||||
if err != nil {
|
||||
failed[a] = cmdx.PrintOpenAPIError(cmd, err)
|
||||
continue
|
||||
}
|
||||
deleted = append(deleted, cmdx.OutputIder(a))
|
||||
}
|
||||
|
||||
if len(deleted) == 1 {
|
||||
cmdx.PrintRow(cmd, &deleted[0])
|
||||
} else if len(deleted) > 1 {
|
||||
cmdx.PrintTable(cmd, &cmdx.OutputIderCollection{Items: deleted})
|
||||
}
|
||||
|
||||
cmdx.PrintErrors(cmd, failed)
|
||||
if len(failed) != 0 {
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
61
cmd/identities/delete_test.go
Normal file
61
cmd/identities/delete_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/cmd/identities"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/sqlcon"
|
||||
)
|
||||
|
||||
func TestDeleteCmd(t *testing.T) {
|
||||
reg, cmd := setup(t, identities.NewDeleteIdentityCmd)
|
||||
|
||||
t.Run("case=deletes successfully", func(t *testing.T) {
|
||||
// create identity to delete
|
||||
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
||||
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, i.ID.String())
|
||||
|
||||
// expect ID and no error
|
||||
assert.Equal(t, i.ID.String(), gjson.Parse(stdOut).String())
|
||||
|
||||
// expect identity to be deleted
|
||||
_, err := reg.Persister().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
|
||||
assert.True(t, errors.Is(err, sqlcon.ErrNoRows()))
|
||||
})
|
||||
|
||||
t.Run("case=deletes three identities", func(t *testing.T) {
|
||||
is, ids := makeIdentities(t, reg, 3)
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, ids...)
|
||||
|
||||
assert.Equal(t, `["`+strings.Join(ids, "\",\"")+"\"]\n", stdOut)
|
||||
|
||||
for _, i := range is {
|
||||
_, err := reg.Persister().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("case=fails with unknown ID", func(t *testing.T) {
|
||||
stdErr := cmd.ExecExpectedErr(t, x.NewUUID().String())
|
||||
|
||||
assert.Contains(t, stdErr, "Unable to locate the resource", stdErr)
|
||||
})
|
||||
}
|
||||
116
cmd/identities/get.go
Normal file
116
cmd/identities/get.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
"github.com/ory/x/cmdx"
|
||||
"github.com/ory/x/stringsx"
|
||||
|
||||
"github.com/ory/kratos/pkg/clihelpers"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/ory/kratos/cmd/cliclient"
|
||||
)
|
||||
|
||||
const (
|
||||
FlagIncludeCreds = "include-credentials"
|
||||
FlagExternalID = "external-id"
|
||||
)
|
||||
|
||||
func NewGetCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get resources",
|
||||
}
|
||||
cmd.AddCommand(NewGetIdentityCmd())
|
||||
cliclient.RegisterClientFlags(cmd.PersistentFlags())
|
||||
cmdx.RegisterFormatFlags(cmd.PersistentFlags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewGetIdentityCmd() *cobra.Command {
|
||||
var (
|
||||
includeCreds []string
|
||||
byExternalID bool
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "identity [id-1] [id-2] [id-n]",
|
||||
Short: "Get one or more identities by their ID(s)",
|
||||
Long: fmt.Sprintf(`This command gets all the details about an identity. To get an identity by some selector, e.g. the recovery email address, use the list command in combination with jq.
|
||||
|
||||
Use the --%s flag to look up identities by their external ID instead of their Ory-internal ID.
|
||||
|
||||
%s`, FlagExternalID, clihelpers.WarningJQIsComplicated),
|
||||
Example: `To get the identities with the recovery email address at the domain "ory.sh", run:
|
||||
|
||||
{{ .CommandPath }} $({{ .Root.Name }} ls identities --format json | jq -r 'map(select(.recovery_addresses[].value | endswith("@ory.sh"))) | .[].id')
|
||||
|
||||
To get an identity by its external ID, run:
|
||||
|
||||
{{ .CommandPath }} --external-id customer-12345`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := cliclient.NewClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we check includeCreds argument is valid
|
||||
for _, opt := range includeCreds {
|
||||
e := stringsx.SwitchExact(opt)
|
||||
if !e.AddCase("oidc") {
|
||||
cmd.PrintErrln(`You have to put a valid value of credentials type to be included, try --help for details.`)
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
identities := make([]kratos.Identity, 0, len(args))
|
||||
failed := make(map[string]error)
|
||||
for _, id := range args {
|
||||
var identity *kratos.Identity
|
||||
if byExternalID {
|
||||
identity, _, err = c.IdentityAPI.
|
||||
GetIdentityByExternalID(cmd.Context(), id).
|
||||
IncludeCredential(includeCreds).
|
||||
Execute()
|
||||
} else {
|
||||
identity, _, err = c.IdentityAPI.
|
||||
GetIdentity(cmd.Context(), id).
|
||||
IncludeCredential(includeCreds).
|
||||
Execute()
|
||||
}
|
||||
|
||||
if clihelpers.SDKError(err) != nil {
|
||||
failed[id] = cmdx.PrintOpenAPIError(cmd, err)
|
||||
continue
|
||||
}
|
||||
|
||||
identities = append(identities, *identity)
|
||||
}
|
||||
|
||||
if len(identities) == 1 {
|
||||
cmdx.PrintRow(cmd, (*outputIdentity)(&identities[0]))
|
||||
} else if len(identities) > 1 {
|
||||
cmdx.PrintTable(cmd, &outputIdentityCollection{Identities: identities, includePageToken: false})
|
||||
}
|
||||
cmdx.PrintErrors(cmd, failed)
|
||||
|
||||
if len(failed) != 0 {
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
// include credential flag to add third party tokens in returned data
|
||||
flags.StringArrayVarP(&includeCreds, FlagIncludeCreds, "i", []string{}, `Include third party tokens (only "oidc" supported) `)
|
||||
flags.BoolVar(&byExternalID, FlagExternalID, false, "Treat the provided ID(s) as external IDs and look up the identities by their external ID instead of their Ory-internal ID.")
|
||||
return cmd
|
||||
}
|
||||
132
cmd/identities/get_test.go
Normal file
132
cmd/identities/get_test.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/cmd/identities"
|
||||
"github.com/ory/x/assertx"
|
||||
"github.com/ory/x/sqlxx"
|
||||
|
||||
"github.com/ory/kratos/x"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
)
|
||||
|
||||
func TestGetCmd(t *testing.T) {
|
||||
reg, cmd := setup(t, identities.NewGetIdentityCmd)
|
||||
|
||||
t.Run("case=gets a single identity", func(t *testing.T) {
|
||||
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
||||
i.MetadataPublic = []byte(`"public"`)
|
||||
i.MetadataAdmin = []byte(`"admin"`)
|
||||
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, i.ID.String())
|
||||
|
||||
ij, err := json.Marshal(identity.WithCredentialsNoConfigAndAdminMetadataInJSON(*i))
|
||||
require.NoError(t, err)
|
||||
|
||||
assertx.EqualAsJSONExcept(t, json.RawMessage(ij), json.RawMessage(stdOut), []string{"created_at", "updated_at", "AdditionalProperties"})
|
||||
})
|
||||
|
||||
t.Run("case=gets three identities", func(t *testing.T) {
|
||||
is, ids := makeIdentities(t, reg, 3)
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, ids...)
|
||||
|
||||
isj, err := json.Marshal(is)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertx.EqualAsJSONExcept(t, json.RawMessage(isj), json.RawMessage(stdOut), []string{"created_at", "updated_at", "AdditionalProperties"})
|
||||
})
|
||||
|
||||
t.Run("case=fails with unknown ID", func(t *testing.T) {
|
||||
stdErr := cmd.ExecExpectedErr(t, x.NewUUID().String())
|
||||
|
||||
assert.Contains(t, stdErr, "Unable to locate the resource", stdErr)
|
||||
})
|
||||
|
||||
t.Run("case=gets a single identity by its external ID", func(t *testing.T) {
|
||||
externalID := "customer-" + x.NewUUID().String()
|
||||
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
||||
i.ExternalID = sqlxx.NullString(externalID)
|
||||
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, "--"+identities.FlagExternalID, externalID)
|
||||
|
||||
assert.Equal(t, i.ID.String(), gjson.Get(stdOut, "id").String())
|
||||
assert.Equal(t, externalID, gjson.Get(stdOut, "external_id").String())
|
||||
})
|
||||
|
||||
t.Run("case=fails with unknown external ID", func(t *testing.T) {
|
||||
stdErr := cmd.ExecExpectedErr(t, "--"+identities.FlagExternalID, "does-not-exist")
|
||||
|
||||
assert.Contains(t, stdErr, "Unable to locate the resource", stdErr)
|
||||
})
|
||||
|
||||
t.Run("case=gets a single identity with oidc credentials", func(t *testing.T) {
|
||||
applyCredentials := func(identifier, accessToken, refreshToken, idToken string, encrypt bool) identity.Credentials {
|
||||
toJson := func(c identity.CredentialsOIDC) []byte {
|
||||
out, err := json.Marshal(&c)
|
||||
require.NoError(t, err)
|
||||
return out
|
||||
}
|
||||
transform := func(token string) string {
|
||||
if encrypt {
|
||||
s, err := reg.Cipher(context.Background()).Encrypt(context.Background(), []byte(token))
|
||||
require.NoError(t, err)
|
||||
return s
|
||||
}
|
||||
return token
|
||||
}
|
||||
return identity.Credentials{
|
||||
Type: identity.CredentialsTypeOIDC,
|
||||
Identifiers: []string{"bar:" + identifier},
|
||||
Config: toJson(identity.CredentialsOIDC{Providers: []identity.CredentialsOIDCProvider{
|
||||
{
|
||||
Subject: "foo",
|
||||
Provider: "bar",
|
||||
InitialAccessToken: transform(accessToken + "0"),
|
||||
InitialRefreshToken: transform(refreshToken + "0"),
|
||||
InitialIDToken: transform(idToken + "0"),
|
||||
Organization: "foo-org-id",
|
||||
},
|
||||
{
|
||||
Subject: "baz",
|
||||
Provider: "zab",
|
||||
InitialAccessToken: transform(accessToken + "1"),
|
||||
InitialRefreshToken: transform(refreshToken + "1"),
|
||||
InitialIDToken: transform(idToken + "1"),
|
||||
Organization: "bar-org-id",
|
||||
},
|
||||
}}),
|
||||
}
|
||||
}
|
||||
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
||||
i.MetadataPublic = []byte(`"public"`)
|
||||
i.MetadataAdmin = []byte(`"admin"`)
|
||||
i.SetCredentials(identity.CredentialsTypeOIDC, applyCredentials("uniqueIdentifier", "accessBar", "refreshBar", "idBar", true))
|
||||
// duplicate identity with decrypted tokens
|
||||
di := i.CopyWithoutCredentials()
|
||||
di.SetCredentials(identity.CredentialsTypeOIDC, applyCredentials("uniqueIdentifier", "accessBar", "refreshBar", "idBar", false))
|
||||
|
||||
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, "--"+identities.FlagIncludeCreds, "oidc", i.ID.String())
|
||||
ij, err := json.Marshal(identity.WithCredentialsAndAdminMetadataInJSON(*di))
|
||||
require.NoError(t, err)
|
||||
|
||||
ii := []string{"id", "schema_url", "state_changed_at", "created_at", "updated_at", "credentials.oidc.created_at", "credentials.oidc.updated_at", "credentials.oidc.version", "AdditionalProperties"}
|
||||
assertx.EqualAsJSONExcept(t, json.RawMessage(ij), json.RawMessage(stdOut), ii)
|
||||
})
|
||||
}
|
||||
53
cmd/identities/helpers.go
Normal file
53
cmd/identities/helpers.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/x/cmdx"
|
||||
)
|
||||
|
||||
func parseIdentities(raw []byte) (rawIdentities []string) {
|
||||
res := gjson.ParseBytes(raw)
|
||||
if !res.IsArray() {
|
||||
return []string{res.Raw}
|
||||
}
|
||||
res.ForEach(func(_, v gjson.Result) bool {
|
||||
rawIdentities = append(rawIdentities, v.Raw)
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func readIdentities(cmd *cobra.Command, args []string) (map[string]string, error) {
|
||||
rawIdentities := make(map[string]string)
|
||||
if len(args) == 0 {
|
||||
fc, err := io.ReadAll(cmd.InOrStdin())
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "STD_IN: Could not read: %s\n", err)
|
||||
return nil, cmdx.FailSilently(cmd)
|
||||
}
|
||||
for i, id := range parseIdentities(fc) {
|
||||
rawIdentities[fmt.Sprintf("STD_IN[%d]", i)] = id
|
||||
}
|
||||
return rawIdentities, nil
|
||||
}
|
||||
for _, fn := range args {
|
||||
fc, err := os.ReadFile(fn) // #nosec G304 -- file is supplied by user
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not open identity file: %s\n", fn, err)
|
||||
return nil, cmdx.FailSilently(cmd)
|
||||
}
|
||||
for i, id := range parseIdentities(fc) {
|
||||
rawIdentities[fmt.Sprintf("%s[%d]", fn, i)] = id
|
||||
}
|
||||
}
|
||||
return rawIdentities, nil
|
||||
}
|
||||
49
cmd/identities/helpers_test.go
Normal file
49
cmd/identities/helpers_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/x/cmdx"
|
||||
|
||||
"github.com/ory/kratos/identity"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/cmd/cliclient"
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/pkg"
|
||||
"github.com/ory/kratos/pkg/testhelpers"
|
||||
)
|
||||
|
||||
func setup(t *testing.T, newCmd func() *cobra.Command) (*driver.RegistryDefault, *cmdx.CommandExecuter) {
|
||||
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
_, admin := testhelpers.NewKratosServerWithCSRF(t, reg)
|
||||
testhelpers.SetDefaultIdentitySchema(conf, "file://./stubs/identity.schema.json")
|
||||
// setup command
|
||||
return reg, &cmdx.CommandExecuter{
|
||||
New: func() *cobra.Command {
|
||||
cmd := newCmd()
|
||||
cliclient.RegisterClientFlags(cmd.Flags())
|
||||
cmdx.RegisterFormatFlags(cmd.Flags())
|
||||
return cmd
|
||||
},
|
||||
PersistentArgs: []string{"--" + cliclient.FlagEndpoint, admin.URL, "--" + cmdx.FlagFormat, string(cmdx.FormatJSON)},
|
||||
}
|
||||
}
|
||||
|
||||
func makeIdentities(t *testing.T, reg driver.Registry, n int) (is []*identity.Identity, ids []string) {
|
||||
for j := 0; j < n; j++ {
|
||||
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
||||
i.MetadataPublic = []byte(`{"foo":"bar"}`)
|
||||
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
|
||||
is = append(is, i)
|
||||
ids = append(ids, i.ID.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
98
cmd/identities/import.go
Normal file
98
cmd/identities/import.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
|
||||
"github.com/ory/x/cmdx"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/ory/kratos/cmd/cliclient"
|
||||
)
|
||||
|
||||
func NewImportCmd() *cobra.Command {
|
||||
var cmd = &cobra.Command{
|
||||
Use: "import",
|
||||
Short: "Import resources",
|
||||
}
|
||||
cmd.AddCommand(NewImportIdentitiesCmd())
|
||||
cliclient.RegisterClientFlags(cmd.PersistentFlags())
|
||||
cmdx.RegisterFormatFlags(cmd.PersistentFlags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewImportIdentitiesCmd represents the import command
|
||||
func NewImportIdentitiesCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "identities file-1.json [file-2.json] [file-3.json] [file-n.json]",
|
||||
Short: "Import one or more identities from files or STD_IN",
|
||||
Example: `Create an example identity:
|
||||
|
||||
cat > ./file.json <<EOF
|
||||
{
|
||||
"schema_id": "default",
|
||||
"traits": {
|
||||
"email": "foo@example.com"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
{{ .CommandPath }} file.json
|
||||
|
||||
Alternatively:
|
||||
|
||||
cat file.json | {{ .CommandPath }}`,
|
||||
Long: `Import identities from files or STD_IN.
|
||||
|
||||
Files can contain only a single or an array of identities. The validity of files can be tested beforehand using "... identities validate".`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := cliclient.NewClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imported := make([]kratos.Identity, 0, len(args))
|
||||
failed := make(map[string]error)
|
||||
|
||||
is, err := readIdentities(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for src, i := range is {
|
||||
var params kratos.CreateIdentityBody
|
||||
err = json.Unmarshal([]byte(i), ¶ms)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "STD_IN: Could not parse identity")
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
|
||||
ident, _, err := c.IdentityAPI.CreateIdentity(cmd.Context()).CreateIdentityBody(params).Execute()
|
||||
if err != nil {
|
||||
failed[src] = cmdx.PrintOpenAPIError(cmd, err)
|
||||
} else {
|
||||
imported = append(imported, *ident)
|
||||
}
|
||||
}
|
||||
|
||||
if len(imported) == 1 {
|
||||
cmdx.PrintRow(cmd, (*outputIdentity)(&imported[0]))
|
||||
} else {
|
||||
cmdx.PrintTable(cmd, &outputIdentityCollection{Identities: imported})
|
||||
}
|
||||
cmdx.PrintErrors(cmd, failed)
|
||||
|
||||
if len(failed) != 0 {
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
150
cmd/identities/import_test.go
Normal file
150
cmd/identities/import_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/identity"
|
||||
|
||||
"github.com/ory/kratos/cmd/identities"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
)
|
||||
|
||||
func TestImportCmd(t *testing.T) {
|
||||
reg, cmd := setup(t, identities.NewImportIdentitiesCmd)
|
||||
|
||||
t.Run("case=imports a new identity from file", func(t *testing.T) {
|
||||
i := kratos.CreateIdentityBody{
|
||||
SchemaId: config.DefaultIdentityTraitsSchemaID,
|
||||
Traits: map[string]interface{}{},
|
||||
}
|
||||
ij, err := json.Marshal(i)
|
||||
require.NoError(t, err)
|
||||
f, err := os.CreateTemp("", "")
|
||||
require.NoError(t, err)
|
||||
_, err = f.Write(ij)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, f.Name())
|
||||
|
||||
id, err := uuid.FromString(gjson.Get(stdOut, "id").String())
|
||||
require.NoError(t, err)
|
||||
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=imports multiple identities from single file", func(t *testing.T) {
|
||||
i := []kratos.CreateIdentityBody{
|
||||
{
|
||||
SchemaId: config.DefaultIdentityTraitsSchemaID,
|
||||
Traits: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
SchemaId: config.DefaultIdentityTraitsSchemaID,
|
||||
Traits: map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
ij, err := json.Marshal(i)
|
||||
require.NoError(t, err)
|
||||
f, err := os.CreateTemp("", "")
|
||||
require.NoError(t, err)
|
||||
_, err = f.Write(ij)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, f.Name())
|
||||
|
||||
id, err := uuid.FromString(gjson.Get(stdOut, "0.id").String())
|
||||
require.NoError(t, err)
|
||||
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
|
||||
assert.NoError(t, err)
|
||||
|
||||
id, err = uuid.FromString(gjson.Get(stdOut, "1.id").String())
|
||||
require.NoError(t, err)
|
||||
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=imports a new identity from STD_IN", func(t *testing.T) {
|
||||
i := []kratos.CreateIdentityBody{
|
||||
{
|
||||
SchemaId: config.DefaultIdentityTraitsSchemaID,
|
||||
Traits: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
SchemaId: config.DefaultIdentityTraitsSchemaID,
|
||||
Traits: map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
ij, err := json.Marshal(i)
|
||||
require.NoError(t, err)
|
||||
|
||||
stdOut, stdErr, err := cmd.Exec(bytes.NewBuffer(ij))
|
||||
require.NoError(t, err, "%s %s", stdOut, stdErr)
|
||||
|
||||
id, err := uuid.FromString(gjson.Get(stdOut, "0.id").String())
|
||||
require.NoError(t, err)
|
||||
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
|
||||
assert.NoError(t, err)
|
||||
|
||||
id, err = uuid.FromString(gjson.Get(stdOut, "1.id").String())
|
||||
require.NoError(t, err)
|
||||
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=imports multiple identities from STD_IN", func(t *testing.T) {
|
||||
i := kratos.CreateIdentityBody{
|
||||
SchemaId: config.DefaultIdentityTraitsSchemaID,
|
||||
Traits: map[string]interface{}{},
|
||||
}
|
||||
ij, err := json.Marshal(i)
|
||||
require.NoError(t, err)
|
||||
|
||||
stdOut, stdErr, err := cmd.Exec(bytes.NewBuffer(ij))
|
||||
require.NoError(t, err, "%s %s", stdOut, stdErr)
|
||||
|
||||
id, err := uuid.FromString(gjson.Get(stdOut, "id").String())
|
||||
require.NoError(t, err)
|
||||
_, err = reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=imports an identity with an external ID", func(t *testing.T) {
|
||||
externalID := "customer-12345"
|
||||
i := kratos.CreateIdentityBody{
|
||||
SchemaId: config.DefaultIdentityTraitsSchemaID,
|
||||
Traits: map[string]interface{}{},
|
||||
ExternalId: kratos.PtrString(externalID),
|
||||
}
|
||||
ij, err := json.Marshal(i)
|
||||
require.NoError(t, err)
|
||||
|
||||
stdOut, stdErr, err := cmd.Exec(bytes.NewBuffer(ij))
|
||||
require.NoError(t, err, "%s %s", stdOut, stdErr)
|
||||
|
||||
// the external ID must be reflected in the command output
|
||||
assert.Equal(t, externalID, gjson.Get(stdOut, "external_id").String())
|
||||
|
||||
// and it must be persisted on the identity
|
||||
id, err := uuid.FromString(gjson.Get(stdOut, "id").String())
|
||||
require.NoError(t, err)
|
||||
stored, err := reg.Persister().GetIdentity(context.Background(), id, identity.ExpandNothing)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, externalID, stored.ExternalID.String())
|
||||
})
|
||||
}
|
||||
80
cmd/identities/list.go
Normal file
80
cmd/identities/list.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/ory/x/flagx"
|
||||
|
||||
"github.com/ory/x/pagination/keysetclient"
|
||||
|
||||
"github.com/ory/kratos/cmd/cliclient"
|
||||
"github.com/ory/x/cmdx"
|
||||
)
|
||||
|
||||
func NewListCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List resources",
|
||||
}
|
||||
c.AddCommand(NewListIdentitiesCmd())
|
||||
cliclient.RegisterClientFlags(c.PersistentFlags())
|
||||
cmdx.RegisterFormatFlags(c.PersistentFlags())
|
||||
return c
|
||||
}
|
||||
|
||||
func NewListIdentitiesCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "identities",
|
||||
Short: "List identities",
|
||||
Long: `Return a list of identities.
|
||||
|
||||
The consistency defaults to ` + "`eventual`" + ` and can be set to ` + "`strong`" + ` or ` + "`eventual`" + `.
|
||||
Eventual consistency means that the list operation will return faster and might not include recently created or updated identities. Replication lag is about 5 seconds.`,
|
||||
Example: "{{ .CommandPath }} --page-size 100 --consistency eventual",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
// zero or exactly two args
|
||||
if len(args) != 0 && len(args) != 2 {
|
||||
return fmt.Errorf("expected zero or two args, got %d: %+v", len(args), args)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := cliclient.NewClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
consistency := flagx.MustGetString(cmd, "consistency")
|
||||
req := c.IdentityAPI.ListIdentities(cmd.Context()).Consistency(consistency)
|
||||
page, perPage, err := cmdx.ParseTokenPaginationArgs(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req = req.PageToken(page)
|
||||
req = req.PageSize(int64(perPage))
|
||||
|
||||
identities, res, err := req.Execute()
|
||||
if err != nil {
|
||||
return cmdx.PrintOpenAPIError(cmd, err)
|
||||
}
|
||||
|
||||
pages := keysetclient.ParseHeader(res)
|
||||
cmdx.PrintTable(cmd, &outputIdentityCollection{
|
||||
Identities: identities,
|
||||
NextPageToken: pages.NextToken,
|
||||
includePageToken: true,
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c.Flags().String("consistency", "eventual", "The read consistency to use. Can be either \"strong\" or \"eventual\". Defaults to \"eventual\".")
|
||||
cmdx.RegisterTokenPaginationFlags(c)
|
||||
return c
|
||||
}
|
||||
66
cmd/identities/list_test.go
Normal file
66
cmd/identities/list_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/cmd/identities"
|
||||
|
||||
"github.com/ory/x/cmdx"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/identity"
|
||||
)
|
||||
|
||||
func TestListCmd(t *testing.T) {
|
||||
reg, cmd := setup(t, identities.NewListIdentitiesCmd)
|
||||
|
||||
var deleteIdentities = func(t *testing.T, is []*identity.Identity) {
|
||||
for _, i := range is {
|
||||
require.NoError(t, reg.Persister().DeleteIdentity(context.Background(), i.ID))
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("case=lists all identities with default pagination", func(t *testing.T) {
|
||||
is, ids := makeIdentities(t, reg, 5)
|
||||
t.Cleanup(func() {
|
||||
deleteIdentities(t, is)
|
||||
})
|
||||
|
||||
stdOut := cmd.ExecNoErr(t, "--"+cmdx.FlagQuiet)
|
||||
|
||||
for _, i := range ids {
|
||||
assert.Contains(t, stdOut, i)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("case=lists all identities with pagination", func(t *testing.T) {
|
||||
is, ids := makeIdentities(t, reg, 10)
|
||||
t.Cleanup(func() {
|
||||
deleteIdentities(t, is)
|
||||
})
|
||||
|
||||
first := cmd.ExecNoErr(t, "--format", "json-pretty", "--page-size", "2")
|
||||
nextPageToken := gjson.Get(first, "next_page_token").String()
|
||||
actualIDs := gjson.Get(first, "identities.#.id").Array()
|
||||
for nextPageToken != "" {
|
||||
next := cmd.ExecNoErr(t, "--page-size", "2", "--page-token", nextPageToken)
|
||||
actualIDs = append(actualIDs, gjson.Get(next, "identities.#.id").Array()...)
|
||||
nextPageToken = gjson.Get(next, "next_page_token").String()
|
||||
}
|
||||
|
||||
actualIDsString := make([]string, len(actualIDs))
|
||||
for i, id := range actualIDs {
|
||||
actualIDsString[i] = id.Str
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, ids, actualIDsString)
|
||||
})
|
||||
}
|
||||
15
cmd/identities/stubs/identity.schema.json
Normal file
15
cmd/identities/stubs/identity.schema.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traits": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"testKey": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
cmd/identities/validate.go
Normal file
152
cmd/identities/validate.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ory/kratos/spec"
|
||||
|
||||
"github.com/ory/x/jsonschemax"
|
||||
|
||||
"github.com/ory/x/cmdx"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
"github.com/ory/kratos/cmd/cliclient"
|
||||
)
|
||||
|
||||
func NewValidateCmd() *cobra.Command {
|
||||
var cmd = &cobra.Command{
|
||||
Use: "validate",
|
||||
Short: "Validate resources",
|
||||
}
|
||||
cmd.AddCommand(NewValidateIdentityCmd())
|
||||
cliclient.RegisterClientFlags(cmd.PersistentFlags())
|
||||
cmdx.RegisterFormatFlags(cmd.PersistentFlags())
|
||||
return cmd
|
||||
|
||||
}
|
||||
|
||||
func NewValidateIdentityCmd() *cobra.Command {
|
||||
var c = &cobra.Command{
|
||||
Use: "identity file.json [file-2.json] [file-3.json] [file-n.json]",
|
||||
Short: "Validate local identity files",
|
||||
Long: `This command allows validation of identity files.
|
||||
It validates against the payload of the API and the identity schema as configured in Ory Kratos.
|
||||
Identities can be supplied via STD_IN or JSON files containing a single or an array of identities.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := cliclient.NewClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
is, err := readIdentities(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for src, i := range is {
|
||||
err = ValidateIdentity(cmd, src, i, func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error) {
|
||||
return c.IdentityAPI.GetIdentitySchema(ctx, id).Execute()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "All identity files are valid.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
var schemas = make(map[string]*jsonschema.Schema)
|
||||
|
||||
const createIdentityPath = "api.json#/components/schemas/createIdentityBody"
|
||||
|
||||
type SchemaGetter = func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error)
|
||||
|
||||
// ValidateIdentity validates the json payload fc against
|
||||
// 1. the swagger payload definition and
|
||||
// 2. the remote custom identity schema.
|
||||
func ValidateIdentity(cmd *cobra.Command, src, i string, getRemoteSchema SchemaGetter) error {
|
||||
swaggerSchema, ok := schemas[createIdentityPath]
|
||||
if !ok {
|
||||
// add swagger schema
|
||||
schemaCompiler := jsonschema.NewCompiler()
|
||||
err := schemaCompiler.AddResource("api.json", bytes.NewReader(spec.API))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Could not add swagger schema to the schema compiler. This is an error with the binary you use and should be reported. Thanks ;)")
|
||||
}
|
||||
|
||||
// compile swagger payload definition
|
||||
swaggerSchema, err = schemaCompiler.Compile(cmd.Context(), createIdentityPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Could not compile the identity schema. This is an error with the binary you use and should be reported. Thanks ;)")
|
||||
}
|
||||
// force additional properties to false because swagger does not render this
|
||||
swaggerSchema.AdditionalProperties = false
|
||||
schemas[createIdentityPath] = swaggerSchema
|
||||
}
|
||||
|
||||
// validate against swagger definition
|
||||
var foundValidationErrors bool
|
||||
err := swaggerSchema.Validate(bytes.NewBufferString(i))
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: not valid\n", src)
|
||||
jsonschemax.FormatValidationErrorForCLI(cmd.ErrOrStderr(), []byte(i), err)
|
||||
foundValidationErrors = true
|
||||
}
|
||||
|
||||
// get custom identity schema id
|
||||
sid := gjson.Get(i, "schema_id")
|
||||
if !sid.Exists() {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), `%s: Expected key "schema_id" to be defined in identity file`, src)
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
|
||||
customSchema, ok := schemas[sid.String()]
|
||||
if !ok {
|
||||
ts, _, err := getRemoteSchema(cmd.Context(), sid.String())
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not fetch schema with ID \"%s\": %s\n", src, sid.String(), err)
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
sf, err := json.Marshal(ts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("%s: Could not marshal the traits schema. This usually means there is a problem with your upstream service as it served an invalid response.", src))
|
||||
}
|
||||
|
||||
// compile custom identity schema
|
||||
customSchema, err = jsonschema.CompileString(cmd.Context(), "identity_traits.schema.json", string(sf))
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not compile the traits schema: %s\n", src, err)
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
schemas[sid.String()] = customSchema
|
||||
}
|
||||
|
||||
// validate against custom identity schema
|
||||
err = customSchema.Validate(bytes.NewBufferString(i))
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: not valid\n", src)
|
||||
jsonschemax.FormatValidationErrorForCLI(cmd.ErrOrStderr(), []byte(i), err)
|
||||
foundValidationErrors = true
|
||||
}
|
||||
|
||||
if foundValidationErrors {
|
||||
return cmdx.FailSilently(cmd)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
78
cmd/identities/validate_test.go
Normal file
78
cmd/identities/validate_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identities_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/cmd/identities"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateIdentity(t *testing.T) {
|
||||
var testCmd = func() (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
|
||||
out, err := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
cmd := &cobra.Command{}
|
||||
cmd.SetOut(out)
|
||||
cmd.SetErr(err)
|
||||
return cmd, out, err
|
||||
}
|
||||
|
||||
var testSchemaGetter = func(resp map[string]interface{}) identities.SchemaGetter {
|
||||
return func(_ context.Context, _ string) (map[string]interface{}, *http.Response, error) {
|
||||
return resp, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
for i, tc := range []struct {
|
||||
description string
|
||||
payload string
|
||||
expectedRequiredError string
|
||||
identitySchema map[string]interface{}
|
||||
}{
|
||||
{
|
||||
description: "requires schema_id from swagger schema",
|
||||
payload: "{}",
|
||||
expectedRequiredError: "schema_id",
|
||||
},
|
||||
{
|
||||
description: "requires traits from swagger schema",
|
||||
payload: `{"schema_id": "case2"}`,
|
||||
expectedRequiredError: "traits",
|
||||
},
|
||||
{
|
||||
description: "requires custom trait key",
|
||||
payload: `{"schema_id": "case3", "traits": {}}`,
|
||||
expectedRequiredError: "random-property",
|
||||
identitySchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"required": []string{"random-property"},
|
||||
"properties": map[string]interface{}{
|
||||
"random-property": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("case=%d/description=%s", i, tc.description), func(t *testing.T) {
|
||||
cmd, stdOut, stdErr := testCmd()
|
||||
|
||||
if tc.identitySchema == nil {
|
||||
tc.identitySchema = map[string]interface{}{}
|
||||
}
|
||||
err := identities.ValidateIdentity(cmd, "test identity", tc.payload, testSchemaGetter(tc.identitySchema))
|
||||
assert.Error(t, err, stdOut.String(), stdErr.String())
|
||||
assert.Len(t, stdOut.String(), 0, stdErr.String())
|
||||
assert.Contains(t, stdErr.String(), "required", stdOut.String())
|
||||
assert.Contains(t, stdErr.String(), tc.expectedRequiredError, stdOut.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user