Skip to content

Commit f777fd1

Browse files
authored
feat: update clients from files through the CLI (#3874)
1 parent 3164970 commit f777fd1

8 files changed

+141
-25
lines changed

.golangci.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ linters:
88
- goimports
99
disable:
1010
- ineffassign
11-
- deadcode
1211
- unused
13-
- structcheck
1412

15-
run:
16-
skip-files:
13+
issues:
14+
exclude-files:
1715
- ".+_test.go"
1816
- ".+_test_.+.go"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"client_name": "updated through file from disk",
3+
"client_secret_expires_at": 0,
4+
"client_uri": "",
5+
"grant_types": [
6+
"implicit"
7+
],
8+
"jwks": {},
9+
"logo_uri": "",
10+
"metadata": {},
11+
"owner": "",
12+
"policy_uri": "",
13+
"request_object_signing_alg": "RS256",
14+
"response_types": [
15+
"code"
16+
],
17+
"scope": "offline_access offline openid",
18+
"skip_consent": false,
19+
"skip_logout_consent": false,
20+
"subject_type": "public",
21+
"token_endpoint_auth_method": "client_secret_basic",
22+
"tos_uri": "",
23+
"userinfo_signed_response_alg": "none"
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"client_name": "updated through file stdin",
3+
"client_secret_expires_at": 0,
4+
"client_uri": "",
5+
"grant_types": [
6+
"implicit"
7+
],
8+
"jwks": {},
9+
"logo_uri": "",
10+
"metadata": {},
11+
"owner": "",
12+
"policy_uri": "",
13+
"request_object_signing_alg": "RS256",
14+
"response_types": [
15+
"code"
16+
],
17+
"scope": "offline_access offline openid",
18+
"skip_consent": false,
19+
"skip_logout_consent": false,
20+
"subject_type": "public",
21+
"token_endpoint_auth_method": "client_secret_basic",
22+
"tos_uri": "",
23+
"userinfo_signed_response_alg": "none"
24+
}

cmd/cmd_create_client.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
)
1818

1919
const (
20+
flagFile = "file"
21+
2022
flagClientAccessTokenStrategy = "access-token-strategy"
2123
flagClientAllowedCORSOrigin = "allowed-cors-origin"
2224
flagClientAudience = "audience"
@@ -87,7 +89,10 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" +
8789
}
8890

8991
secret := flagx.MustGetString(cmd, flagClientSecret)
90-
cl := clientFromFlags(cmd)
92+
cl, err := clientFromFlags(cmd)
93+
if err != nil {
94+
return err
95+
}
9196
cl.ClientId = pointerx.Ptr(flagx.MustGetString(cmd, flagClientId))
9297

9398
//nolint:bodyclose

cmd/cmd_helper_client.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package cmd
55

66
import (
77
"encoding/json"
8+
"fmt"
9+
"os"
810
"strings"
911

1012
"github.com/spf13/cobra"
@@ -16,7 +18,24 @@ import (
1618
"github.com/ory/x/pointerx"
1719
)
1820

19-
func clientFromFlags(cmd *cobra.Command) hydra.OAuth2Client {
21+
func clientFromFlags(cmd *cobra.Command) (hydra.OAuth2Client, error) {
22+
if filename := flagx.MustGetString(cmd, flagFile); filename != "" {
23+
src := cmd.InOrStdin()
24+
if filename != "-" {
25+
f, err := os.Open(filename)
26+
if err != nil {
27+
return hydra.OAuth2Client{}, fmt.Errorf("unable to open file %q: %w", filename, err)
28+
}
29+
defer f.Close()
30+
src = f
31+
}
32+
client := hydra.OAuth2Client{}
33+
if err := json.NewDecoder(src).Decode(&client); err != nil {
34+
return hydra.OAuth2Client{}, fmt.Errorf("unable to decode JSON: %w", err)
35+
}
36+
return client, nil
37+
}
38+
2039
return hydra.OAuth2Client{
2140
AccessTokenStrategy: pointerx.Ptr(flagx.MustGetString(cmd, flagClientAccessTokenStrategy)),
2241
AllowedCorsOrigins: flagx.MustGetStringSlice(cmd, flagClientAllowedCORSOrigin),
@@ -47,7 +66,7 @@ func clientFromFlags(cmd *cobra.Command) hydra.OAuth2Client {
4766
SubjectType: pointerx.Ptr(flagx.MustGetString(cmd, flagClientSubjectType)),
4867
TokenEndpointAuthMethod: pointerx.Ptr(flagx.MustGetString(cmd, flagClientTokenEndpointAuthMethod)),
4968
TosUri: pointerx.Ptr(flagx.MustGetString(cmd, flagClientTOSURI)),
50-
}
69+
}, nil
5170
}
5271

5372
func registerEncryptFlags(flags *pflag.FlagSet) {
@@ -58,6 +77,8 @@ func registerEncryptFlags(flags *pflag.FlagSet) {
5877
}
5978

6079
func registerClientFlags(flags *pflag.FlagSet) {
80+
flags.String(flagFile, "", "Read a JSON file representing a client from this location. If set, the other client flags are ignored.")
81+
6182
flags.String(flagClientMetadata, "{}", "Metadata is an arbitrary JSON String of your choosing.")
6283
flags.String(flagClientOwner, "", "The owner of this client, typically email addresses or a user ID.")
6384
flags.StringSlice(flagClientContact, nil, "A list representing ways to contact people responsible for this client, typically email addresses.")

cmd/cmd_import_client_test.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"encoding/json"
1010
"os"
11+
"path/filepath"
1112
"testing"
1213

1314
"github.com/stretchr/testify/assert"
@@ -23,23 +24,21 @@ import (
2324

2425
func writeTempFile(t *testing.T, contents interface{}) string {
2526
t.Helper()
26-
ij, err := json.Marshal(contents)
27-
require.NoError(t, err)
28-
f, err := os.CreateTemp(t.TempDir(), "")
29-
require.NoError(t, err)
30-
_, err = f.Write(ij)
27+
fn := filepath.Join(t.TempDir(), "content.json")
28+
f, err := os.Create(fn)
3129
require.NoError(t, err)
30+
require.NoError(t, json.NewEncoder(f).Encode(contents))
3231
require.NoError(t, f.Close())
33-
return f.Name()
32+
return fn
3433
}
3534

3635
func TestImportClient(t *testing.T) {
3736
ctx := context.Background()
3837
c := cmd.NewImportClientCmd()
3938
reg := setup(t, c)
4039

41-
file1 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.String("foo")}, {Scope: pointerx.String("bar"), ClientSecret: pointerx.String("some-secret")}})
42-
file2 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.String("baz")}, {Scope: pointerx.String("zab"), ClientSecret: pointerx.String("some-secret")}})
40+
file1 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.Ptr("foo")}, {Scope: pointerx.Ptr("bar"), ClientSecret: pointerx.Ptr("some-secret")}})
41+
file2 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.Ptr("baz")}, {Scope: pointerx.Ptr("zab"), ClientSecret: pointerx.Ptr("some-secret")}})
4342

4443
t.Run("case=imports clients from single file", func(t *testing.T) {
4544
actual := gjson.Parse(cmdx.ExecNoErr(t, c, file1))
@@ -77,7 +76,7 @@ func TestImportClient(t *testing.T) {
7776

7877
t.Run("case=imports clients from multiple files and stdin", func(t *testing.T) {
7978
var stdin bytes.Buffer
80-
require.NoError(t, json.NewEncoder(&stdin).Encode([]hydra.OAuth2Client{{Scope: pointerx.String("oof")}, {Scope: pointerx.String("rab"), ClientSecret: pointerx.String("some-secret")}}))
79+
require.NoError(t, json.NewEncoder(&stdin).Encode([]hydra.OAuth2Client{{Scope: pointerx.Ptr("oof")}, {Scope: pointerx.Ptr("rab"), ClientSecret: pointerx.Ptr("some-secret")}}))
8180

8281
stdout, _, err := cmdx.Exec(t, c, &stdin, file1, file2)
8382
require.NoError(t, err)
@@ -93,7 +92,7 @@ func TestImportClient(t *testing.T) {
9392
})
9493

9594
t.Run("case=performs appropriate error reporting", func(t *testing.T) {
96-
file3 := writeTempFile(t, []hydra.OAuth2Client{{ClientSecret: pointerx.String("short")}})
95+
file3 := writeTempFile(t, []hydra.OAuth2Client{{ClientSecret: pointerx.Ptr("short")}})
9796
stdout, stderr, err := cmdx.Exec(t, c, nil, file1, file3)
9897
require.Error(t, err)
9998
actual := gjson.Parse(stdout)

cmd/cmd_update_client.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,18 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" +
4242
}
4343

4444
id := args[0]
45-
cc := clientFromFlags(cmd)
45+
cc, err := clientFromFlags(cmd)
46+
if err != nil {
47+
return err
48+
}
4649

4750
client, _, err := m.OAuth2API.SetOAuth2Client(context.Background(), id).OAuth2Client(cc).Execute() //nolint:bodyclose
4851
if err != nil {
4952
return cmdx.PrintOpenAPIError(cmd, err)
5053
}
5154

5255
if client.ClientSecret == nil && len(secret) > 0 {
53-
client.ClientSecret = pointerx.String(secret)
56+
client.ClientSecret = pointerx.Ptr(secret)
5457
}
5558

5659
if encryptSecret && client.ClientSecret != nil {
@@ -60,7 +63,7 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" +
6063
return cmdx.FailSilently(cmd)
6164
}
6265

63-
client.ClientSecret = pointerx.String(enc.Base64Encode())
66+
client.ClientSecret = pointerx.Ptr(enc.Base64Encode())
6467
}
6568

6669
cmdx.PrintRow(cmd, (*outputOAuth2Client)(client))

cmd/cmd_update_client_test.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
package cmd_test
55

66
import (
7+
"bytes"
78
"context"
89
"encoding/json"
910
"testing"
1011

12+
"github.com/tidwall/sjson"
13+
1114
"github.com/stretchr/testify/assert"
1215
"github.com/stretchr/testify/require"
1316
"github.com/tidwall/gjson"
@@ -25,11 +28,11 @@ func TestUpdateClient(t *testing.T) {
2528
original := createClient(t, reg, nil)
2629
t.Run("case=creates successfully", func(t *testing.T) {
2730
actual := gjson.Parse(cmdx.ExecNoErr(t, c, "--grant-type", "implicit", original.GetID()))
28-
expected, err := reg.ClientManager().GetClient(ctx, actual.Get("client_id").String())
31+
expected, err := reg.ClientManager().GetClient(ctx, actual.Get("client_id").Str)
2932
require.NoError(t, err)
3033

31-
assert.Equal(t, expected.GetID(), actual.Get("client_id").String())
32-
assert.Equal(t, "implicit", actual.Get("grant_types").Array()[0].String())
34+
assert.Equal(t, expected.GetID(), actual.Get("client_id").Str)
35+
assert.Equal(t, "implicit", actual.Get("grant_types").Array()[0].Str)
3336
snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...)
3437
})
3538

@@ -39,9 +42,48 @@ func TestUpdateClient(t *testing.T) {
3942
"--secret", "some-userset-secret",
4043
"--pgp-key", base64EncodedPGPPublicKey(t),
4144
))
42-
assert.NotEmpty(t, actual.Get("client_id").String())
43-
assert.NotEmpty(t, actual.Get("client_secret").String())
45+
assert.Equal(t, original.ID, actual.Get("client_id").Str)
46+
assert.NotEmpty(t, actual.Get("client_secret").Str)
47+
assert.NotEqual(t, original.Secret, actual.Get("client_secret").Str)
4448

4549
snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...)
4650
})
51+
52+
t.Run("case=updates from file", func(t *testing.T) {
53+
original, err := reg.ClientManager().GetConcreteClient(ctx, original.GetID())
54+
require.NoError(t, err)
55+
56+
raw, err := json.Marshal(original)
57+
require.NoError(t, err)
58+
59+
t.Run("file=stdin", func(t *testing.T) {
60+
raw, err = sjson.SetBytes(raw, "client_name", "updated through file stdin")
61+
require.NoError(t, err)
62+
63+
stdout, stderr, err := cmdx.Exec(t, c, bytes.NewReader(raw), original.GetID(), "--file", "-")
64+
require.NoError(t, err, stderr)
65+
66+
actual := gjson.Parse(stdout)
67+
assert.Equal(t, original.ID, actual.Get("client_id").Str)
68+
assert.Equal(t, "updated through file stdin", actual.Get("client_name").Str)
69+
70+
snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...)
71+
})
72+
73+
t.Run("file=from disk", func(t *testing.T) {
74+
raw, err = sjson.SetBytes(raw, "client_name", "updated through file from disk")
75+
require.NoError(t, err)
76+
77+
fn := writeTempFile(t, json.RawMessage(raw))
78+
79+
stdout, stderr, err := cmdx.Exec(t, c, nil, original.GetID(), "--file", fn)
80+
require.NoError(t, err, stderr)
81+
82+
actual := gjson.Parse(stdout)
83+
assert.Equal(t, original.ID, actual.Get("client_id").Str)
84+
assert.Equal(t, "updated through file from disk", actual.Get("client_name").Str)
85+
86+
snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...)
87+
})
88+
})
4789
}

0 commit comments

Comments
 (0)