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

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

View 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
View 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
},
}
}

View 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
View 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
View 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
View 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
}

View 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
View 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), &params)
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
},
}
}

View 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
View 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
}

View 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)
})
}

View 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
View 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
}

View 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())
})
}
}