Skip to content

Commit

Permalink
feat: implement partial client updates (PATCH) with JSON Patch syntax (
Browse files Browse the repository at this point in the history
…ory#2411)

Implements a new endpoint `PATCH /clients/{id}` which uses JSON Patch syntax to update an OAuth2 client partially. This removes the need to do `PUT /clients/{id}` with the full OAuth2 Client in the payload.

Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com>
  • Loading branch information
mattbonnell and aeneasr committed Apr 5, 2021
1 parent 7ba4b47 commit 540c89d
Show file tree
Hide file tree
Showing 13 changed files with 865 additions and 9 deletions.
39 changes: 39 additions & 0 deletions client/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,45 @@ type swaggerUpdateClientPayload struct {
Body Client
}

// swagger:parameters patchOAuth2Client
type swaggerPatchClientPayload struct {
// in: path
// required: true
ID string `json:"id"`

// in: body
// required: true
Body patchRequest
}

// A JSONPatch request
//
// swagger:model patchRequest
type patchRequest []patchDocument

// A JSONPatch document as defined by RFC 6902
//
// swagger:model patchDocument
type patchDocument struct {
// The operation to be performed
//
// required: true
// example: "replace"
Op string `json:"op"`

// A JSON-pointer
//
// required: true
// example: "/name"
Path string `json:"path"`

// The value to be used within the operations
Value interface{} `json:"value"`

// A JSON-pointer
From string `json:"from"`
}

// swagger:parameters listOAuth2Clients
type swaggerListClientsParameter struct {
// The maximum amount of policies returned, upper bound is 500 policies
Expand Down
70 changes: 62 additions & 8 deletions client/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
package client

import (
"context"
"encoding/json"
"io"
"net/http"
"time"

Expand Down Expand Up @@ -57,6 +59,7 @@ func (h *Handler) SetRoutes(admin *x.RouterAdmin) {
admin.POST(ClientsHandlerPath, h.Create)
admin.GET(ClientsHandlerPath+"/:id", h.Get)
admin.PUT(ClientsHandlerPath+"/:id", h.Update)
admin.PATCH(ClientsHandlerPath+"/:id", h.Patch)
admin.DELETE(ClientsHandlerPath+"/:id", h.Delete)
}

Expand Down Expand Up @@ -146,25 +149,76 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request, ps httprouter.P
return
}

var secret string
if len(c.Secret) > 0 {
secret = c.Secret
c.OutfacingID = ps.ByName("id")
if err := h.updateClient(r.Context(), &c); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

c.OutfacingID = ps.ByName("id")
if err := h.r.ClientValidator().Validate(&c); err != nil {
h.r.Writer().Write(w, r, &c)
}

// swagger:route PATCH /clients/{id} admin patchOAuth2Client
//
// Patch an OAuth 2.0 Client
//
// Patch an existing OAuth 2.0 Client. If you pass `client_secret` the secret will be updated and returned via the API. This is the only time you will be able to retrieve the client secret, so write it down and keep it safe.
//
// OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. To manage ORY Hydra, you will need an OAuth 2.0 Client as well. Make sure that this endpoint is well protected and only callable by first-party components.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2Client
// 500: genericError
func (h *Handler) Patch(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
patchJSON, err := io.ReadAll(r.Body)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

c.UpdatedAt = time.Now().UTC().Round(time.Second)
if err := h.r.ClientManager().UpdateClient(r.Context(), &c); err != nil {
id := ps.ByName("id")
c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

if err := x.ApplyJSONPatch(patchJSON, c, "/id"); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

if err := h.updateClient(r.Context(), c); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

h.r.Writer().Write(w, r, c)
}

func (h *Handler) updateClient(ctx context.Context, c *Client) error {
var secret string
if len(c.Secret) > 0 {
secret = c.Secret
}
if err := h.r.ClientValidator().Validate(c); err != nil {
return err
}

c.UpdatedAt = time.Now().UTC().Round(time.Second)
if err := h.r.ClientManager().UpdateClient(ctx, c); err != nil {
return err
}
c.Secret = secret
h.r.Writer().Write(w, r, &c)
return nil
}

// swagger:route GET /clients admin listOAuth2Clients
Expand Down
36 changes: 36 additions & 0 deletions client/sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"testing"

"github.com/go-openapi/strfmt"
"github.com/mohae/deepcopy"

"github.com/ory/x/pointerx"
"github.com/ory/x/urlx"
Expand Down Expand Up @@ -230,4 +231,39 @@ func TestClientSDK(t *testing.T) {
})
}
})
t.Run("case=patch client legally", func(t *testing.T) {
op := "add"
path := "/redirect_uris/-"
value := "http://foo.bar"

client := createTestClient("")
client.ClientID = "patch1_client"
_, err := c.Admin.CreateOAuth2Client(admin.NewCreateOAuth2ClientParams().WithBody(client))
require.NoError(t, err)

expected := deepcopy.Copy(client).(*models.OAuth2Client)
expected.RedirectUris = append(expected.RedirectUris, value)

result, err := c.Admin.PatchOAuth2Client(admin.NewPatchOAuth2ClientParams().WithID(client.ClientID).WithBody(models.PatchRequest{{Op: &op, Path: &path, Value: value}}))
require.NoError(t, err)
expected.CreatedAt = result.Payload.CreatedAt
expected.UpdatedAt = result.Payload.UpdatedAt
expected.ClientSecret = result.Payload.ClientSecret
expected.ClientSecretExpiresAt = result.Payload.ClientSecretExpiresAt
require.Equal(t, expected, result.Payload)
})

t.Run("case=patch client illegally", func(t *testing.T) {
op := "replace"
path := "/id"
value := "foo"

client := createTestClient("")
client.ClientID = "patch2_client"
_, err := c.Admin.CreateOAuth2Client(admin.NewCreateOAuth2ClientParams().WithBody(client))
require.NoError(t, err)

_, err = c.Admin.PatchOAuth2Client(admin.NewPatchOAuth2ClientParams().WithID(client.ClientID).WithBody(models.PatchRequest{{Op: &op, Path: &path, Value: value}}))
require.Error(t, err)
})
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2
require (
github.com/cenkalti/backoff/v3 v3.0.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/evanphx/json-patch v0.5.2
github.com/go-bindata/go-bindata v3.1.1+incompatible
github.com/go-openapi/errors v0.20.0
github.com/go-openapi/runtime v0.19.26
Expand Down
44 changes: 44 additions & 0 deletions internal/httpclient/client/admin/admin_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 540c89d

Please sign in to comment.