Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement partial client updates #2411

Merged
merged 18 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
aeneasr marked this conversation as resolved.
Show resolved Hide resolved

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