diff --git a/cmd/cliclient/identity.go b/cmd/cliclient/identity.go deleted file mode 100644 index 12e17ee712e..00000000000 --- a/cmd/cliclient/identity.go +++ /dev/null @@ -1,44 +0,0 @@ -package cliclient - -import ( - "github.com/spf13/cobra" - - "github.com/ory/x/cmdx" -) - -type IdentityClient struct{} - -func NewIdentityClient() *IdentityClient { - return new(IdentityClient) -} - -func (ic *IdentityClient) Import(cmd *cobra.Command, args []string) { - cmdx.MinArgs(cmd, args, 1) - // - // for _, p := range args { - // var is []models.Identity - // f, err := os.Open(p) - // cmdx.Must(err, "Unable to open file %s: %s", p, err) - // d := json.NewDecoder(f) - // d.DisallowUnknownFields() - // err = d.Decode(&is) - // cmdx.Must(err, "Unable to decode file %s to JSON: %s", p, err) - // - // for _, i := range is { - // _, err := c(cmd, "endpoint").Admin.UpsertIdentity( - // admin.NewUpsertIdentityParams().WithBody(&i), - // ) - // cmdx.Must(err, "Unable to import identity: %s", err) - // } - // } -} - -func (ic *IdentityClient) List(cmd *cobra.Command, args []string) { - cmdx.ExactArgs(cmd, args, 2) - //ic.c.Admin - -} - -func (ic *IdentityClient) Get(cmd *cobra.Command, args []string) { - -} diff --git a/cmd/identities/delete_test.go b/cmd/identities/delete_test.go index 62405b46c42..e57b99cadca 100644 --- a/cmd/identities/delete_test.go +++ b/cmd/identities/delete_test.go @@ -11,7 +11,6 @@ import ( "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" - "github.com/ory/kratos/internal/clihelpers" "github.com/ory/kratos/x" "github.com/ory/x/sqlcon" ) @@ -48,11 +47,8 @@ func TestDeleteCmd(t *testing.T) { }) t.Run("case=fails with unknown ID", func(t *testing.T) { - stdOut, stdErr, err := exec(deleteCmd, nil, x.NewUUID().String()) - require.True(t, errors.Is(err, clihelpers.NoPrintButFailError)) + stdErr := execErr(t, deleteCmd, x.NewUUID().String()) - // expect ID and no error - assert.Len(t, stdOut, 0, stdErr) - assert.Contains(t, stdErr, "[DELETE /identities/{id}][404] deleteIdentityNotFound", stdOut) + assert.Contains(t, stdErr, "[DELETE /identities/{id}][404] deleteIdentityNotFound", stdErr) }) } diff --git a/cmd/identities/get_test.go b/cmd/identities/get_test.go index dda4195c837..045e6db54d6 100644 --- a/cmd/identities/get_test.go +++ b/cmd/identities/get_test.go @@ -3,10 +3,8 @@ package identities import ( "context" "encoding/json" - "errors" "testing" - "github.com/ory/kratos/internal/clihelpers" "github.com/ory/kratos/x" "github.com/stretchr/testify/assert" @@ -43,10 +41,8 @@ func TestGetCmd(t *testing.T) { }) t.Run("case=fails with unknown ID", func(t *testing.T) { - stdOut, stdErr, err := exec(getCmd, nil, x.NewUUID().String()) - require.True(t, errors.Is(err, clihelpers.NoPrintButFailError)) + stdErr := execErr(t, getCmd, x.NewUUID().String()) - assert.Len(t, stdOut, 0, stdErr) - assert.Contains(t, stdErr, "status 404", stdOut) + assert.Contains(t, stdErr, "status 404", stdErr) }) } diff --git a/cmd/identities/definitions_test.go b/cmd/identities/helpers.go similarity index 65% rename from cmd/identities/definitions_test.go rename to cmd/identities/helpers.go index 050d11f4749..f6dd833fedb 100644 --- a/cmd/identities/definitions_test.go +++ b/cmd/identities/helpers.go @@ -3,8 +3,11 @@ package identities import ( "bytes" "context" - "errors" + "fmt" + "github.com/pkg/errors" + "github.com/tidwall/gjson" "io" + "io/ioutil" "testing" "github.com/ory/kratos/identity" @@ -21,6 +24,44 @@ import ( "github.com/ory/viper" ) +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 := ioutil.ReadAll(cmd.InOrStdin()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "STD_IN: Could not read: %s\n", err) + return nil, clihelpers.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 := ioutil.ReadFile(fn) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not open identity file: %s\n", fn, err) + return nil, clihelpers.FailSilently(cmd) + } + for i, id := range parseIdentities(fc) { + rawIdentities[fmt.Sprintf("%s[%d]", fn, i)] = id + } + } + return rawIdentities, nil +} + func setup(t *testing.T, cmd *cobra.Command) driver.Registry { _, reg := internal.NewRegistryDefaultWithDSN(t, configuration.DefaultSQLiteMemoryDSN) _, admin := testhelpers.NewKratosServerWithCSRF(t, reg) diff --git a/cmd/identities/import.go b/cmd/identities/import.go index 831aa4711a5..63504c17491 100644 --- a/cmd/identities/import.go +++ b/cmd/identities/import.go @@ -1,13 +1,9 @@ package identities import ( - "bytes" "context" "encoding/json" "fmt" - "io/ioutil" - "time" - "github.com/spf13/cobra" "github.com/ory/kratos/internal/clihelpers" @@ -27,7 +23,7 @@ var importCmd = &cobra.Command{ cat file.json | kratos identities import -Files are expected to each contain a single identity. The validity of files can be tested beforehand using "... identities validate". +Files can contain only a single or an array of identities. The validity of files can be tested beforehand using "... identities validate". WARNING: Importing credentials is not yet supported.`, RunE: func(cmd *cobra.Command, args []string) error { @@ -36,20 +32,19 @@ WARNING: Importing credentials is not yet supported.`, imported := make([]*models.Identity, 0, len(args)) failed := make(map[string]error) - if len(args) == 0 { - fc, err := ioutil.ReadAll(cmd.InOrStdin()) - if err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "STD_IN: Could not read: %s\n", err) - return clihelpers.FailSilently(cmd) - } + is, err := readIdentities(cmd, args) + if err != nil { + return err + } - err = validateIdentity(cmd, "STD_IN", fc, c.Common.GetSchema) + for src, i := range is { + err = validateIdentity(cmd, src, i, c.Common.GetSchema) if err != nil { return err } var params models.CreateIdentity - err = json.NewDecoder(bytes.NewBuffer(fc)).Decode(¶ms) + err = json.Unmarshal([]byte(i), ¶ms) if err != nil { _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "STD_IN: Could not parse identity") return clihelpers.FailSilently(cmd) @@ -59,36 +54,9 @@ WARNING: Importing credentials is not yet supported.`, Body: ¶ms, Context: context.Background(), }) - if err != nil { - failed["STD_IN"] = err + failed[src] = err } else { - imported = append(imported, resp.Payload) - } - } else { - for _, fn := range args { - fc, err := validateIdentityFile(cmd, fn, c) - if err != nil { - return err - } - - var params models.CreateIdentity - err = json.Unmarshal(fc, ¶ms) - if err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not parse identity file\n", fn) - return clihelpers.FailSilently(cmd) - } - - resp, err := c.Admin.CreateIdentity( - admin.NewCreateIdentityParams(). - WithBody(¶ms). - WithTimeout(time.Second)) - - if err != nil { - failed[fn] = err - continue - } - imported = append(imported, resp.Payload) } } diff --git a/cmd/identities/import_test.go b/cmd/identities/import_test.go index d9acbe14bc8..852050f22c8 100644 --- a/cmd/identities/import_test.go +++ b/cmd/identities/import_test.go @@ -43,7 +43,67 @@ func TestImportCmd(t *testing.T) { assert.NoError(t, err) }) - t.Run("case=imports a new identity from stdIn", func(t *testing.T) { + t.Run("case=imports multiple identities from single file", func(t *testing.T) { + i := []models.CreateIdentity{ + { + SchemaID: pointerx.String(configuration.DefaultIdentityTraitsSchemaID), + Traits: map[string]interface{}{}, + }, + { + SchemaID: pointerx.String(configuration.DefaultIdentityTraitsSchemaID), + Traits: map[string]interface{}{}, + }, + } + ij, err := json.Marshal(i) + require.NoError(t, err) + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + _, err = f.Write(ij) + require.NoError(t, err) + require.NoError(t, f.Close()) + + stdOut := execNoErr(t, importCmd, f.Name()) + + id, err := uuid.FromString(gjson.Get(stdOut, "0.id").String()) + require.NoError(t, err) + _, err = reg.Persister().GetIdentity(context.Background(), id) + 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) + assert.NoError(t, err) + }) + + t.Run("case=imports a new identity from STD_IN", func(t *testing.T) { + i := []models.CreateIdentity{ + { + SchemaID: pointerx.String(configuration.DefaultIdentityTraitsSchemaID), + Traits: map[string]interface{}{}, + }, + { + SchemaID: pointerx.String(configuration.DefaultIdentityTraitsSchemaID), + Traits: map[string]interface{}{}, + }, + } + ij, err := json.Marshal(i) + require.NoError(t, err) + + stdOut, stdErr, err := exec(importCmd, bytes.NewBuffer(ij)) + require.NoError(t, err, stdOut, stdErr) + + id, err := uuid.FromString(gjson.Get(stdOut, "0.id").String()) + require.NoError(t, err) + _, err = reg.Persister().GetIdentity(context.Background(), id) + 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) + assert.NoError(t, err) + }) + + t.Run("case=imports multiple identities from STD_IN", func(t *testing.T) { i := models.CreateIdentity{ SchemaID: pointerx.String(configuration.DefaultIdentityTraitsSchemaID), Traits: map[string]interface{}{}, @@ -64,7 +124,7 @@ func TestImportCmd(t *testing.T) { // validation is further tested with the validate command stdOut, stdErr, err := exec(importCmd, bytes.NewBufferString("{}")) assert.True(t, errors.Is(err, clihelpers.NoPrintButFailError)) - assert.Contains(t, stdErr, "STD_IN: not valid") + assert.Contains(t, stdErr, "STD_IN[0]: not valid") assert.Len(t, stdOut, 0) }) } diff --git a/cmd/identities/list_test.go b/cmd/identities/list_test.go index f71d84bc991..e264dbc7c69 100644 --- a/cmd/identities/list_test.go +++ b/cmd/identities/list_test.go @@ -40,8 +40,6 @@ func TestListCmd(t *testing.T) { stdoutP1 := execNoErr(t, listCmd, "1", "3") stdoutP2 := execNoErr(t, listCmd, "2", "3") - t.Logf(stdoutP1, stdoutP2, strings.Join(ids, "\n")) - for _, id := range ids { // exactly one of page 1 and 2 should contain the id assert.True(t, strings.Contains(stdoutP1, id) != strings.Contains(stdoutP2, id)) diff --git a/cmd/identities/validate.go b/cmd/identities/validate.go index 694965c90d3..69dde3bd865 100644 --- a/cmd/identities/validate.go +++ b/cmd/identities/validate.go @@ -5,8 +5,6 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" - "github.com/markbates/pkger" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -16,7 +14,6 @@ import ( "github.com/ory/jsonschema/v3" "github.com/ory/kratos/cmd/cliclient" - "github.com/ory/kratos/internal/httpclient/client" "github.com/ory/kratos/internal/httpclient/client/common" "github.com/ory/x/viperx" ) @@ -24,13 +21,21 @@ import ( var validateCmd = &cobra.Command{ Use: "validate ", Short: "Validate local identity files", - Args: cobra.MinimumNArgs(1), + Long: `This command allows validation of identity files. +It validates against the payload of the API and the identity schema as configured in 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 := cliclient.NewClient(cmd) - for _, fn := range args { - if _, err := validateIdentityFile(cmd, fn, c); err != nil { - cmd.SilenceUsage = true + is, err := readIdentities(cmd, args) + if err != nil { + return err + } + + for src, i := range is { + err = validateIdentity(cmd, src, i, c.Common.GetSchema) + if err != nil { return err } } @@ -40,16 +45,6 @@ var validateCmd = &cobra.Command{ }, } -func validateIdentityFile(cmd *cobra.Command, fn string, c *client.OryKratos) ([]byte, error) { - fc, err := ioutil.ReadFile(fn) - if err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: Could not open identity file: %s\n", fn, err) - return nil, clihelpers.FailSilently(cmd) - } - - return fc, validateIdentity(cmd, fn, fc, c.Common.GetSchema) -} - var schemas = make(map[string]*jsonschema.Schema) const createIdentityPath = "api.swagger.json#/definitions/CreateIdentity" @@ -59,7 +54,7 @@ type schemaGetter = func(params *common.GetSchemaParams) (*common.GetSchemaOK, e // 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 string, fc []byte, getRemoteSchema schemaGetter) error { +func validateIdentity(cmd *cobra.Command, src, i string, getRemoteSchema schemaGetter) error { swaggerSchema, ok := schemas[createIdentityPath] if !ok { // get swagger schema @@ -87,7 +82,7 @@ func validateIdentity(cmd *cobra.Command, src string, fc []byte, getRemoteSchema // validate against swagger definition var foundValidationErrors bool - err := swaggerSchema.Validate(bytes.NewBuffer(fc)) + err := swaggerSchema.Validate(bytes.NewBufferString(i)) if err != nil { _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: not valid\n", src) viperx.PrintHumanReadableValidationErrors(cmd.ErrOrStderr(), err) @@ -95,7 +90,7 @@ func validateIdentity(cmd *cobra.Command, src string, fc []byte, getRemoteSchema } // get custom identity schema id - sid := gjson.GetBytes(fc, "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 clihelpers.FailSilently(cmd) @@ -124,7 +119,7 @@ func validateIdentity(cmd *cobra.Command, src string, fc []byte, getRemoteSchema } // validate against custom identity schema - err = customSchema.Validate(bytes.NewBuffer(fc)) + err = customSchema.Validate(bytes.NewBufferString(i)) if err != nil { _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: not valid\n", src) viperx.PrintHumanReadableValidationErrors(cmd.ErrOrStderr(), err) diff --git a/cmd/identities/validate_test.go b/cmd/identities/validate_test.go index 20ec3f18be7..ce95f67d540 100644 --- a/cmd/identities/validate_test.go +++ b/cmd/identities/validate_test.go @@ -58,12 +58,11 @@ func TestValidateIdentity(t *testing.T) { } { t.Run(fmt.Sprintf("case=%d/description=%s", i, tc.description), func(t *testing.T) { cmd, stdOut, stdErr := testCmd() - payload := []byte(tc.payload) if tc.identitySchema == nil { tc.identitySchema = map[string]interface{}{} } - err := validateIdentity(cmd, "test identity", payload, testSchemaGetter(tc.identitySchema)) + err := 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())