Skip to content

Commit

Permalink
feat: add API to batch insert identities (#3157)
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl authored Mar 29, 2023
1 parent b69981a commit 829bda7
Show file tree
Hide file tree
Showing 48 changed files with 3,407 additions and 183 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
GOGC: 100
with:
args: --timeout 10m0s
version: v1.49.0
version: v1.50.1
skip-go-installation: true
skip-pkg-cache: true
- name: Build Kratos
Expand Down
2 changes: 1 addition & 1 deletion driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ func (s Schemas) FindSchemaByID(id string) (*Schema, error) {
return nil, errors.Errorf("unable to find identity schema with id: %s", id)
}

func MustNew(t *testing.T, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) *Config {
func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) *Config {
p, err := New(context.TODO(), l, stdOutOrErr, opts...)
require.NoError(t, err)
return p
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"credentials": {
"password": {
"type": "password",
"version": 0
}
},
"metadata_admin": {
"admin-0": "admin"
},
"metadata_public": {
"public-0": "public"
},
"schema_id": "multiple_emails",
"state": "active",
"traits": {
"emails": [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]"
],
"username": "[email protected]"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"credentials": {
"password": {
"type": "password",
"version": 0
}
},
"metadata_admin": {
"admin-1": "admin"
},
"metadata_public": {
"public-1": "public"
},
"schema_id": "multiple_emails",
"state": "active",
"traits": {
"emails": [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]"
],
"username": "[email protected]"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"credentials": {
"password": {
"type": "password",
"version": 0
}
},
"metadata_admin": {
"admin-2": "admin"
},
"metadata_public": {
"public-2": "public"
},
"schema_id": "multiple_emails",
"state": "active",
"traits": {
"emails": [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]"
],
"username": "[email protected]"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"credentials": {
"password": {
"type": "password",
"version": 0
}
},
"metadata_admin": {
"admin-3": "admin"
},
"metadata_public": {
"public-3": "public"
},
"schema_id": "multiple_emails",
"state": "active",
"traits": {
"emails": [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]"
],
"username": "[email protected]"
}
}
13 changes: 12 additions & 1 deletion identity/extension_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,21 @@ func (r *SchemaExtensionVerification) Run(ctx jsonschema.ValidationContext, s sc
}

func (r *SchemaExtensionVerification) Finish() error {
r.i.VerifiableAddresses = r.v
r.i.VerifiableAddresses = merge(r.v, r.i.VerifiableAddresses)
return nil
}

// merge merges the base with the overrides through comparison with `has`. It changes the base slice in place.
func merge(base []VerifiableAddress, overrides []VerifiableAddress) []VerifiableAddress {
for i := range base {
if override := has(overrides, &base[i]); override != nil {
base[i] = *override
}
}

return base
}

func (r *SchemaExtensionVerification) appendAddress(address *VerifiableAddress) {
if h := has(r.i.VerifiableAddresses, address); h != nil {
if has(r.v, address) == nil {
Expand Down
52 changes: 52 additions & 0 deletions identity/extension_verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,55 @@ func mustContainAddress(t *testing.T, expected, actual []VerifiableAddress) {
assert.True(t, found, "%+v not in %+v", act, expected)
}
}

func TestMergeVerifiableAddresses(t *testing.T) {
for _, tt := range []struct {
name string
base, overrides, expected []VerifiableAddress
}{
{
name: "empty base",
base: []VerifiableAddress{},
overrides: []VerifiableAddress{{
Value: "[email protected]",
Via: "email",
}},
expected: []VerifiableAddress{},
}, {
name: "no overlap",
base: []VerifiableAddress{{
Value: "[email protected]",
Via: "email",
}},
overrides: []VerifiableAddress{{
Value: "[email protected]",
Via: "email",
}},
expected: []VerifiableAddress{{
Value: "[email protected]",
Via: "email",
}},
}, {
name: "overrides",
base: []VerifiableAddress{
{Value: "[email protected]", Via: "email"},
{Value: "[email protected]", Via: "email"},
{Value: "[email protected]", Via: "no match"},
},
overrides: []VerifiableAddress{
{Value: "[email protected]", Via: "email", Verified: true, Status: VerifiableAddressStatusCompleted},
{Value: "[email protected]", Via: "wrong via"},
{Value: "[email protected]", Via: "email"},
},
expected: []VerifiableAddress{
{Value: "[email protected]", Via: "email"},
{Value: "[email protected]", Via: "email", Verified: true, Status: VerifiableAddressStatusCompleted},
{Value: "[email protected]", Via: "no match"},
},
},
} {
t.Run("case="+tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, merge(tt.base, tt.overrides))
})
}
}
124 changes: 108 additions & 16 deletions identity/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ import (
"github.com/ory/kratos/driver/config"
)

const RouteCollection = "/identities"
const RouteItem = RouteCollection + "/:id"
const RouteCredentialItem = RouteItem + "/credentials/:type"
const (
RouteCollection = "/identities"
RouteItem = RouteCollection + "/:id"
RouteCredentialItem = RouteItem + "/credentials/:type"

BatchPatchIdentitiesLimit = 2000
)

type (
handlerDependencies interface {
Expand Down Expand Up @@ -98,6 +102,7 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) {
admin.PATCH(RouteItem, h.patch)

admin.POST(RouteCollection, h.create)
admin.PATCH(RouteCollection, h.batchPatchIdentities)
admin.PUT(RouteItem, h.update)

admin.DELETE(RouteCredentialItem, h.deleteIdentityCredentials)
Expand Down Expand Up @@ -404,12 +409,33 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
return
}

i, err := h.identityFromCreateIdentityBody(r.Context(), &cr)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

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

h.r.Writer().WriteCreated(w, r,
urlx.AppendPaths(
h.r.Config().SelfAdminURL(r.Context()),
"identities",
i.ID.String(),
).String(),
WithCredentialsMetadataAndAdminMetadataInJSON(*i),
)
}

func (h *Handler) identityFromCreateIdentityBody(ctx context.Context, cr *CreateIdentityBody) (*Identity, error) {
stateChangedAt := sqlxx.NullTime(time.Now())
state := StateActive
if cr.State != "" {
if err := cr.State.IsValid(); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("%s", err).WithWrap(err)))
return
return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("%s", err).WithWrap(err))
}
state = cr.State
}
Expand All @@ -425,24 +451,90 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
MetadataPublic: []byte(cr.MetadataPublic),
}

if err := h.importCredentials(r.Context(), i, cr.Credentials); err != nil {
h.r.Writer().WriteError(w, r, err)
if err := h.importCredentials(ctx, i, cr.Credentials); err != nil {
return nil, err
}

return i, nil
}

// swagger:route PATCH /admin/identities identity batchPatchIdentities
//
// # Create and deletes multiple identities
//
// Creates or delete multiple
// [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).
// This endpoint can also be used to [import
// credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)
// for instance passwords, social sign in configurations or multifactor methods.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 200: batchPatchIdentitiesResponse
// 400: errorGeneric
// 409: errorGeneric
// default: errorGeneric
func (h *Handler) batchPatchIdentities(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var (
req BatchPatchIdentitiesBody
res batchPatchIdentitiesResponse
)
if err := jsonx.NewStrictDecoder(r.Body).Decode(&req); err != nil {
h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, errors.WithStack(err))
return
}

if err := h.r.IdentityManager().Create(r.Context(), i); err != nil {
if len(req.Identities) > BatchPatchIdentitiesLimit {
h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest,
errors.WithStack(herodot.ErrBadRequest.WithReasonf(
"The maximum number of identities that can be created or deleted at once is %d.",
BatchPatchIdentitiesLimit)))
return
}

res.Identities = make([]*BatchIdentityPatchResponse, len(req.Identities))
// Array to look up the index of the identity in the identities array.
indexInIdentities := make([]*int, len(req.Identities))
identities := make([]*Identity, 0, len(req.Identities))

for i, patch := range req.Identities {
if patch.Create != nil {
res.Identities[i] = &BatchIdentityPatchResponse{
Action: ActionCreate,
PatchID: patch.ID,
}
identity, err := h.identityFromCreateIdentityBody(r.Context(), patch.Create)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
identities = append(identities, identity)
idx := len(identities) - 1
indexInIdentities[i] = &idx
}
}

if err := h.r.IdentityManager().CreateIdentities(r.Context(), identities); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
for resIdx, identitiesIdx := range indexInIdentities {
if identitiesIdx != nil {
res.Identities[resIdx].IdentityID = &identities[*identitiesIdx].ID
}
}

h.r.Writer().WriteCreated(w, r,
urlx.AppendPaths(
h.r.Config().SelfAdminURL(r.Context()),
"identities",
i.ID.String(),
).String(),
WithCredentialsMetadataAndAdminMetadataInJSON(*i),
)
h.r.Writer().Write(w, r, &res)
}

// Update Identity Parameters
Expand Down
Loading

0 comments on commit 829bda7

Please sign in to comment.