Skip to content

Commit

Permalink
feat: implement password profile management flow
Browse files Browse the repository at this point in the history
Closes #243
  • Loading branch information
aeneasr committed Apr 10, 2020
1 parent 4f1e033 commit a31839a
Show file tree
Hide file tree
Showing 44 changed files with 1,636 additions and 1,021 deletions.
37 changes: 4 additions & 33 deletions .schema/api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,9 @@
}
}
},
"/self-service/browser/flows/profile/update": {
"/self-service/browser/flows/profile/strategies/profile": {
"post": {
"description": "This endpoint completes a browser-based profile management flow. This is usually achieved by POSTing data to this\nendpoint.\n\nIf the provided profile data is valid against the Identity's Traits JSON Schema, the data will be updated and\nthe browser redirected to `url.profile_ui` for further steps.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.\n\nMore information can be found at [ORY Kratos Profile Management Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-profile-management).",
"description": "This endpoint completes a browser-based profile management flow. This is usually achieved by POSTing data to this\nendpoint.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.\n\nMore information can be found at [ORY Kratos Profile Management Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-profile-management).",
"consumes": [
"application/json",
"application/x-www-form-urlencoded"
Expand All @@ -441,25 +441,8 @@
"tags": [
"public"
],
"summary": "Complete the browser-based profile management flows",
"operationId": "completeSelfServiceBrowserProfileManagementFlow",
"parameters": [
{
"type": "string",
"description": "Request is the request ID.",
"name": "request",
"in": "query",
"required": true
},
{
"name": "Body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/completeSelfServiceBrowserProfileManagementFlowPayload"
}
}
],
"summary": "Complete the browser-based profile management flow for the password strategy",
"operationId": "completeSelfServiceBrowserProfileManagementPasswordStrategyFlow",
"responses": {
"302": {
"description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201."
Expand Down Expand Up @@ -1082,18 +1065,6 @@
"VerifiableAddressType": {
"type": "string"
},
"completeSelfServiceBrowserProfileManagementFlowPayload": {
"type": "object",
"required": [
"traits"
],
"properties": {
"traits": {
"description": "Traits contains all of the identity's traits.\n\ntype: string\nformat: binary",
"type": "object"
}
}
},
"errorContainer": {
"type": "object",
"properties": {
Expand Down
3 changes: 2 additions & 1 deletion cmd/daemon/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func servePublic(d driver.Driver, wg *sync.WaitGroup, cmd *cobra.Command, args [
r.LogoutHandler().RegisterPublicRoutes(router)
r.ProfileManagementHandler().RegisterPublicRoutes(router)
r.LoginStrategies().RegisterPublicRoutes(router)
r.ProfileManagementStrategies().RegisterPublicRoutes(router)
r.RegistrationStrategies().RegisterPublicRoutes(router)
r.SessionHandler().RegisterPublicRoutes(router)
r.SelfServiceErrorHandler().RegisterPublicRoutes(router)
Expand Down Expand Up @@ -147,7 +148,7 @@ func sqa(cmd *cobra.Command, d driver.Driver) *metricsx.Service {
session.SessionsWhoamiPath,
identity.IdentitiesPath,
profile.PublicProfileManagementPath,
profile.AdminBrowserProfileRequestPath,
profile.BrowserProfileRequestPath,
profile.PublicProfileManagementPath,
profile.PublicProfileManagementUpdatePath,
verify.PublicVerificationCompletePath,
Expand Down
1 change: 1 addition & 0 deletions driver/configuration/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type Provider interface {
SelfServiceRegistrationBeforeHooks() []SelfServiceHook
SelfServiceLoginAfterHooks(strategy string) []SelfServiceHook
SelfServiceRegistrationAfterHooks(strategy string) []SelfServiceHook
SelfServiceProfileManagementAfterHooks(strategy string) []SelfServiceHook
SelfServiceLogoutRedirectURL() *url.URL
SelfServiceVerificationLinkLifespan() time.Duration
SelfServicePrivilegedSessionMaxAge() time.Duration
Expand Down
5 changes: 5 additions & 0 deletions driver/configuration/provider_viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const (
ViperKeySelfServiceLifespanRegistrationRequest = "selfservice.registration.request_lifespan"
ViperKeySelfServiceLoginBeforeConfig = "selfservice.login.before"
ViperKeySelfServiceLoginAfterConfig = "selfservice.login.after"
ViperKeySelfServiceProfileManagementAfterConfig = "selfservice.profile_management.after"
ViperKeySelfServiceLifespanLoginRequest = "selfservice.login.request_lifespan"
ViperKeySelfServiceLogoutRedirectURL = "selfservice.logout.redirect_to"
ViperKeySelfServiceLifespanProfileRequest = "selfservice.profile.request_lifespan"
Expand Down Expand Up @@ -200,6 +201,10 @@ func (p *ViperProvider) SelfServiceLoginAfterHooks(strategy string) []SelfServic
return p.selfServiceHooks(ViperKeySelfServiceLoginAfterConfig + "." + strategy)
}

func (p *ViperProvider) SelfServiceProfileManagementAfterHooks(strategy string) []SelfServiceHook {
return p.selfServiceHooks(ViperKeySelfServiceProfileManagementAfterConfig + "." + strategy)
}

func (p *ViperProvider) SelfServiceRegistrationAfterHooks(strategy string) []SelfServiceHook {
return p.selfServiceHooks(ViperKeySelfServiceRegistrationAfterConfig + "." + strategy)
}
Expand Down
1 change: 1 addition & 0 deletions driver/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type Registry interface {
profile.HandlerProvider
profile.ErrorHandlerProvider
profile.RequestPersistenceProvider
profile.StrategyProvider

login.RequestPersistenceProvider
login.ErrorHandlerProvider
Expand Down
1 change: 1 addition & 0 deletions driver/registry_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type RegistryDefault struct {

selfserviceProfileManagementHandler *profile.Handler
selfserviceProfileRequestRequestErrorHandler *profile.ErrorHandler
selfserviceProfileManagementExecutor *profile.HookExecutor

selfserviceVerifyErrorHandler *verify.ErrorHandler
selfserviceVerifyManager *identity.Manager
Expand Down
3 changes: 1 addition & 2 deletions driver/registry_default_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import (
"net/url"

"github.com/ory/kratos/driver/configuration"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/hook"
)

func (m *RegistryDefault) getHooks(credentialsType identity.CredentialsType, configs []configuration.SelfServiceHook) []interface{} {
func (m *RegistryDefault) getHooks(credentialsType string, configs []configuration.SelfServiceHook) []interface{} {
var i []interface{}

for _, h := range configs {
Expand Down
2 changes: 1 addition & 1 deletion driver/registry_default_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (m *RegistryDefault) PreLoginHooks() []login.PreHookExecutor {
}

func (m *RegistryDefault) PostLoginHooks(credentialsType identity.CredentialsType) []login.PostHookExecutor {
a := m.getHooks(credentialsType, m.c.SelfServiceLoginAfterHooks(string(credentialsType)))
a := m.getHooks(string(credentialsType), m.c.SelfServiceLoginAfterHooks(string(credentialsType)))

var b []login.PostHookExecutor

Expand Down
25 changes: 25 additions & 0 deletions driver/registry_default_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package driver

import (
"github.com/ory/kratos/selfservice/flow/profile"
)

func (m *RegistryDefault) PostProfileManagementHooks(credentialsType string) []profile.PostHookExecutor {
a := m.getHooks(credentialsType, m.c.SelfServiceProfileManagementAfterHooks(credentialsType))

var b []profile.PostHookExecutor
for _, v := range a {
if hook, ok := v.(profile.PostHookExecutor); ok {
b = append(b, hook)
}
}

return b
}

func (m *RegistryDefault) ProfileManagementExecutor() *profile.HookExecutor {
if m.selfserviceProfileManagementExecutor == nil {
m.selfserviceProfileManagementExecutor = profile.NewHookExecutor(m, m.c)
}
return m.selfserviceProfileManagementExecutor
}
2 changes: 1 addition & 1 deletion driver/registry_default_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

func (m *RegistryDefault) PostRegistrationHooks(credentialsType identity.CredentialsType) []registration.PostHookExecutor {
a := m.getHooks(credentialsType, m.c.SelfServiceRegistrationAfterHooks(string(credentialsType)))
a := m.getHooks(string(credentialsType), m.c.SelfServiceRegistrationAfterHooks(string(credentialsType)))

var b []registration.PostHookExecutor

Expand Down
62 changes: 39 additions & 23 deletions identity/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,46 +72,62 @@ func (m *Manager) Create(ctx context.Context, i *Identity, opts ...ManagerOption
return m.r.IdentityPool().(PrivilegedPool).CreateIdentity(ctx, i)
}

func (m *Manager) Update(ctx context.Context, i *Identity, opts ...ManagerOption) error {
func (m *Manager) requiresPrivilegedAccess(_ context.Context, original, updated *Identity, o *managerOptions) error {
if !o.AllowWriteProtectedTraits {
if !CredentialsEqual(updated.Credentials, original.Credentials) {
// reset the identity
*updated = *original
return errors.WithStack(ErrProtectedFieldModified)
}

if !reflect.DeepEqual(original.Addresses, updated.Addresses) &&
/* prevent nil != []string{} */
len(original.Addresses)+len(updated.Addresses) != 0 {
// reset the identity
*updated = *original
return errors.WithStack(ErrProtectedFieldModified)
}
}
return nil
}

func (m *Manager) Update(ctx context.Context, updated *Identity, opts ...ManagerOption) error {
o := newManagerOptions(opts)
if err := m.validate(i, o); err != nil {
if err := m.validate(updated, o); err != nil {
return err
}

return m.r.IdentityPool().(PrivilegedPool).UpdateIdentity(ctx, i)
original, err := m.r.IdentityPool().(PrivilegedPool).GetIdentityConfidential(ctx, updated.ID)
if err != nil {
return err
}

if err := m.requiresPrivilegedAccess(ctx, original, updated, o); err != nil {
return err
}

return m.r.IdentityPool().(PrivilegedPool).UpdateIdentity(ctx, updated)
}

func (m *Manager) UpdateTraits(ctx context.Context, id uuid.UUID, traits Traits, opts ...ManagerOption) error {
o := newManagerOptions(opts)

identity, err := m.r.IdentityPool().(PrivilegedPool).GetIdentityConfidential(ctx, id)
original, err := m.r.IdentityPool().(PrivilegedPool).GetIdentityConfidential(ctx, id)
if err != nil {
return err
}

// original is used to check whether protected traits were modified
original := deepcopy.Copy(identity).(*Identity)
identity.Traits = traits
if err := m.validate(identity, o); err != nil {
updated := deepcopy.Copy(original).(*Identity)
updated.Traits = traits
if err := m.validate(updated, o); err != nil {
return err
}

if !o.AllowWriteProtectedTraits {
if !CredentialsEqual(identity.Credentials, original.Credentials) {
// reset the identity
*identity = *original
return errors.WithStack(ErrProtectedFieldModified)
}

if !reflect.DeepEqual(original.Addresses, identity.Addresses) &&
/* prevent nil != []string{} */
len(original.Addresses)+len(identity.Addresses) != 0 {
// reset the identity
*identity = *original
return errors.WithStack(ErrProtectedFieldModified)
}
if err := m.requiresPrivilegedAccess(ctx, original, updated, o); err != nil {
return err
}

return m.r.IdentityPool().(PrivilegedPool).UpdateIdentity(ctx, identity)
return m.r.IdentityPool().(PrivilegedPool).UpdateIdentity(ctx, updated)
}

func (m *Manager) RefreshVerifyAddress(ctx context.Context, address *VerifiableAddress) error {
Expand Down
31 changes: 24 additions & 7 deletions identity/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func TestManager(t *testing.T) {
require.NoError(t, reg.IdentityManager().Create(context.Background(), original))

original.Traits = identity.Traits(`{"email":"[email protected]"}`)
require.NoError(t, reg.IdentityManager().Update(context.Background(), original))
require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits))

checkExtensionFieldsForIdentities(t, "[email protected]", original)
})
Expand All @@ -101,40 +101,57 @@ func TestManager(t *testing.T) {
require.NoError(t, err)
assert.JSONEq(t, `{"email":"[email protected]","email_verify":"[email protected]","email_creds":"[email protected]","unprotected": "bar"}`, string(actual.Traits))
})

t.Run("case=should not update protected traits without option", func(t *testing.T) {
original := identity.NewIdentity(configuration.DefaultIdentityTraitsSchemaID)
original.Traits = identity.Traits(`{"email":"[email protected]"}`)
require.NoError(t, reg.IdentityManager().Create(context.Background(), original))

original.Traits = identity.Traits(`{"email":"[email protected]"}`)
err := reg.IdentityManager().Update(context.Background(), original)
require.Error(t, err)
assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err))

fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
require.NoError(t, err)
// As UpdateTraits takes only the ID as a parameter it cannot update the identity in place.
// That is why we only check the identity in the store.
checkExtensionFields(fromStore, "[email protected]")(t)
})
})

t.Run("method=UpdateTraits", func(t *testing.T) {
t.Run("case=should update protected traits with option", func(t *testing.T) {
original := identity.NewIdentity(configuration.DefaultIdentityTraitsSchemaID)
original.Traits = identity.Traits(`{"email":"email1@ory.sh"}`)
original.Traits = identity.Traits(`{"email":"email-updatetraits-1@ory.sh"}`)
require.NoError(t, reg.IdentityManager().Create(context.Background(), original))

require.NoError(t, reg.IdentityManager().UpdateTraits(
context.Background(), original.ID, identity.Traits(`{"email":"email2@ory.sh"}`),
context.Background(), original.ID, identity.Traits(`{"email":"email-updatetraits-2@ory.sh"}`),
identity.ManagerAllowWriteProtectedTraits))

fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
require.NoError(t, err)
// As UpdateTraits takes only the ID as a parameter it cannot update the identity in place.
// That is why we only check the identity in the store.
checkExtensionFields(fromStore, "email2@ory.sh")(t)
checkExtensionFields(fromStore, "email-updatetraits-2@ory.sh")(t)
})

t.Run("case=should not update protected traits without option", func(t *testing.T) {
original := identity.NewIdentity(configuration.DefaultIdentityTraitsSchemaID)
original.Traits = identity.Traits(`{"email":"email1@ory.sh"}`)
original.Traits = identity.Traits(`{"email":"email-updatetraits-1@ory.sh"}`)
require.NoError(t, reg.IdentityManager().Create(context.Background(), original))

err := reg.IdentityManager().UpdateTraits(
context.Background(), original.ID, identity.Traits(`{"email":"email2@ory.sh"}`))
context.Background(), original.ID, identity.Traits(`{"email":"email-updatetraits-2@ory.sh"}`))
require.Error(t, err)
assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err))

fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
require.NoError(t, err)
// As UpdateTraits takes only the ID as a parameter it cannot update the identity in place.
// That is why we only check the identity in the store.
checkExtensionFields(fromStore, "email1@ory.sh")(t)
checkExtensionFields(fromStore, "email-updatetraits-1@ory.sh")(t)
})
})

Expand Down
2 changes: 1 addition & 1 deletion identity/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ type (
// if identity exists, backend connectivity is broken, or trait validation fails.
CreateIdentity(context.Context, *Identity) error

// UpdateUnprotectedTraits updates an identity excluding its confidential / privileged / protected data.
// UpdateIdentity updates an identity including its confidential / privileged / protected data.
UpdateIdentity(context.Context, *Identity) error

// GetClassified returns the identity including it's raw credentials. This should only be used internally.
Expand Down
2 changes: 1 addition & 1 deletion internal/faker.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func RegisterFakes() {

if err := faker.AddProvider("profile_management_request_methods", func(v reflect.Value) (interface{}, error) {
var methods = make(map[string]*profile.RequestMethod)
for _, ct := range []string{profile.FormTraitsID, string(identity.CredentialsTypePassword), string(identity.CredentialsTypeOIDC)} {
for _, ct := range []string{profile.StrategyTraitsID, string(identity.CredentialsTypePassword), string(identity.CredentialsTypeOIDC)} {
var f form.HTMLForm
if err := faker.FakeData(&f); err != nil {
return nil, err
Expand Down
Loading

0 comments on commit a31839a

Please sign in to comment.