From f777fd1ac1047575624924d9243f6e532af4f538 Mon Sep 17 00:00:00 2001 From: Patrik Date: Mon, 4 Nov 2024 15:04:21 +0100 Subject: [PATCH] feat: update clients from files through the CLI (#3874) --- .golangci.yml | 6 +-- ...case=updates_from_file-file=from_disk.json | 24 +++++++++ ...ent-case=updates_from_file-file=stdin.json | 24 +++++++++ cmd/cmd_create_client.go | 7 ++- cmd/cmd_helper_client.go | 25 ++++++++- cmd/cmd_import_client_test.go | 19 ++++--- cmd/cmd_update_client.go | 9 ++-- cmd/cmd_update_client_test.go | 52 +++++++++++++++++-- 8 files changed, 141 insertions(+), 25 deletions(-) create mode 100644 cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=from_disk.json create mode 100644 cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=stdin.json diff --git a/.golangci.yml b/.golangci.yml index 00ee1f9963c..2dff48664e4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,11 +8,9 @@ linters: - goimports disable: - ineffassign - - deadcode - unused - - structcheck -run: - skip-files: +issues: + exclude-files: - ".+_test.go" - ".+_test_.+.go" diff --git a/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=from_disk.json b/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=from_disk.json new file mode 100644 index 00000000000..e9a42532a2f --- /dev/null +++ b/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=from_disk.json @@ -0,0 +1,24 @@ +{ + "client_name": "updated through file from disk", + "client_secret_expires_at": 0, + "client_uri": "", + "grant_types": [ + "implicit" + ], + "jwks": {}, + "logo_uri": "", + "metadata": {}, + "owner": "", + "policy_uri": "", + "request_object_signing_alg": "RS256", + "response_types": [ + "code" + ], + "scope": "offline_access offline openid", + "skip_consent": false, + "skip_logout_consent": false, + "subject_type": "public", + "token_endpoint_auth_method": "client_secret_basic", + "tos_uri": "", + "userinfo_signed_response_alg": "none" +} diff --git a/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=stdin.json b/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=stdin.json new file mode 100644 index 00000000000..4491f0eed55 --- /dev/null +++ b/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=stdin.json @@ -0,0 +1,24 @@ +{ + "client_name": "updated through file stdin", + "client_secret_expires_at": 0, + "client_uri": "", + "grant_types": [ + "implicit" + ], + "jwks": {}, + "logo_uri": "", + "metadata": {}, + "owner": "", + "policy_uri": "", + "request_object_signing_alg": "RS256", + "response_types": [ + "code" + ], + "scope": "offline_access offline openid", + "skip_consent": false, + "skip_logout_consent": false, + "subject_type": "public", + "token_endpoint_auth_method": "client_secret_basic", + "tos_uri": "", + "userinfo_signed_response_alg": "none" +} diff --git a/cmd/cmd_create_client.go b/cmd/cmd_create_client.go index 4c3105283ce..4d72b22f1ab 100644 --- a/cmd/cmd_create_client.go +++ b/cmd/cmd_create_client.go @@ -17,6 +17,8 @@ import ( ) const ( + flagFile = "file" + flagClientAccessTokenStrategy = "access-token-strategy" flagClientAllowedCORSOrigin = "allowed-cors-origin" flagClientAudience = "audience" @@ -87,7 +89,10 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" + } secret := flagx.MustGetString(cmd, flagClientSecret) - cl := clientFromFlags(cmd) + cl, err := clientFromFlags(cmd) + if err != nil { + return err + } cl.ClientId = pointerx.Ptr(flagx.MustGetString(cmd, flagClientId)) //nolint:bodyclose diff --git a/cmd/cmd_helper_client.go b/cmd/cmd_helper_client.go index 70fa6f98bcc..ca28fafa12a 100644 --- a/cmd/cmd_helper_client.go +++ b/cmd/cmd_helper_client.go @@ -5,6 +5,8 @@ package cmd import ( "encoding/json" + "fmt" + "os" "strings" "github.com/spf13/cobra" @@ -16,7 +18,24 @@ import ( "github.com/ory/x/pointerx" ) -func clientFromFlags(cmd *cobra.Command) hydra.OAuth2Client { +func clientFromFlags(cmd *cobra.Command) (hydra.OAuth2Client, error) { + if filename := flagx.MustGetString(cmd, flagFile); filename != "" { + src := cmd.InOrStdin() + if filename != "-" { + f, err := os.Open(filename) + if err != nil { + return hydra.OAuth2Client{}, fmt.Errorf("unable to open file %q: %w", filename, err) + } + defer f.Close() + src = f + } + client := hydra.OAuth2Client{} + if err := json.NewDecoder(src).Decode(&client); err != nil { + return hydra.OAuth2Client{}, fmt.Errorf("unable to decode JSON: %w", err) + } + return client, nil + } + return hydra.OAuth2Client{ AccessTokenStrategy: pointerx.Ptr(flagx.MustGetString(cmd, flagClientAccessTokenStrategy)), AllowedCorsOrigins: flagx.MustGetStringSlice(cmd, flagClientAllowedCORSOrigin), @@ -47,7 +66,7 @@ func clientFromFlags(cmd *cobra.Command) hydra.OAuth2Client { SubjectType: pointerx.Ptr(flagx.MustGetString(cmd, flagClientSubjectType)), TokenEndpointAuthMethod: pointerx.Ptr(flagx.MustGetString(cmd, flagClientTokenEndpointAuthMethod)), TosUri: pointerx.Ptr(flagx.MustGetString(cmd, flagClientTOSURI)), - } + }, nil } func registerEncryptFlags(flags *pflag.FlagSet) { @@ -58,6 +77,8 @@ func registerEncryptFlags(flags *pflag.FlagSet) { } func registerClientFlags(flags *pflag.FlagSet) { + flags.String(flagFile, "", "Read a JSON file representing a client from this location. If set, the other client flags are ignored.") + flags.String(flagClientMetadata, "{}", "Metadata is an arbitrary JSON String of your choosing.") flags.String(flagClientOwner, "", "The owner of this client, typically email addresses or a user ID.") flags.StringSlice(flagClientContact, nil, "A list representing ways to contact people responsible for this client, typically email addresses.") diff --git a/cmd/cmd_import_client_test.go b/cmd/cmd_import_client_test.go index 9e32dd2907f..cc82b1aa69e 100644 --- a/cmd/cmd_import_client_test.go +++ b/cmd/cmd_import_client_test.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -23,14 +24,12 @@ import ( func writeTempFile(t *testing.T, contents interface{}) string { t.Helper() - ij, err := json.Marshal(contents) - require.NoError(t, err) - f, err := os.CreateTemp(t.TempDir(), "") - require.NoError(t, err) - _, err = f.Write(ij) + fn := filepath.Join(t.TempDir(), "content.json") + f, err := os.Create(fn) require.NoError(t, err) + require.NoError(t, json.NewEncoder(f).Encode(contents)) require.NoError(t, f.Close()) - return f.Name() + return fn } func TestImportClient(t *testing.T) { @@ -38,8 +37,8 @@ func TestImportClient(t *testing.T) { c := cmd.NewImportClientCmd() reg := setup(t, c) - file1 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.String("foo")}, {Scope: pointerx.String("bar"), ClientSecret: pointerx.String("some-secret")}}) - file2 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.String("baz")}, {Scope: pointerx.String("zab"), ClientSecret: pointerx.String("some-secret")}}) + file1 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.Ptr("foo")}, {Scope: pointerx.Ptr("bar"), ClientSecret: pointerx.Ptr("some-secret")}}) + file2 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.Ptr("baz")}, {Scope: pointerx.Ptr("zab"), ClientSecret: pointerx.Ptr("some-secret")}}) t.Run("case=imports clients from single file", func(t *testing.T) { actual := gjson.Parse(cmdx.ExecNoErr(t, c, file1)) @@ -77,7 +76,7 @@ func TestImportClient(t *testing.T) { t.Run("case=imports clients from multiple files and stdin", func(t *testing.T) { var stdin bytes.Buffer - require.NoError(t, json.NewEncoder(&stdin).Encode([]hydra.OAuth2Client{{Scope: pointerx.String("oof")}, {Scope: pointerx.String("rab"), ClientSecret: pointerx.String("some-secret")}})) + require.NoError(t, json.NewEncoder(&stdin).Encode([]hydra.OAuth2Client{{Scope: pointerx.Ptr("oof")}, {Scope: pointerx.Ptr("rab"), ClientSecret: pointerx.Ptr("some-secret")}})) stdout, _, err := cmdx.Exec(t, c, &stdin, file1, file2) require.NoError(t, err) @@ -93,7 +92,7 @@ func TestImportClient(t *testing.T) { }) t.Run("case=performs appropriate error reporting", func(t *testing.T) { - file3 := writeTempFile(t, []hydra.OAuth2Client{{ClientSecret: pointerx.String("short")}}) + file3 := writeTempFile(t, []hydra.OAuth2Client{{ClientSecret: pointerx.Ptr("short")}}) stdout, stderr, err := cmdx.Exec(t, c, nil, file1, file3) require.Error(t, err) actual := gjson.Parse(stdout) diff --git a/cmd/cmd_update_client.go b/cmd/cmd_update_client.go index 19c6a096da0..6205b21ad71 100644 --- a/cmd/cmd_update_client.go +++ b/cmd/cmd_update_client.go @@ -42,7 +42,10 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" + } id := args[0] - cc := clientFromFlags(cmd) + cc, err := clientFromFlags(cmd) + if err != nil { + return err + } client, _, err := m.OAuth2API.SetOAuth2Client(context.Background(), id).OAuth2Client(cc).Execute() //nolint:bodyclose if err != nil { @@ -50,7 +53,7 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" + } if client.ClientSecret == nil && len(secret) > 0 { - client.ClientSecret = pointerx.String(secret) + client.ClientSecret = pointerx.Ptr(secret) } if encryptSecret && client.ClientSecret != nil { @@ -60,7 +63,7 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" + return cmdx.FailSilently(cmd) } - client.ClientSecret = pointerx.String(enc.Base64Encode()) + client.ClientSecret = pointerx.Ptr(enc.Base64Encode()) } cmdx.PrintRow(cmd, (*outputOAuth2Client)(client)) diff --git a/cmd/cmd_update_client_test.go b/cmd/cmd_update_client_test.go index c21aa0277bc..6cbcb7dfe5f 100644 --- a/cmd/cmd_update_client_test.go +++ b/cmd/cmd_update_client_test.go @@ -4,10 +4,13 @@ package cmd_test import ( + "bytes" "context" "encoding/json" "testing" + "github.com/tidwall/sjson" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -25,11 +28,11 @@ func TestUpdateClient(t *testing.T) { original := createClient(t, reg, nil) t.Run("case=creates successfully", func(t *testing.T) { actual := gjson.Parse(cmdx.ExecNoErr(t, c, "--grant-type", "implicit", original.GetID())) - expected, err := reg.ClientManager().GetClient(ctx, actual.Get("client_id").String()) + expected, err := reg.ClientManager().GetClient(ctx, actual.Get("client_id").Str) require.NoError(t, err) - assert.Equal(t, expected.GetID(), actual.Get("client_id").String()) - assert.Equal(t, "implicit", actual.Get("grant_types").Array()[0].String()) + assert.Equal(t, expected.GetID(), actual.Get("client_id").Str) + assert.Equal(t, "implicit", actual.Get("grant_types").Array()[0].Str) snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...) }) @@ -39,9 +42,48 @@ func TestUpdateClient(t *testing.T) { "--secret", "some-userset-secret", "--pgp-key", base64EncodedPGPPublicKey(t), )) - assert.NotEmpty(t, actual.Get("client_id").String()) - assert.NotEmpty(t, actual.Get("client_secret").String()) + assert.Equal(t, original.ID, actual.Get("client_id").Str) + assert.NotEmpty(t, actual.Get("client_secret").Str) + assert.NotEqual(t, original.Secret, actual.Get("client_secret").Str) snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...) }) + + t.Run("case=updates from file", func(t *testing.T) { + original, err := reg.ClientManager().GetConcreteClient(ctx, original.GetID()) + require.NoError(t, err) + + raw, err := json.Marshal(original) + require.NoError(t, err) + + t.Run("file=stdin", func(t *testing.T) { + raw, err = sjson.SetBytes(raw, "client_name", "updated through file stdin") + require.NoError(t, err) + + stdout, stderr, err := cmdx.Exec(t, c, bytes.NewReader(raw), original.GetID(), "--file", "-") + require.NoError(t, err, stderr) + + actual := gjson.Parse(stdout) + assert.Equal(t, original.ID, actual.Get("client_id").Str) + assert.Equal(t, "updated through file stdin", actual.Get("client_name").Str) + + snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...) + }) + + t.Run("file=from disk", func(t *testing.T) { + raw, err = sjson.SetBytes(raw, "client_name", "updated through file from disk") + require.NoError(t, err) + + fn := writeTempFile(t, json.RawMessage(raw)) + + stdout, stderr, err := cmdx.Exec(t, c, nil, original.GetID(), "--file", fn) + require.NoError(t, err, stderr) + + actual := gjson.Parse(stdout) + assert.Equal(t, original.ID, actual.Get("client_id").Str) + assert.Equal(t, "updated through file from disk", actual.Get("client_name").Str) + + snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...) + }) + }) }