Skip to content

Commit 8c631e5

Browse files
hperljonas-jonas
authored andcommitted
feat: add break-glass login for organization identities
Co-authored-by: Jonas Hungershausen <jonas.hungershausen@ory.sh> GitOrigin-RevId: 9d39c556b93da3aeb9cfab9c3dcd8d7349a9e8e7
1 parent 9b0df0f commit 8c631e5

23 files changed

+270
-18
lines changed

identity/extension_recovery_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/ory/jsonschema/v3"
1414
_ "github.com/ory/jsonschema/v3/fileloader"
1515

16+
"github.com/gofrs/uuid"
17+
1618
"github.com/ory/kratos/schema"
1719
"github.com/ory/kratos/x"
1820

@@ -22,6 +24,7 @@ import (
2224

2325
func TestSchemaExtensionRecovery(t *testing.T) {
2426
iid := x.NewUUID()
27+
breakGlassOrgID := uuid.NullUUID{UUID: x.NewUUID(), Valid: true}
2528
for k, tc := range []struct {
2629
expectErr error
2730
schema string
@@ -210,6 +213,46 @@ func TestSchemaExtensionRecovery(t *testing.T) {
210213
// We get 2 errors: one from the JSON schema `format` validation and one from the Go validation.
211214
expectErr: errors.New("I[#/telephoneNumber] S[#/properties/telephoneNumber] validation failed\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"foobar\" is not valid \"tel\"\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"foobar\" is not valid \"tel\""),
212215
},
216+
{
217+
description: "break_glass_for_organization preserved on existing recovery address",
218+
doc: `{"username":"foo@ory.sh"}`,
219+
schema: "file://./stub/extension/recovery/email.schema.json",
220+
expect: []RecoveryAddress{
221+
{
222+
Value: "foo@ory.sh",
223+
Via: AddressTypeEmail,
224+
IdentityID: iid,
225+
BreakGlassForOrganization: breakGlassOrgID,
226+
},
227+
},
228+
existing: []RecoveryAddress{
229+
{
230+
Value: "foo@ory.sh",
231+
Via: AddressTypeEmail,
232+
IdentityID: iid,
233+
BreakGlassForOrganization: breakGlassOrgID,
234+
},
235+
},
236+
},
237+
{
238+
description: "break_glass null preserved on existing recovery address",
239+
doc: `{"username":"foo@ory.sh"}`,
240+
schema: "file://./stub/extension/recovery/email.schema.json",
241+
expect: []RecoveryAddress{
242+
{
243+
Value: "foo@ory.sh",
244+
Via: AddressTypeEmail,
245+
IdentityID: iid,
246+
},
247+
},
248+
existing: []RecoveryAddress{
249+
{
250+
Value: "foo@ory.sh",
251+
Via: AddressTypeEmail,
252+
IdentityID: iid,
253+
},
254+
},
255+
},
213256
} {
214257
t.Run(fmt.Sprintf("case=%d description=%s", k, tc.description), func(t *testing.T) {
215258
id := &Identity{ID: iid, RecoveryAddresses: tc.existing}

identity/identity_recovery.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ type (
2424
// required: true
2525
Via string `json:"via" db:"via"`
2626

27+
// BreakGlassForOrganization, when set to an organization ID, allows this
28+
// recovery address to bypass SSO enforcement for that organization. This
29+
// enables designated users to recover their account via email when the
30+
// SSO provider is unavailable.
31+
BreakGlassForOrganization uuid.NullUUID `json:"break_glass_for_organization,omitzero" db:"break_glass_for_organization"`
32+
2733
// IdentityID is a helper struct field for gobuffalo.pop.
2834
IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"`
2935
// CreatedAt is a helper struct field for gobuffalo.pop.
@@ -39,7 +45,7 @@ func (a RecoveryAddress) GetID() uuid.UUID { return a.ID }
3945

4046
// Signature returns a unique string representation for the recovery address.
4147
func (a RecoveryAddress) Signature() string {
42-
return fmt.Sprintf("%v|%v|%v|%v", a.Value, a.Via, a.IdentityID, a.NID)
48+
return fmt.Sprintf("%v|%v|%v|%v|%v", a.Value, a.Via, a.BreakGlassForOrganization, a.IdentityID, a.NID)
4349
}
4450

4551
func NewRecoveryEmailAddress(

identity/identity_recovery_test.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ func TestRecoveryAddress_Hash(t *testing.T) {
4242
IdentityID: x.NewUUID(),
4343
NID: x.NewUUID(),
4444
},
45-
}, {
45+
},
46+
{
4647
name: "empty fields",
4748
a: RecoveryAddress{},
48-
}, {
49+
},
50+
{
4951
name: "email constructor",
5052
a: *NewRecoveryEmailAddress("foo@ory.sh", x.NewUUID()),
5153
},
@@ -60,10 +62,24 @@ func TestRecoveryAddress_Hash(t *testing.T) {
6062
IdentityID: x.NewUUID(),
6163
NID: x.NewUUID(),
6264
},
63-
}, {
65+
},
66+
{
6467
name: "SMS constructor",
6568
a: *NewRecoverySMSAddress("6502530000", x.NewUUID()),
6669
},
70+
{
71+
name: "break glass for organization",
72+
a: RecoveryAddress{
73+
ID: x.NewUUID(),
74+
Value: "breakglass@ory.sh",
75+
Via: AddressTypeEmail,
76+
CreatedAt: time.Now(),
77+
UpdatedAt: time.Now(),
78+
IdentityID: x.NewUUID(),
79+
NID: x.NewUUID(),
80+
BreakGlassForOrganization: uuid.NullUUID{UUID: x.NewUUID(), Valid: true},
81+
},
82+
},
6783
}
6884

6985
for _, tc := range cases {
@@ -74,5 +90,4 @@ func TestRecoveryAddress_Hash(t *testing.T) {
7490
)
7591
})
7692
}
77-
7893
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"TableName": "\"identity_recovery_addresses\"",
3-
"ColumnsDecl": "\"created_at\", \"id\", \"identity_id\", \"nid\", \"updated_at\", \"value\", \"via\"",
3+
"ColumnsDecl": "\"break_glass_for_organization\", \"created_at\", \"id\", \"identity_id\", \"nid\", \"updated_at\", \"value\", \"via\"",
44
"Columns": [
5+
"break_glass_for_organization",
56
"created_at",
67
"id",
78
"identity_id",
@@ -10,5 +11,5 @@
1011
"value",
1112
"via"
1213
],
13-
"Placeholders": "(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?)"
14+
"Placeholders": "(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?)"
1415
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"TableName": "\"identity_recovery_addresses\"",
3-
"ColumnsDecl": "\"created_at\", \"id\", \"identity_id\", \"nid\", \"updated_at\", \"value\", \"via\"",
3+
"ColumnsDecl": "\"break_glass_for_organization\", \"created_at\", \"id\", \"identity_id\", \"nid\", \"updated_at\", \"value\", \"via\"",
44
"Columns": [
5+
"break_glass_for_organization",
56
"created_at",
67
"id",
78
"identity_id",
@@ -10,5 +11,5 @@
1011
"value",
1112
"via"
1213
],
13-
"Placeholders": "(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?)"
14+
"Placeholders": "(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?, ?)"
1415
}

persistence/sql/identity/persister_identity.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,9 +1426,8 @@ func (p *IdentityPersister) FindAllRecoveryAddressesForIdentityByRecoveryAddress
14261426
//
14271427
// This is all done in one query with a self-join.
14281428
// We also bound the results for safety.
1429-
err = p.GetConnection(ctx).RawQuery(
1430-
`
1431-
SELECT A.id, A.via, A.value, A.identity_id, A.created_at, A.updated_at, A.nid
1429+
err = p.GetConnection(ctx).RawQuery(`
1430+
SELECT A.id, A.via, A.value, A.identity_id, A.created_at, A.updated_at, A.nid, A.break_glass_for_organization
14321431
FROM identity_recovery_addresses A
14331432
JOIN identity_recovery_addresses B
14341433
ON A.identity_id = B.identity_id
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE identity_recovery_addresses DROP COLUMN IF EXISTS break_glass_for_organization;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE identity_recovery_addresses ADD COLUMN IF NOT EXISTS break_glass_for_organization UUID;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE identity_recovery_addresses DROP COLUMN break_glass_for_organization;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE identity_recovery_addresses DROP COLUMN break_glass_for_organization;

0 commit comments

Comments
 (0)