Skip to content

Commit c6fa2a6

Browse files
alnrory-bot
authored andcommitted
fix: jsonx.ApplyJSONPatch
GitOrigin-RevId: 43c10801f5051e3d5fbea5f4f5e90394f6da0fbb
1 parent 86f5fc7 commit c6fa2a6

File tree

6 files changed

+78
-89
lines changed

6 files changed

+78
-89
lines changed

CHANGELOG.md

Lines changed: 2 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
55
**Table of Contents**
66

7-
- [0.0.0 (2025-07-23)](#000-2025-07-23)
7+
- [0.0.0 (2025-07-03)](#000-2025-07-03)
88
- [Breaking Changes](#breaking-changes)
99
- [Related issue(s)](#related-issues)
1010

1111
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
1212

13-
# [0.0.0](https://github.com/ory/hydra/compare/v2.3.0...v0.0.0) (2025-07-23)
13+
# [0.0.0](https://github.com/ory/hydra/compare/v2.3.0...v0.0.0) (2025-07-03)
1414
## Breaking Changes
1515

1616
This patch changes the behavior of configuration item `foo` to do bar. To keep the existing
@@ -41,10 +41,6 @@ If this pull request
4141

4242
### Bug Fixes
4343

44-
* Add repo syncing for polis ([46d17f8](https://github.com/ory/hydra/commit/46d17f8bfdc59e2185e9ce65823eb2652e01f1b8)):
45-
46-
GitOrigin-RevId: e277a25d594b512b800d39dd18f36ea3d99fcf84
47-
4844
* Allow updating when JWKS URI is set ([#3935](https://github.com/ory/hydra/issues/3935)) ([#3946](https://github.com/ory/hydra/issues/3946)) ([fb1655b](https://github.com/ory/hydra/commit/fb1655ba86077b10141132ed332ba8d6f8c70582)):
4945

5046
The client validator no longer rejects PATCH and PUT updates when `JSONWebKeysURI` is non-empty and `JSONWebKeys` is not nil.
@@ -75,10 +71,6 @@ If this pull request
7571
GitOrigin-RevId: 61645585277edd95914705499afd7211a85983eb
7672

7773
* CLI usage help examples ([#3943](https://github.com/ory/hydra/issues/3943)) ([e24f9a7](https://github.com/ory/hydra/commit/e24f9a704c22c72690bc20c498439865181d9239))
78-
* Copybara script ([7b33358](https://github.com/ory/hydra/commit/7b333585bb44a069bf47267c853aa2e91db0efa3)):
79-
80-
GitOrigin-RevId: 14665e01451ac5fcdda148b473b8fc35d4fe21ef
81-
8274
* Correct multiple instances of 'stragegy' typo ([#3906](https://github.com/ory/hydra/issues/3906)) ([50eefbc](https://github.com/ory/hydra/commit/50eefbc21c2c43d221b6079bbd78a33ef8c754c4)):
8375

8476
This commit addresses several occurrences where 'strategy' was
@@ -105,10 +97,6 @@ If this pull request
10597

10698
GitOrigin-RevId: 83312e6544bc33ccc30e1e1e414cc04910429192
10799

108-
* Include go.mod in vendored oryx ([08a3ab4](https://github.com/ory/hydra/commit/08a3ab43ddb1e2a0df430224f969fe3f0ba161bf)):
109-
110-
GitOrigin-RevId: 20365bbe6b2cf95ac7973bcca9056455d2cb3803
111-
112100
* **infrastructure:** Hydra oss CI ([e846541](https://github.com/ory/hydra/commit/e84654185cdfffbf160d5309f744795d15d723f9)):
113101

114102
GitOrigin-RevId: 3df0724e5ea4c81a0f4c481c1a3a34529356d073
@@ -157,14 +145,6 @@ If this pull request
157145

158146
GitOrigin-RevId: 64950988a466bbdb4f25b8d9f5c416ff591c00bf
159147

160-
* Use hard-coded fallback key instead of panic ([e1f6450](https://github.com/ory/hydra/commit/e1f645012f43f62928fdc79710b45d935878367f)):
161-
162-
GitOrigin-RevId: d7a2270bbf5360288199e9632b2eac6cbc29737c
163-
164-
* Use main branch for polis ([6c24e68](https://github.com/ory/hydra/commit/6c24e68995b8eae9ba0b8872867270ef1d35113b)):
165-
166-
GitOrigin-RevId: 04533493184c6abdc3a211daffd98f6b68e1c9cc
167-
168148
* Using uuid_generate_v4 function ([#3958](https://github.com/ory/hydra/issues/3958)) ([c206066](https://github.com/ory/hydra/commit/c20606606654af975e5d82998956bb998acee576)):
169149

170150
Removing the md5 function for the uuid generation with native pgsql
@@ -173,13 +153,6 @@ If this pull request
173153
Closes https://github.com/ory/hydra/issues/3844
174154

175155

176-
### Code Refactoring
177-
178-
* Move database meta functions to root x folder for reusability ([7e49133](https://github.com/ory/hydra/commit/7e49133f435d6a0f74a63e8f8d03c5d314d7d3c6)):
179-
180-
GitOrigin-RevId: 30ee938ea5f1d19bac8967e0ebfe2d595ec27d2b
181-
182-
183156
### Features
184157

185158
* Add error reason to OAuth2TokenExchangeError event ([#3971](https://github.com/ory/hydra/issues/3971)) ([241dd45](https://github.com/ory/hydra/commit/241dd45fa17ed10d1101d890199df47dab4dbce5))
@@ -254,10 +227,6 @@ If this pull request
254227

255228
GitOrigin-RevId: dbb48d171fad1f9b4fd31385f0ef4fb01e39e823
256229

257-
* Move config testhelpers to ory/x ([3a4ba08](https://github.com/ory/hydra/commit/3a4ba084c74cf49a521856f150a8a2c6f3c1aa25)):
258-
259-
GitOrigin-RevId: fd484445e9715760231f7f86ec212d094e826377
260-
261230
* Revoke Kratos session asynchronously ([#3936](https://github.com/ory/hydra/issues/3936)) ([a0e7ee2](https://github.com/ory/hydra/commit/a0e7ee29298d4f882a7d471e0601b01c6848c40d)):
262231

263232
This change makes the session revocation in Kratos async to improve
@@ -337,18 +306,10 @@ If this pull request
337306
POST admin/oauth2/auth/sessions/consent?consent_challenge_id=G_TIM3XABG14UwIgDoT1DRfipjhC1uix
338307
```
339308

340-
* Use stdlib HTTP router in Kratos ([8f81931](https://github.com/ory/hydra/commit/8f8193179a39dc142d502fbc559891ffa0385ed8)):
341-
342-
GitOrigin-RevId: 799513e99acbf43a05fe3113ffda45d2fff2a9e0
343-
344309
* Use vendored jackson ([a0a9062](https://github.com/ory/hydra/commit/a0a906211bce4ced3e1f4324eb9d287ef10892a6)):
345310

346311
GitOrigin-RevId: 591238768218ba2b5af93f91ac7e16f4c170da5b
347312

348-
* Use vendored ory/x ([6581e01](https://github.com/ory/hydra/commit/6581e01679b2e146433061cbaaebb80a0e3905b5)):
349-
350-
GitOrigin-RevId: 994f3b754946ca5b2bd1bab0fe20532f5d5ab62f
351-
352313

353314
### Performance Improvements
354315

@@ -359,19 +320,8 @@ If this pull request
359320

360321
### Tests
361322

362-
* **hydra:** Clean oauth2 session setup ([699e382](https://github.com/ory/hydra/commit/699e38238220f857148b503dfb96d9b057bb4583)):
363-
364-
GitOrigin-RevId: e05097c7439096cf40fdcf059b3396970b2f1219
365-
366323
* Parallelize and improve ([#3989](https://github.com/ory/hydra/issues/3989)) ([a47e395](https://github.com/ory/hydra/commit/a47e39513f1f08076849f77517977abffa195364))
367324

368-
### Unclassified
369-
370-
* Merge 3834fab8c161a7dc98d43f32acf8efd9e6e95352 into 4dae0f4a8785eb36d8dbb27137f6b924c1e0f0b5 ([dc84053](https://github.com/ory/hydra/commit/dc840535c19d58caf130966e00d3d2fe9f3eb577)):
371-
372-
GitOrigin-RevId: 0c2dcaa065b64d2aafbcfd49c79363a214c5b2aa
373-
374-
375325

376326
# [2.3.0](https://github.com/ory/hydra/compare/v2.2.0...v2.3.0) (2025-01-17)
377327

client/handler.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -428,15 +428,16 @@ func (h *Handler) patchOAuth2Client(w http.ResponseWriter, r *http.Request, ps h
428428
}
429429

430430
id := ps.ByName("id")
431-
c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id)
431+
client, err := h.r.ClientManager().GetConcreteClient(r.Context(), id)
432432
if err != nil {
433433
h.r.Writer().WriteError(w, r, err)
434434
return
435435
}
436436

437-
oldSecret := c.Secret
437+
oldSecret := client.Secret
438438

439-
if err := jsonx.ApplyJSONPatch(patchJSON, c, "/id"); err != nil {
439+
client, err = jsonx.ApplyJSONPatch(patchJSON, client, "/id")
440+
if err != nil {
440441
h.r.Writer().WriteError(w, r, err)
441442
return
442443
}
@@ -445,16 +446,16 @@ func (h *Handler) patchOAuth2Client(w http.ResponseWriter, r *http.Request, ps h
445446
// GetConcreteClient returns a client with the hashed secret, however updateClient expects
446447
// an empty secret if the secret hasn't changed. As such we need to check if the patch has
447448
// updated the secret or not
448-
if oldSecret == c.Secret {
449-
c.Secret = ""
449+
if oldSecret == client.Secret {
450+
client.Secret = ""
450451
}
451452

452-
if err := h.updateClient(r.Context(), c, h.r.ClientValidator().Validate); err != nil {
453+
if err := h.updateClient(r.Context(), client, h.r.ClientValidator().Validate); err != nil {
453454
h.r.Writer().WriteError(w, r, err)
454455
return
455456
}
456457

457-
h.r.Writer().Write(w, r, c)
458+
h.r.Writer().Write(w, r, client)
458459
}
459460

460461
// Paginated OAuth2 Client List Response
@@ -573,7 +574,7 @@ type adminGetOAuth2Client struct {
573574
// 200: oAuth2Client
574575
// default: errorOAuth2Default
575576
func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
576-
var id = ps.ByName("id")
577+
id := ps.ByName("id")
577578
c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id)
578579
if err != nil {
579580
h.r.Writer().WriteError(w, r, err)
@@ -683,7 +684,7 @@ type deleteOAuth2Client struct {
683684
// 204: emptyResponse
684685
// default: genericError
685686
func (h *Handler) deleteOAuth2Client(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
686-
var id = ps.ByName("id")
687+
id := ps.ByName("id")
687688
if err := h.r.ClientManager().DeleteClient(r.Context(), id); err != nil {
688689
h.r.Writer().WriteError(w, r, err)
689690
return
@@ -723,7 +724,7 @@ type setOAuth2ClientLifespans struct {
723724
// 200: oAuth2Client
724725
// default: genericError
725726
func (h *Handler) setOAuth2ClientLifespans(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
726-
var id = ps.ByName("id")
727+
id := ps.ByName("id")
727728
c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id)
728729
if err != nil {
729730
h.r.Writer().WriteError(w, r, err)

client/sdk_test.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@ package client_test
55

66
import (
77
"context"
8+
"net/http"
89
"net/http/httptest"
910
"strings"
1011
"testing"
1112

1213
"github.com/mohae/deepcopy"
1314
"github.com/stretchr/testify/assert"
1415
"github.com/stretchr/testify/require"
16+
goauth2 "golang.org/x/oauth2"
17+
"golang.org/x/oauth2/clientcredentials"
1518

1619
hydra "github.com/ory/hydra-client-go/v2"
1720
"github.com/ory/hydra/v2/client"
1821
"github.com/ory/hydra/v2/driver/config"
1922
"github.com/ory/hydra/v2/internal/testhelpers"
23+
"github.com/ory/hydra/v2/oauth2"
2024
"github.com/ory/hydra/v2/x"
2125
"github.com/ory/x/assertx"
2226
"github.com/ory/x/contextx"
@@ -64,11 +68,19 @@ func TestClientSDK(t *testing.T) {
6468

6569
routerAdmin := x.NewRouterAdmin(conf.AdminURL)
6670
routerPublic := x.NewRouterPublic()
67-
handler := client.NewHandler(r)
68-
handler.SetPublicRoutes(routerPublic)
69-
handler.SetAdminRoutes(routerAdmin)
71+
clHandler := client.NewHandler(r)
72+
clHandler.SetPublicRoutes(routerPublic)
73+
clHandler.SetAdminRoutes(routerAdmin)
74+
o2Handler := oauth2.NewHandler(r, conf)
75+
o2Handler.SetPublicRoutes(routerPublic, func(h http.Handler) http.Handler { return h })
76+
o2Handler.SetAdminRoutes(routerAdmin)
77+
7078
server := httptest.NewServer(routerAdmin)
79+
t.Cleanup(server.Close)
80+
publicServer := httptest.NewServer(routerPublic)
81+
t.Cleanup(publicServer.Close)
7182
conf.MustSet(ctx, config.KeyAdminURL, server.URL)
83+
conf.MustSet(ctx, config.KeyOAuth2TokenURL, publicServer.URL+"/oauth2/token")
7284

7385
c := hydra.NewAPIClient(hydra.NewConfiguration())
7486
c.GetConfig().Servers = hydra.ServerConfigurations{{URL: server.URL}}
@@ -210,22 +222,44 @@ func TestClientSDK(t *testing.T) {
210222
})
211223

212224
t.Run("case=patch should not alter secret if not requested", func(t *testing.T) {
213-
op := "replace"
214-
path := "/client_uri"
215-
value := "http://foo.bar"
225+
created, _, err := c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(createTestClient("")).Execute()
226+
require.NoError(t, err)
227+
require.Equal(t, "secret", *created.ClientSecret)
216228

217-
client := createTestClient("")
218-
created, _, err := c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(client).Execute()
229+
cc := clientcredentials.Config{
230+
ClientID: *created.ClientId,
231+
ClientSecret: "secret",
232+
TokenURL: conf.OAuth2TokenURL(t.Context()).String(),
233+
AuthStyle: goauth2.AuthStyleInHeader,
234+
}
235+
token, err := cc.Token(t.Context())
219236
require.NoError(t, err)
220-
client.ClientId = created.ClientId
237+
require.NotZero(t, token.AccessToken)
238+
239+
ignoreFields := []string{"registration_access_token", "registration_client_uri", "updated_at"}
240+
241+
patchedURI, _, err := c.OAuth2API.PatchOAuth2Client(context.Background(), *created.ClientId).JsonPatch([]hydra.JsonPatch{{Op: "replace", Path: "/client_uri", Value: "http://foo.bar"}}).Execute()
242+
require.NoError(t, err)
243+
require.Nil(t, patchedURI.ClientSecret, "client secret should not be returned in the response if it wasn't changed")
244+
assertx.EqualAsJSONExcept(t, created, patchedURI, append(ignoreFields, "client_uri", "client_secret"), "client unchanged except client_uri; client_secret should not be returned")
221245

222-
result1, _, err := c.OAuth2API.PatchOAuth2Client(context.Background(), *client.ClientId).JsonPatch([]hydra.JsonPatch{{Op: op, Path: path, Value: value}}).Execute()
246+
token2, err := cc.Token(t.Context())
223247
require.NoError(t, err)
224-
result2, _, err := c.OAuth2API.PatchOAuth2Client(context.Background(), *client.ClientId).JsonPatch([]hydra.JsonPatch{{Op: op, Path: path, Value: value}}).Execute()
248+
require.NotZero(t, token2.AccessToken)
249+
require.NotEqual(t, token.AccessToken, token2.AccessToken, "Got a new token after patching, with unchanged secret")
250+
251+
patchedSecret, _, err := c.OAuth2API.PatchOAuth2Client(context.Background(), *created.ClientId).JsonPatch([]hydra.JsonPatch{{Op: "replace", Path: "/client_secret", Value: "newsecret"}}).Execute()
225252
require.NoError(t, err)
253+
require.Equal(t, "newsecret", *patchedSecret.ClientSecret, "client secret should be returned if it was changed")
254+
assertx.EqualAsJSONExcept(t, patchedURI, patchedSecret, append(ignoreFields, "client_secret"), "client unchanged except secret")
255+
256+
_, err = cc.Token(t.Context())
257+
require.ErrorContains(t, err, "Client authentication failed", "should not be able to get a token with the old secret")
226258

227-
// secret hashes shouldn't change between these PUT calls
228-
require.Equal(t, result1.ClientSecret, result2.ClientSecret)
259+
cc.ClientSecret = "newsecret"
260+
token3, err := cc.Token(t.Context())
261+
require.NoError(t, err, "should be able to get a token with the new secret")
262+
require.NotZero(t, token3.AccessToken, "Got a new token after patching with new secret")
229263
})
230264

231265
t.Run("case=patch client that has JSONWebKeysURI", func(t *testing.T) {

oryx/jsonx/debug.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package jsonx
66
import (
77
"encoding/json"
88
"fmt"
9+
"slices"
910
)
1011

1112
// Anonymize takes a JSON byte array and anonymizes its content by
@@ -30,7 +31,7 @@ func Anonymize(data []byte, except ...string) []byte {
3031

3132
func anonymize(obj map[string]any, except ...string) {
3233
for k, v := range obj {
33-
if k == "schemas" || k == "id" {
34+
if slices.Contains(except, k) {
3435
continue
3536
}
3637

oryx/jsonx/patch.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
jsonpatch "github.com/evanphx/json-patch/v5"
1313
"github.com/gobwas/glob"
14+
"github.com/pkg/errors"
1415

1516
"github.com/ory/x/pointerx"
1617
)
@@ -43,33 +44,34 @@ func isElementAccess(path string) bool {
4344
return false
4445
}
4546

46-
// ApplyJSONPatch applies a JSON patch to an object. It returns an error if the
47-
// patch is invalid or if the patch includes paths that are denied. denyPaths is
48-
// a list of path globs (interpreted with [glob.Compile] that are not allowed to
47+
// ApplyJSONPatch applies a JSON patch to an object and returns the modified
48+
// object. The original object is not modified. It returns an error if the patch
49+
// is invalid or if the patch includes paths that are denied. denyPaths is a
50+
// list of path globs (interpreted with [glob.Compile] that are not allowed to
4951
// be patched.
50-
func ApplyJSONPatch(p json.RawMessage, object interface{}, denyPaths ...string) error {
52+
func ApplyJSONPatch[T any](p json.RawMessage, object T, denyPaths ...string) (result T, err error) {
5153
patch, err := jsonpatch.DecodePatch(p)
5254
if err != nil {
53-
return err
55+
return result, errors.WithStack(err)
5456
}
5557

5658
denyPattern := fmt.Sprintf("{%s}", strings.ToLower(strings.Join(denyPaths, ",")))
5759
matcher, err := glob.Compile(denyPattern, '/')
5860
if err != nil {
59-
return err
61+
return result, errors.WithStack(err)
6062
}
6163

6264
for _, op := range patch {
6365
// Some operations are buggy, see https://github.com/evanphx/json-patch/pull/158
6466
if isUnsupported(op) {
65-
return fmt.Errorf("unsupported operation: %s", op.Kind())
67+
return result, errors.Errorf("unsupported operation: %s", op.Kind())
6668
}
6769
path, err := op.Path()
6870
if err != nil {
69-
return fmt.Errorf("error parsing patch operations: %v", err)
71+
return result, errors.Errorf("error parsing patch operations: %v", err)
7072
}
7173
if matcher.Match(strings.ToLower(path)) {
72-
return fmt.Errorf("patch includes denied path: %s", path)
74+
return result, errors.Errorf("patch includes denied path: %s", path)
7375
}
7476

7577
// JSON patch officially rejects replacing paths that don't exist, but we want to be more tolerant.
@@ -81,16 +83,17 @@ func ApplyJSONPatch(p json.RawMessage, object interface{}, denyPaths ...string)
8183

8284
original, err := json.Marshal(object)
8385
if err != nil {
84-
return err
86+
return result, errors.WithStack(err)
8587
}
8688

8789
options := jsonpatch.NewApplyOptions()
8890
options.EnsurePathExistsOnAdd = true
8991

9092
modified, err := patch.ApplyWithOptions(original, options)
9193
if err != nil {
92-
return err
94+
return result, errors.WithStack(err)
9395
}
9496

95-
return json.Unmarshal(modified, object)
97+
err = json.Unmarshal(modified, &result)
98+
return result, errors.WithStack(err)
9699
}

oryx/snapshotx/snapshot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func SnapshotT(t *testing.T, actual interface{}, except ...ExceptOpt) {
9393
).SnapshotT(t, compare)
9494
}
9595

96-
// SnapshotTExcept
96+
// SnapshotTExcept is deprecated in favor of SnapshotT with ExceptOpt.
9797
//
9898
// DEPRECATED: please use SnapshotT instead
9999
func SnapshotTExcept(t *testing.T, actual interface{}, except []string) {

0 commit comments

Comments
 (0)