Skip to content

Commit

Permalink
feat(identities): add a state to identities
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasruiz committed May 5, 2021
1 parent 6771958 commit a4d43d6
Show file tree
Hide file tree
Showing 54 changed files with 131 additions and 28 deletions.
3 changes: 2 additions & 1 deletion identity/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package identity
import (
"encoding/json"
"net/http"
"time"

"github.com/ory/kratos/driver/config"

Expand Down Expand Up @@ -214,7 +215,7 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
return
}

i := &Identity{SchemaID: cr.SchemaID, Traits: []byte(cr.Traits)}
i := &Identity{SchemaID: cr.SchemaID, Traits: []byte(cr.Traits), State: StateActive, StateChangedAt: time.Now()}
if err := h.r.IdentityManager().Create(r.Context(), i); err != nil {
h.r.Writer().WriteError(w, r, err)
return
Expand Down
19 changes: 19 additions & 0 deletions identity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ import (
"github.com/ory/kratos/x"
)

type State uint8

const (
StateActive State = iota + 1
StateDisabled
)

type (
// Identity represents an Ory Kratos identity
//
Expand Down Expand Up @@ -75,6 +82,12 @@ type (
// ---
RecoveryAddresses []RecoveryAddress `json:"recovery_addresses,omitempty" faker:"-" has_many:"identity_recovery_addresses" fk_id:"identity_id"`

// State is the identity's state.
State State `json:"-" faker:"-" db:"state"`

// StateChangedAt contains the last time when the identity's state changed.
StateChangedAt time.Time `json:"-" faker:"-" db:"state_changed_at"`

// CreatedAt is a helper struct field for gobuffalo.pop.
CreatedAt time.Time `json:"-" db:"created_at"`

Expand Down Expand Up @@ -125,6 +138,10 @@ func (i *Identity) lock() *sync.RWMutex {
return i.l
}

func (i *Identity) IsActive() bool {
return i.State == StateActive
}

func (i *Identity) SetCredentials(t CredentialsType, c Credentials) {
i.lock().Lock()
defer i.lock().Unlock()
Expand Down Expand Up @@ -178,6 +195,8 @@ func NewIdentity(traitsSchemaID string) *Identity {
Traits: Traits("{}"),
SchemaID: traitsSchemaID,
VerifiableAddresses: []VerifiableAddress{},
State: StateActive,
StateChangedAt: time.Now(),
l: new(sync.RWMutex),
}
}
Expand Down
1 change: 1 addition & 0 deletions identity/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ func TestNewIdentity(t *testing.T) {
// assert.NotEmpty(t, i.Metadata)
assert.NotEmpty(t, i.Traits)
assert.NotNil(t, i.Credentials)
assert.True(t, i.IsActive())
}
5 changes: 3 additions & 2 deletions internal/testhelpers/handler_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ func MockSetSession(t *testing.T, reg mockDeps, conf *config.Config) httprouter.
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))

require.NoError(t, reg.SessionManager().CreateAndIssueCookie(context.Background(), w, r, session.NewActiveSession(i, conf, time.Now().UTC())))
activeSession, _ := session.NewActiveSession(i, conf, time.Now().UTC())
require.NoError(t, reg.SessionManager().CreateAndIssueCookie(context.Background(), w, r, activeSession))

w.WriteHeader(http.StatusOK)
}
Expand Down Expand Up @@ -121,5 +122,5 @@ func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identit

func MockSessionCreateHandler(t *testing.T, reg mockDeps) (httprouter.Handle, *session.Session) {
return MockSessionCreateHandlerWithIdentity(t, reg, &identity.Identity{
ID: x.NewUUID(), Traits: identity.Traits(`{"baz":"bar","foo":true,"bar":2.5}`)})
ID: x.NewUUID(), State: identity.StateActive, Traits: identity.Traits(`{"baz":"bar","foo":true,"bar":2.5}`)})
}
26 changes: 16 additions & 10 deletions internal/testhelpers/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,27 +92,33 @@ func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, se
}

func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDefault) *http.Client {
return NewHTTPClientWithSessionToken(t, reg, session.NewActiveSession(
&identity.Identity{ID: x.NewUUID()},
s, _ := session.NewActiveSession(
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
NewSessionLifespanProvider(time.Hour),
time.Now(),
))
)

return NewHTTPClientWithSessionToken(t, reg, s)
}

func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client {
return NewHTTPClientWithSessionCookie(t, reg, session.NewActiveSession(
&identity.Identity{ID: x.NewUUID()},
s, _ := session.NewActiveSession(
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
NewSessionLifespanProvider(time.Hour),
time.Now(),
))
)

return NewHTTPClientWithSessionCookie(t, reg, s)
}

func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
return NewHTTPClientWithSessionCookie(t, reg,
session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now()))
s, _ := session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now())

return NewHTTPClientWithSessionCookie(t, reg, s)
}

func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
return NewHTTPClientWithSessionToken(t, reg,
session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now()))
s, _ := session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now())

return NewHTTPClientWithSessionToken(t, reg, s)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" DROP COLUMN "state_changed_at";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" ADD COLUMN "state" int NOT NULL DEFAULT '1';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `identities` DROP COLUMN `state_changed_at`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `identities` ADD COLUMN `state` INTEGER NOT NULL DEFAULT 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" DROP COLUMN "state_changed_at";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" ADD COLUMN "state" int NOT NULL DEFAULT '1';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "_identities_tmp" RENAME TO "identities";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" ADD COLUMN "state" INTEGER NOT NULL DEFAULT '1';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" DROP COLUMN "state";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" ADD COLUMN "state_changed_at" timestamp;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `identities` DROP COLUMN `state`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `identities` ADD COLUMN `state_changed_at` DATETIME;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" DROP COLUMN "state";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" ADD COLUMN "state_changed_at" timestamp;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

DROP TABLE "identities";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "identities" ADD COLUMN "state_changed_at" DATETIME;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INSERT INTO "_identities_tmp" (id, schema_id, traits, created_at, updated_at, nid) SELECT id, schema_id, traits, created_at, updated_at, nid FROM "identities";
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX "identities_nid_idx" ON "_identities_tmp" (id, nid);
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE "_identities_tmp" (
"id" TEXT PRIMARY KEY,
"schema_id" TEXT NOT NULL,
"traits" TEXT NOT NULL,
"created_at" DATETIME NOT NULL,
"updated_at" DATETIME NOT NULL,
"nid" char(36)
);
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP INDEX IF EXISTS "identities_nid_idx";
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "_identities_tmp" RENAME TO "identities";
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

DROP TABLE "identities";
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INSERT INTO "_identities_tmp" (id, schema_id, traits, created_at, updated_at, nid, state_changed_at) SELECT id, schema_id, traits, created_at, updated_at, nid, state_changed_at FROM "identities";
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX "identities_nid_idx" ON "_identities_tmp" (id, nid);
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE "_identities_tmp" (
"id" TEXT PRIMARY KEY,
"schema_id" TEXT NOT NULL,
"traits" TEXT NOT NULL,
"created_at" DATETIME NOT NULL,
"updated_at" DATETIME NOT NULL,
"nid" char(36),
"state_changed_at" DATETIME
);
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP INDEX IF EXISTS "identities_nid_idx";
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
drop_column("identities", "state")
drop_column("identities", "state_changed_at")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
add_column("identities", "state", "int", {"default": 1})
add_column("identities", "state_changed_at", "timestamp", {"null": true})
5 changes: 5 additions & 0 deletions persistence/sql/persister_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ func (p *Persister) CreateIdentity(ctx context.Context, i *identity.Identity) er
i.SchemaID = config.DefaultIdentityTraitsSchemaID
}

i.StateChangedAt = time.Now()
if i.State == 0 {
i.State = identity.StateActive
}

if len(i.Traits) == 0 {
i.Traits = identity.Traits("{}")
}
Expand Down
1 change: 1 addition & 0 deletions persistence/sql/persister_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ func TestPersister_Transaction(t *testing.T) {
t.Run("case=should not create identity because callback returned error", func(t *testing.T) {
i := &ri.Identity{
ID: x.NewUUID(),
State: ri.StateActive,
Traits: ri.Traits(`{}`),
}
errMessage := "failing because why not"
Expand Down
6 changes: 5 additions & 1 deletion selfservice/flow/login/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ func NewHookExecutor(d executorDependencies) *HookExecutor {
}

func (e *HookExecutor) PostLoginHook(w http.ResponseWriter, r *http.Request, ct identity.CredentialsType, a *Flow, i *identity.Identity) error {
s := session.NewActiveSession(i, e.d.Config(r.Context()), time.Now().UTC()).Declassify()
s, err := session.NewActiveSession(i, e.d.Config(r.Context()), time.Now().UTC())
if err != nil {
return err
}
s = s.Declassify()

e.d.Logger().
WithRequest(r).
Expand Down
6 changes: 5 additions & 1 deletion selfservice/flow/registration/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque
WithField("identity_id", i.ID).
Info("A new identity has registered using self-service registration.")

s := session.NewActiveSession(i, e.d.Config(r.Context()), time.Now().UTC())
s, err := session.NewActiveSession(i, e.d.Config(r.Context()), time.Now().UTC())
if err != nil {
return err
}

e.d.Logger().
WithRequest(r).
WithField("identity_id", i.ID).
Expand Down
4 changes: 2 additions & 2 deletions selfservice/flow/settings/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ func TestHandleError(t *testing.T) {
t.Cleanup(reset)

// This needs an authenticated client in order to call the RouteGetFlow endpoint
c := testhelpers.NewHTTPClientWithSessionToken(t, reg, session.NewActiveSession(&id,
testhelpers.NewSessionLifespanProvider(time.Hour), time.Now()))
s, _ := session.NewActiveSession(&id, testhelpers.NewSessionLifespanProvider(time.Hour), time.Now())
c := testhelpers.NewHTTPClientWithSessionToken(t, reg, s)

settingsFlow = newFlow(t, time.Minute, flow.TypeAPI)
flowError = settings.NewFlowExpiredError(expiredAnHourAgo)
Expand Down
2 changes: 1 addition & 1 deletion selfservice/flow/settings/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestSettingsExecutor(t *testing.T) {
handleErr := testhelpers.SelfServiceHookSettingsErrorHandler
router.GET("/settings/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
i := testhelpers.SelfServiceHookCreateFakeIdentity(t, reg)
sess := session.NewActiveSession(i, conf, time.Now().UTC())
sess, _ := session.NewActiveSession(i, conf, time.Now().UTC())

a := settings.NewFlow(conf, time.Minute, r, sess.Identity, ft)
a.RequestURL = x.RequestURL(r).String()
Expand Down
2 changes: 1 addition & 1 deletion selfservice/strategy/link/strategy_recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request,
return s.handleRecoveryError(w, r, f, nil, err)
}

sess := session.NewActiveSession(recovered, s.d.Config(r.Context()), time.Now().UTC())
sess, _ := session.NewActiveSession(recovered, s.d.Config(r.Context()), time.Now().UTC())
if err := s.d.SessionManager().CreateAndIssueCookie(r.Context(), w, r, sess); err != nil {
return s.handleRecoveryError(w, r, f, nil, err)
}
Expand Down
2 changes: 1 addition & 1 deletion session/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestSessionRevoke(t *testing.T) {
conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://stub/identity.schema.json")
i := &identity.Identity{Traits: identity.Traits(`{"baz":"bar"}`)}
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
sess := NewActiveSession(i, conf, time.Now())
sess, _ := NewActiveSession(i, conf, time.Now())
require.NoError(t, reg.SessionPersister().CreateSession(context.Background(), sess))

sdk := testhelpers.NewSDKClient(publicTS)
Expand Down
8 changes: 4 additions & 4 deletions session/manager_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func TestManagerHTTP(t *testing.T) {

i := identity.Identity{Traits: []byte("{}")}
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
s = session.NewActiveSession(&i, conf, time.Now())
s, _ = session.NewActiveSession(&i, conf, time.Now())

c := testhelpers.NewClientWithCookies(t)
testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set")
Expand All @@ -105,7 +105,7 @@ func TestManagerHTTP(t *testing.T) {

i := identity.Identity{Traits: []byte("{}")}
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
s = session.NewActiveSession(&i, conf, time.Now())
s, _ = session.NewActiveSession(&i, conf, time.Now())

c := testhelpers.NewClientWithCookies(t)
testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set")
Expand All @@ -120,9 +120,9 @@ func TestManagerHTTP(t *testing.T) {
t.Run("case=revoked", func(t *testing.T) {
i := identity.Identity{Traits: []byte("{}")}
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
s = session.NewActiveSession(&i, conf, time.Now())
s, _ = session.NewActiveSession(&i, conf, time.Now())

s = session.NewActiveSession(&i, conf, time.Now())
s, _ = session.NewActiveSession(&i, conf, time.Now())

c := testhelpers.NewClientWithCookies(t)
testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set")
Expand Down
13 changes: 10 additions & 3 deletions session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package session

import (
"context"
"github.com/ory/herodot"
"time"

"github.com/ory/kratos/corp"
Expand All @@ -14,6 +15,8 @@ import (
"github.com/ory/kratos/x"
)

var ErrIdentityDisabled = herodot.ErrUnauthorized.WithError("identity is disabled").WithReason("This account was disabled.")

// swagger:model session
type Session struct {
// required: true
Expand Down Expand Up @@ -49,7 +52,11 @@ func (s Session) TableName(ctx context.Context) string {

func NewActiveSession(i *identity.Identity, c interface {
SessionLifespan() time.Duration
}, authenticatedAt time.Time) *Session {
}, authenticatedAt time.Time) (*Session, error) {
if i != nil && !i.IsActive() {
return nil, ErrIdentityDisabled
}

return &Session{
ID: x.NewUUID(),
ExpiresAt: authenticatedAt.Add(c.SessionLifespan()),
Expand All @@ -59,7 +66,7 @@ func NewActiveSession(i *identity.Identity, c interface {
IdentityID: i.ID,
Token: randx.MustString(32, randx.AlphaNum),
Active: true,
}
}, nil
}

type Device struct {
Expand All @@ -73,5 +80,5 @@ func (s *Session) Declassify() *Session {
}

func (s *Session) IsActive() bool {
return s.Active && s.ExpiresAt.After(time.Now())
return s.Active && s.ExpiresAt.After(time.Now()) && (s.Identity == nil || s.Identity.IsActive())
}
Loading

0 comments on commit a4d43d6

Please sign in to comment.