From 550198becf14f48bddc1f19f4c532e7413581cfe Mon Sep 17 00:00:00 2001 From: zepatrik Date: Wed, 29 May 2024 11:08:52 +0200 Subject: [PATCH] feat: allow admin to create API code recovery flows --- ..._create_recovery_code_for_identity_body.go | 37 +++++++ ..._create_recovery_code_for_identity_body.go | 37 +++++++ selfservice/flow/type.go | 8 ++ .../strategy/code/strategy_recovery.go | 9 +- .../strategy/code/strategy_recovery_admin.go | 22 +++- .../code/strategy_recovery_admin_test.go | 102 ++++++++++++------ .../strategy/link/strategy_recovery.go | 5 +- .../strategy/link/strategy_recovery_test.go | 42 +++----- spec/api.json | 3 + spec/swagger.json | 3 + 10 files changed, 194 insertions(+), 74 deletions(-) diff --git a/internal/client-go/model_create_recovery_code_for_identity_body.go b/internal/client-go/model_create_recovery_code_for_identity_body.go index 850c7e086206..2947fad34e51 100644 --- a/internal/client-go/model_create_recovery_code_for_identity_body.go +++ b/internal/client-go/model_create_recovery_code_for_identity_body.go @@ -19,6 +19,8 @@ import ( type CreateRecoveryCodeForIdentityBody struct { // Code Expires In The recovery code will expire after that amount of time has passed. Defaults to the configuration value of `selfservice.methods.code.config.lifespan`. ExpiresIn *string `json:"expires_in,omitempty"` + // The flow type can either be `api` or `browser`. + FlowType *string `json:"flow_type,omitempty"` // Identity to Recover The identity's ID you wish to recover. IdentityId string `json:"identity_id"` } @@ -73,6 +75,38 @@ func (o *CreateRecoveryCodeForIdentityBody) SetExpiresIn(v string) { o.ExpiresIn = &v } +// GetFlowType returns the FlowType field value if set, zero value otherwise. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowType() string { + if o == nil || o.FlowType == nil { + var ret string + return ret + } + return *o.FlowType +} + +// GetFlowTypeOk returns a tuple with the FlowType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowTypeOk() (*string, bool) { + if o == nil || o.FlowType == nil { + return nil, false + } + return o.FlowType, true +} + +// HasFlowType returns a boolean if a field has been set. +func (o *CreateRecoveryCodeForIdentityBody) HasFlowType() bool { + if o != nil && o.FlowType != nil { + return true + } + + return false +} + +// SetFlowType gets a reference to the given string and assigns it to the FlowType field. +func (o *CreateRecoveryCodeForIdentityBody) SetFlowType(v string) { + o.FlowType = &v +} + // GetIdentityId returns the IdentityId field value func (o *CreateRecoveryCodeForIdentityBody) GetIdentityId() string { if o == nil { @@ -102,6 +136,9 @@ func (o CreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { if o.ExpiresIn != nil { toSerialize["expires_in"] = o.ExpiresIn } + if o.FlowType != nil { + toSerialize["flow_type"] = o.FlowType + } if true { toSerialize["identity_id"] = o.IdentityId } diff --git a/internal/httpclient/model_create_recovery_code_for_identity_body.go b/internal/httpclient/model_create_recovery_code_for_identity_body.go index 850c7e086206..2947fad34e51 100644 --- a/internal/httpclient/model_create_recovery_code_for_identity_body.go +++ b/internal/httpclient/model_create_recovery_code_for_identity_body.go @@ -19,6 +19,8 @@ import ( type CreateRecoveryCodeForIdentityBody struct { // Code Expires In The recovery code will expire after that amount of time has passed. Defaults to the configuration value of `selfservice.methods.code.config.lifespan`. ExpiresIn *string `json:"expires_in,omitempty"` + // The flow type can either be `api` or `browser`. + FlowType *string `json:"flow_type,omitempty"` // Identity to Recover The identity's ID you wish to recover. IdentityId string `json:"identity_id"` } @@ -73,6 +75,38 @@ func (o *CreateRecoveryCodeForIdentityBody) SetExpiresIn(v string) { o.ExpiresIn = &v } +// GetFlowType returns the FlowType field value if set, zero value otherwise. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowType() string { + if o == nil || o.FlowType == nil { + var ret string + return ret + } + return *o.FlowType +} + +// GetFlowTypeOk returns a tuple with the FlowType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowTypeOk() (*string, bool) { + if o == nil || o.FlowType == nil { + return nil, false + } + return o.FlowType, true +} + +// HasFlowType returns a boolean if a field has been set. +func (o *CreateRecoveryCodeForIdentityBody) HasFlowType() bool { + if o != nil && o.FlowType != nil { + return true + } + + return false +} + +// SetFlowType gets a reference to the given string and assigns it to the FlowType field. +func (o *CreateRecoveryCodeForIdentityBody) SetFlowType(v string) { + o.FlowType = &v +} + // GetIdentityId returns the IdentityId field value func (o *CreateRecoveryCodeForIdentityBody) GetIdentityId() string { if o == nil { @@ -102,6 +136,9 @@ func (o CreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { if o.ExpiresIn != nil { toSerialize["expires_in"] = o.ExpiresIn } + if o.FlowType != nil { + toSerialize["flow_type"] = o.FlowType + } if true { toSerialize["identity_id"] = o.IdentityId } diff --git a/selfservice/flow/type.go b/selfservice/flow/type.go index 2f0726b0352e..4206344ed7a9 100644 --- a/selfservice/flow/type.go +++ b/selfservice/flow/type.go @@ -22,3 +22,11 @@ func (t Type) IsBrowser() bool { func (t Type) IsAPI() bool { return t == TypeAPI } + +func (t Type) Valid() bool { + switch t { + case TypeAPI, TypeBrowser: + return true + } + return false +} diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 0cbddf393325..758e81d04fd9 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -161,9 +161,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch recoveryFlow.State { - case flow.StateChooseMethod: - fallthrough - case flow.StateEmailSent: + case flow.StateChooseMethod, + flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, recoveryFlow, body) case flow.StatePassedChallenge: // was already handled, do not allow retry @@ -237,9 +236,7 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, if s.deps.Config().UseContinueWithTransitions(ctx) { switch { - case f.Type.IsAPI(): - fallthrough - case x.IsJSONRequest(r): + case f.Type.IsAPI(), x.IsJSONRequest(r): f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf)) s.deps.Writer().Write(w, r, f) default: diff --git a/selfservice/strategy/code/strategy_recovery_admin.go b/selfservice/strategy/code/strategy_recovery_admin.go index 8964682afe30..028bb811bcaa 100644 --- a/selfservice/strategy/code/strategy_recovery_admin.go +++ b/selfservice/strategy/code/strategy_recovery_admin.go @@ -73,6 +73,13 @@ type createRecoveryCodeForIdentityBody struct { // - 1m // - 1s ExpiresIn string `json:"expires_in"` + + // Flow Type + // + // The flow type for the recovery flow. Defaults to browser. + // + // required: false + FlowType *flow.Type `json:"flow_type"` } // Recovery Code for Identity @@ -149,12 +156,21 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. } } - if time.Now().Add(expiresIn).Before(time.Now()) { - s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "expires_in" must result to a future time: %s`, p.ExpiresIn))) + if expiresIn <= 0 { + s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "expires_in" must result to a future time: %s`, expiresIn))) + return + } + + flowType := flow.TypeBrowser + if p.FlowType != nil { + flowType = *p.FlowType + } + if !flowType.Valid() { + s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "flow_type" is not valid: %q`, flowType))) return } - recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) + recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flowType) if err != nil { s.deps.Writer().WriteError(w, r, err) return diff --git a/selfservice/strategy/code/strategy_recovery_admin_test.go b/selfservice/strategy/code/strategy_recovery_admin_test.go index aed7bbcbaf43..882831b06b14 100644 --- a/selfservice/strategy/code/strategy_recovery_admin_test.go +++ b/selfservice/strategy/code/strategy_recovery_admin_test.go @@ -8,8 +8,10 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/cookiejar" "net/http/httptest" "net/url" + "strings" "testing" "time" @@ -18,13 +20,14 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" - "github.com/ory/kratos/selfservice/strategy/code" + . "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/x" "github.com/ory/x/ioutilx" "github.com/ory/x/pointerx" @@ -35,6 +38,7 @@ func TestAdminStrategy(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) initViper(t, ctx, conf) + conf.MustSet(ctx, config.ViperKeyUseContinueWithTransitions, true) _ = testhelpers.NewRecoveryUIFlowEchoServer(t, reg) _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) @@ -44,14 +48,11 @@ func TestAdminStrategy(t *testing.T) { publicTS, adminTS := testhelpers.NewKratosServer(t, reg) adminSDK := testhelpers.NewSDKClient(adminTS) - createCode := func(id string, expiresIn *string) (*kratos.RecoveryCodeForIdentity, *http.Response, error) { + type createCodeParams = kratos.CreateRecoveryCodeForIdentityBody + createCode := func(params createCodeParams) (*kratos.RecoveryCodeForIdentity, *http.Response, error) { return adminSDK.IdentityApi. CreateRecoveryCodeForIdentity(context.Background()). - CreateRecoveryCodeForIdentityBody( - kratos.CreateRecoveryCodeForIdentityBody{ - IdentityId: id, - ExpiresIn: expiresIn, - }).Execute() + CreateRecoveryCodeForIdentityBody(params).Execute() } t.Run("no panic on empty body #1384", func(t *testing.T) { @@ -63,39 +64,42 @@ func TestAdminStrategy(t *testing.T) { f, err := recovery.NewFlow(reg.Config(), time.Minute, "", r, s, flow.TypeBrowser) require.NoError(t, err) require.NotPanics(t, func() { - require.Error(t, s.(*code.Strategy).HandleRecoveryError(w, r, f, nil, errors.New("test"))) + require.Error(t, s.(*Strategy).HandleRecoveryError(w, r, f, nil, errors.New("test"))) }) }) t.Run("description=should not be able to recover an account that does not exist", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), nil) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String()}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) t.Run("description=should fail on malformed expiry time", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), pointerx.String("not-a-valid-value")) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String(), ExpiresIn: pointerx.Ptr("not-a-valid-value")}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) t.Run("description=should fail on negative expiry time", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), pointerx.String("-1h")) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String(), ExpiresIn: pointerx.Ptr("-1h")}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) - submitRecoveryLink := func(t *testing.T, link string, code string) []byte { + submitRecoveryCode := func(t *testing.T, client *http.Client, link string, code string) []byte { t.Helper() - res, err := publicTS.Client().Get(link) + if client == nil { + client = publicTS.Client() + } + res, err := client.Get(link) require.NoError(t, err) body := ioutilx.MustReadAll(res.Body) action := gjson.GetBytes(body, "ui.action").String() require.NotEmpty(t, action) - res, err = publicTS.Client().PostForm(action, url.Values{ + res, err = client.PostForm(action, url.Values{ "code": {code}, }) require.NoError(t, err) @@ -104,13 +108,21 @@ func TestAdminStrategy(t *testing.T) { return ioutilx.MustReadAll(res.Body) } + assertEmailNotVerified := func(t *testing.T, email string) { + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, email) + assert.NoError(t, err) + assert.False(t, addr.Verified) + assert.Nil(t, addr.VerifiedAt) + assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + } + t.Run("description=should create code without email", func(t *testing.T) { id := identity.Identity{Traits: identity.Traits(`{}`)} require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), nil) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String()}) require.NoError(t, err) require.NotEmpty(t, code.RecoveryLink) @@ -119,8 +131,14 @@ func TestAdminStrategy(t *testing.T) { require.NotEmpty(t, code.RecoveryCode) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + client := pointerx.Ptr(*publicTS.Client()) + client.Jar, _ = cookiejar.New(nil) + body := submitRecoveryCode(t, client, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "You successfully recovered your account. Please change your password or set up an alternative login method (e.g. social sign in) within the next 60.00 minutes.") + u, err := url.Parse(publicTS.URL) + cs := client.Jar.Cookies(u) + require.Len(t, cs, 1, "%s", body) + assert.Equal(t, "ory_kratos_session", cs[0].Name, "%s", body) }) t.Run("description=should not be able to recover with expired code", func(t *testing.T) { @@ -130,22 +148,18 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), pointerx.String("100ms")) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String(), ExpiresIn: pointerx.Ptr("100ms")}) require.NoError(t, err) time.Sleep(time.Millisecond * 100) require.NotEmpty(t, code.RecoveryLink) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + body := submitRecoveryCode(t, nil, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "The recovery flow expired 0.00 minutes ago, please try again.") // The recovery address should not be verified if the flow was initiated by the admins - addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) - assert.NoError(t, err) - assert.False(t, addr.Verified) - assert.Nil(t, addr.VerifiedAt) - assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + assertEmailNotVerified(t, recoveryEmail) }) t.Run("description=should create a valid recovery link and set the expiry time as well and recover the account", func(t *testing.T) { @@ -155,35 +169,32 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), nil) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String()}) require.NoError(t, err) require.NotEmpty(t, code.RecoveryLink) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)+time.Second))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + body := submitRecoveryCode(t, nil, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "You successfully recovered your account. Please change your password or set up an alternative login method (e.g. social sign in) within the next 60.00 minutes.") - addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) - assert.NoError(t, err) - assert.False(t, addr.Verified) - assert.Nil(t, addr.VerifiedAt) - assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + // The recovery address should be verified if the flow was initiated by the admins + assertEmailNotVerified(t, recoveryEmail) }) t.Run("case=should not be able to use code from different flow", func(t *testing.T) { email := testhelpers.RandomEmail() i := createIdentityToRecover(t, reg, email) - c1, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c1, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) - c2, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c2, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) code2 := c2.RecoveryCode require.NotEmpty(t, code2) - body := submitRecoveryLink(t, c1.RecoveryLink, c2.RecoveryCode) + body := submitRecoveryCode(t, nil, c1.RecoveryLink, c2.RecoveryCode) testhelpers.AssertMessage(t, body, "The recovery code is invalid or has already been used. Please try again.") }) @@ -192,7 +203,7 @@ func TestAdminStrategy(t *testing.T) { email := testhelpers.RandomEmail() i := createIdentityToRecover(t, reg, email) - c1, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c1, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) res, err := http.Get(c1.RecoveryLink) @@ -201,4 +212,27 @@ func TestAdminStrategy(t *testing.T) { snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes").String())) }) + + t.Run("case=should be able to create and complete an API flow", func(t *testing.T) { + email := testhelpers.RandomEmail() + i := createIdentityToRecover(t, reg, email) + + code, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), FlowType: pointerx.Ptr(string(flow.TypeAPI))}) + require.NoError(t, err) + + res, err := publicTS.Client().Get(code.RecoveryLink) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + + action := gjson.GetBytes(body, "ui.action").String() + require.NotEmpty(t, action) + + res, err = publicTS.Client().Post(action, "application/json", strings.NewReader(fmt.Sprintf(`{"code":"%s"}`, code.RecoveryCode))) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + continueWith := gjson.GetBytes(ioutilx.MustReadAll(res.Body), "continue_with").Array() + require.Len(t, continueWith, 2) + assert.EqualValues(t, flow.ContinueWithActionSetOrySessionTokenString, continueWith[0].Get("action").String()) + }) } diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index c8be6025f840..184399ca1002 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -283,9 +283,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch req.State { - case flow.StateChooseMethod: - fallthrough - case flow.StateEmailSent: + case flow.StateChooseMethod, + flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, req) case flow.StatePassedChallenge: // was already handled, do not allow retry diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index 67e1cd388671..7b56ca5f1728 100644 --- a/selfservice/strategy/link/strategy_recovery_test.go +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -5,7 +5,6 @@ package link_test import ( "context" - _ "embed" "encoding/json" "fmt" "net/http" @@ -15,45 +14,32 @@ import ( "testing" "time" - "github.com/ory/kratos/driver" - "github.com/ory/kratos/session" - "github.com/davecgh/go-spew/spew" - "github.com/gofrs/uuid" - "github.com/pkg/errors" - - "github.com/ory/kratos/selfservice/flow" - "github.com/ory/kratos/selfservice/strategy/link" - - "github.com/ory/kratos/ui/node" - - kratos "github.com/ory/kratos/internal/httpclient" - - "github.com/ory/kratos/corpx" - - "github.com/ory/x/ioutilx" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "github.com/ory/x/urlx" - - "github.com/ory/x/sqlxx" - - "github.com/ory/x/assertx" - - "github.com/ory/x/pointerx" - + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/kratos/session" "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/pointerx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" ) func init() { @@ -128,7 +114,7 @@ func TestAdminStrategy(t *testing.T) { rl, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), - ExpiresIn: pointerx.String("100ms"), + ExpiresIn: pointerx.Ptr("100ms"), }).Execute() require.NoError(t, err) @@ -152,7 +138,7 @@ func TestAdminStrategy(t *testing.T) { rl, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), - ExpiresIn: pointerx.String("100ms"), + ExpiresIn: pointerx.Ptr("100ms"), }).Execute() require.NoError(t, err) diff --git a/spec/api.json b/spec/api.json index 48b0d934d382..365cc3fc5f7f 100644 --- a/spec/api.json +++ b/spec/api.json @@ -701,6 +701,9 @@ "pattern": "^([0-9]+(ns|us|ms|s|m|h))*$", "type": "string" }, + "flow_type": { + "$ref": "#/components/schemas/selfServiceFlowType" + }, "identity_id": { "description": "Identity to Recover\n\nThe identity's ID you wish to recover.", "format": "uuid", diff --git a/spec/swagger.json b/spec/swagger.json index 1d548df9a00f..9753c89c8991 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3824,6 +3824,9 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))*$" }, + "flow_type": { + "$ref": "#/definitions/selfServiceFlowType" + }, "identity_id": { "description": "Identity to Recover\n\nThe identity's ID you wish to recover.", "type": "string",