chore: move the go code into a subfolder

This commit is contained in:
Florian Forster
2025-08-05 15:20:32 -07:00
parent 4ad22ba456
commit cd2921de26
2978 changed files with 373 additions and 300 deletions

View File

@@ -0,0 +1,56 @@
package model
import (
"encoding/json"
"github.com/zitadel/logging"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Address struct {
es_models.ObjectRoot
Country string `json:"country,omitempty"`
Locality string `json:"locality,omitempty"`
PostalCode string `json:"postalCode,omitempty"`
Region string `json:"region,omitempty"`
StreetAddress string `json:"streetAddress,omitempty"`
}
func (a *Address) Changes(changed *Address) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if a.Country != changed.Country {
changes["country"] = changed.Country
}
if a.Locality != changed.Locality {
changes["locality"] = changed.Locality
}
if a.PostalCode != changed.PostalCode {
changes["postalCode"] = changed.PostalCode
}
if a.Region != changed.Region {
changes["region"] = changed.Region
}
if a.StreetAddress != changed.StreetAddress {
changes["streetAddress"] = changed.StreetAddress
}
return changes
}
func (u *Human) appendUserAddressChangedEvent(event *es_models.Event) error {
if u.Address == nil {
u.Address = new(Address)
}
return u.Address.setData(event)
}
func (a *Address) setData(event *es_models.Event) error {
a.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-clos0").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-so92s", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,93 @@
package model
import (
"encoding/json"
"testing"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
func TestAddressChanges(t *testing.T) {
type args struct {
existingAddress *Address
newAddress *Address
}
type res struct {
changesLen int
}
tests := []struct {
name string
args args
res res
}{
{
name: "all fields changed",
args: args{
existingAddress: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
newAddress: &Address{Country: "CountryChanged", Locality: "LocalityChanged", PostalCode: "PostalCodeChanged", Region: "RegionChanged", StreetAddress: "StreetAddressChanged"},
},
res: res{
changesLen: 5,
},
},
{
name: "no fields changed",
args: args{
existingAddress: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
newAddress: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
},
res: res{
changesLen: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := tt.args.existingAddress.Changes(tt.args.newAddress)
if len(changes) != tt.res.changesLen {
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
}
})
}
}
func TestAppendUserAddressChangedEvent(t *testing.T) {
type args struct {
user *Human
address *Address
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user address event",
args: args{
user: &Human{Address: &Address{Locality: "Locality", Country: "Country"}},
address: &Address{Locality: "LocalityChanged", PostalCode: "PostalCode"},
event: &es_models.Event{},
},
result: &Human{Address: &Address{Locality: "LocalityChanged", Country: "Country", PostalCode: "PostalCode"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.address != nil {
data, _ := json.Marshal(tt.args.address)
tt.args.event.Data = data
}
tt.args.user.appendUserAddressChangedEvent(tt.args.event)
if tt.args.user.Address.Locality != tt.result.Address.Locality {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
if tt.args.user.Address.Country != tt.result.Address.Country {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
if tt.args.user.Address.PostalCode != tt.result.Address.PostalCode {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@@ -0,0 +1,31 @@
package model
import (
"net"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
type AuthRequest struct {
ID string `json:"id,omitempty"`
UserAgentID string `json:"userAgentID,omitempty"`
SelectedIDPConfigID string `json:"selectedIDPConfigID,omitempty"`
*BrowserInfo
}
type BrowserInfo struct {
UserAgent string `json:"userAgent,omitempty"`
AcceptLanguage string `json:"acceptLanguage,omitempty"`
RemoteIP net.IP `json:"remoteIP,omitempty"`
}
func (a *AuthRequest) SetData(event eventstore.Event) error {
if err := event.Unmarshal(a); err != nil {
logging.Log("EVEN-T5df6").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-yGmhh", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,67 @@
package model
import (
"encoding/json"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/crypto"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Email struct {
es_models.ObjectRoot
EmailAddress string `json:"email,omitempty"`
IsEmailVerified bool `json:"-"`
}
type EmailCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
}
func (e *Email) Changes(changed *Email) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if changed.EmailAddress != "" && e.EmailAddress != changed.EmailAddress {
changes["email"] = changed.EmailAddress
}
return changes
}
func (u *Human) appendUserEmailChangedEvent(event *es_models.Event) error {
u.Email = new(Email)
return u.Email.setData(event)
}
func (u *Human) appendUserEmailCodeAddedEvent(event *es_models.Event) error {
u.EmailCode = new(EmailCode)
return u.EmailCode.SetData(event)
}
func (u *Human) appendUserEmailVerifiedEvent() {
u.IsEmailVerified = true
}
func (a *Email) setData(event *es_models.Event) error {
a.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-dlo9s").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-sl9xw", "could not unmarshal event")
}
return nil
}
func (a *EmailCode) SetData(event *es_models.Event) error {
a.ObjectRoot.AppendEvent(event)
a.CreationDate = event.CreationDate
if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-lo9s").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-s8uws", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,153 @@
package model
import (
"encoding/json"
"testing"
"time"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
func TestEmailChanges(t *testing.T) {
type args struct {
existingEmail *Email
new *Email
}
type res struct {
changesLen int
}
tests := []struct {
name string
args args
res res
}{
{
name: "all fields changed",
args: args{
existingEmail: &Email{EmailAddress: "Email", IsEmailVerified: true},
new: &Email{EmailAddress: "EmailChanged", IsEmailVerified: false},
},
res: res{
changesLen: 1,
},
},
{
name: "no fields changed",
args: args{
existingEmail: &Email{EmailAddress: "Email", IsEmailVerified: true},
new: &Email{EmailAddress: "Email", IsEmailVerified: false},
},
res: res{
changesLen: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := tt.args.existingEmail.Changes(tt.args.new)
if len(changes) != tt.res.changesLen {
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
}
})
}
}
func TestAppendUserEmailChangedEvent(t *testing.T) {
type args struct {
user *Human
email *Email
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user email event",
args: args{
user: &Human{Email: &Email{EmailAddress: "EmailAddress"}},
email: &Email{EmailAddress: "EmailAddressChanged"},
event: &es_models.Event{},
},
result: &Human{Email: &Email{EmailAddress: "EmailAddressChanged"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.email != nil {
data, _ := json.Marshal(tt.args.email)
tt.args.event.Data = data
}
tt.args.user.appendUserEmailChangedEvent(tt.args.event)
if tt.args.user.Email.EmailAddress != tt.result.Email.EmailAddress {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUserEmailCodeAddedEvent(t *testing.T) {
type args struct {
user *Human
code *EmailCode
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user email code added event",
args: args{
user: &Human{Email: &Email{EmailAddress: "EmailAddress"}},
code: &EmailCode{Expiry: time.Hour * 1},
event: &es_models.Event{},
},
result: &Human{EmailCode: &EmailCode{Expiry: time.Hour * 1}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.code != nil {
data, _ := json.Marshal(tt.args.code)
tt.args.event.Data = data
}
tt.args.user.appendUserEmailCodeAddedEvent(tt.args.event)
if tt.args.user.EmailCode.Expiry != tt.result.EmailCode.Expiry {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUserEmailVerifiedEvent(t *testing.T) {
type args struct {
user *Human
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user email event",
args: args{
user: &Human{Email: &Email{EmailAddress: "EmailAddress"}},
event: &es_models.Event{},
},
result: &Human{Email: &Email{EmailAddress: "EmailAddress", IsEmailVerified: true}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendUserEmailVerifiedEvent()
if tt.args.user.Email.IsEmailVerified != tt.result.Email.IsEmailVerified {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@@ -0,0 +1,60 @@
package model
import (
"encoding/json"
"github.com/zitadel/logging"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/zerrors"
)
type ExternalIDP struct {
es_models.ObjectRoot
IDPConfigID string `json:"idpConfigId,omitempty"`
UserID string `json:"userId,omitempty"`
DisplayName string `json:"displayName,omitempty"`
}
func GetExternalIDP(idps []*ExternalIDP, id string) (int, *ExternalIDP) {
for i, idp := range idps {
if idp.UserID == id {
return i, idp
}
}
return -1, nil
}
func (u *Human) appendExternalIDPAddedEvent(event *es_models.Event) error {
idp := new(ExternalIDP)
err := idp.setData(event)
if err != nil {
return err
}
idp.ObjectRoot.CreationDate = event.CreationDate
u.ExternalIDPs = append(u.ExternalIDPs, idp)
return nil
}
func (u *Human) appendExternalIDPRemovedEvent(event *es_models.Event) error {
idp := new(ExternalIDP)
err := idp.setData(event)
if err != nil {
return err
}
if i, externalIdp := GetExternalIDP(u.ExternalIDPs, idp.UserID); externalIdp != nil {
u.ExternalIDPs[i] = u.ExternalIDPs[len(u.ExternalIDPs)-1]
u.ExternalIDPs[len(u.ExternalIDPs)-1] = nil
u.ExternalIDPs = u.ExternalIDPs[:len(u.ExternalIDPs)-1]
}
return nil
}
func (pw *ExternalIDP) setData(event *es_models.Event) error {
pw.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, pw); err != nil {
logging.Log("EVEN-Msi9d").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-A9osf", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,90 @@
package model
import (
"encoding/json"
"testing"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
func TestAppendExternalIDPAddedEvent(t *testing.T) {
type args struct {
user *Human
externalIDP *ExternalIDP
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append external idp added event",
args: args{
user: &Human{},
externalIDP: &ExternalIDP{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"},
event: &es_models.Event{},
},
result: &Human{ExternalIDPs: []*ExternalIDP{{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.externalIDP != nil {
data, _ := json.Marshal(tt.args.externalIDP)
tt.args.event.Data = data
}
tt.args.user.appendExternalIDPAddedEvent(tt.args.event)
if len(tt.args.user.ExternalIDPs) == 0 {
t.Error("got wrong result expected external idps on user ")
}
if tt.args.user.ExternalIDPs[0].UserID != tt.result.ExternalIDPs[0].UserID {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].UserID, tt.args.user.ExternalIDPs[0].UserID)
}
if tt.args.user.ExternalIDPs[0].IDPConfigID != tt.result.ExternalIDPs[0].IDPConfigID {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].IDPConfigID, tt.args.user.ExternalIDPs[0].IDPConfigID)
}
if tt.args.user.ExternalIDPs[0].DisplayName != tt.result.ExternalIDPs[0].DisplayName {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].DisplayName, tt.args.user.ExternalIDPs[0].IDPConfigID)
}
})
}
}
func TestAppendExternalIDPRemovedEvent(t *testing.T) {
type args struct {
user *Human
externalIDP *ExternalIDP
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append external idp removed event",
args: args{
user: &Human{
ExternalIDPs: []*ExternalIDP{
{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"},
}},
externalIDP: &ExternalIDP{UserID: "UserID"},
event: &es_models.Event{},
},
result: &Human{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.externalIDP != nil {
data, _ := json.Marshal(tt.args.externalIDP)
tt.args.event.Data = data
}
tt.args.user.appendExternalIDPRemovedEvent(tt.args.event)
if len(tt.args.user.ExternalIDPs) != 0 {
t.Error("got wrong result expected 0 external idps on user ")
}
})
}
}

View File

@@ -0,0 +1,54 @@
package model
import (
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
type OTP struct {
es_models.ObjectRoot
Secret *crypto.CryptoValue `json:"otpSecret,omitempty"`
State int32 `json:"-"`
}
type OTPVerified struct {
UserAgentID string `json:"userAgentID,omitempty"`
}
func (u *Human) appendOTPAddedEvent(event eventstore.Event) error {
u.OTP = &OTP{
State: int32(model.MFAStateNotReady),
}
return u.OTP.setData(event)
}
func (u *Human) appendOTPVerifiedEvent() {
u.OTP.State = int32(model.MFAStateReady)
}
func (u *Human) appendOTPRemovedEvent() {
u.OTP = nil
}
func (o *OTP) setData(event eventstore.Event) error {
o.ObjectRoot.AppendEvent(event)
if err := event.Unmarshal(o); err != nil {
logging.Log("EVEN-d9soe").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event")
}
return nil
}
func (o *OTPVerified) SetData(event eventstore.Event) error {
if err := event.Unmarshal(o); err != nil {
logging.Log("EVEN-BF421").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-GB6hj", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,110 @@
package model
import (
"encoding/json"
"testing"
"github.com/zitadel/zitadel/internal/crypto"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/user/model"
)
func TestAppendMFAOTPAddedEvent(t *testing.T) {
type args struct {
user *Human
otp *OTP
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user otp event",
args: args{
user: &Human{},
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
event: &es_models.Event{},
},
result: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFAStateNotReady)}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.otp != nil {
data, _ := json.Marshal(tt.args.otp)
tt.args.event.Data = data
}
tt.args.user.appendOTPAddedEvent(tt.args.event)
if tt.args.user.OTP.State != tt.result.OTP.State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.OTP.State, tt.args.user.OTP.State)
}
})
}
}
func TestAppendMFAOTPVerifyEvent(t *testing.T) {
type args struct {
user *Human
otp *OTP
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append otp verify event",
args: args{
user: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}},
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
event: &es_models.Event{},
},
result: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFAStateReady)}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.otp != nil {
data, _ := json.Marshal(tt.args.otp)
tt.args.event.Data = data
}
tt.args.user.appendOTPVerifiedEvent()
if tt.args.user.OTP.State != tt.result.OTP.State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.OTP.State, tt.args.user.OTP.State)
}
})
}
}
func TestAppendMFAOTPRemoveEvent(t *testing.T) {
type args struct {
user *Human
otp *OTP
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append otp verify event",
args: args{
user: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}},
event: &es_models.Event{},
},
result: &Human{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendOTPRemovedEvent()
if tt.args.user.OTP != nil {
t.Errorf("got wrong result: actual: %v ", tt.result.OTP)
}
})
}
}

View File

@@ -0,0 +1,76 @@
package model
import (
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Password struct {
es_models.ObjectRoot
Secret *crypto.CryptoValue `json:"secret,omitempty"`
EncodedHash string `json:"encodedHash,omitempty"`
ChangeRequired bool `json:"changeRequired,omitempty"`
}
type PasswordCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
NotificationType int32 `json:"notificationType,omitempty"`
}
type PasswordChange struct {
Password
UserAgentID string `json:"userAgentID,omitempty"`
}
func (u *Human) appendUserPasswordChangedEvent(event eventstore.Event) error {
u.Password = new(Password)
err := u.Password.setData(event)
if err != nil {
return err
}
u.Password.ObjectRoot.CreationDate = event.CreatedAt()
return nil
}
func (u *Human) appendPasswordSetRequestedEvent(event eventstore.Event) error {
u.PasswordCode = new(PasswordCode)
return u.PasswordCode.SetData(event)
}
func (pw *Password) setData(event eventstore.Event) error {
pw.ObjectRoot.AppendEvent(event)
if err := event.Unmarshal(pw); err != nil {
logging.Log("EVEN-dks93").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-sl9xlo2rsw", "could not unmarshal event")
}
return nil
}
func (c *PasswordCode) SetData(event eventstore.Event) error {
c.ObjectRoot.AppendEvent(event)
c.CreationDate = event.CreatedAt()
if err := event.Unmarshal(c); err != nil {
logging.Log("EVEN-lo0y2").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-q21dr", "could not unmarshal event")
}
return nil
}
func (pw *PasswordChange) SetData(event eventstore.Event) error {
if err := event.Unmarshal(pw); err != nil {
logging.Log("EVEN-ADs31").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-BDd32", "could not unmarshal event")
}
pw.ObjectRoot.AppendEvent(event)
return nil
}

View File

@@ -0,0 +1,79 @@
package model
import (
"encoding/json"
"testing"
"time"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
func TestAppendUserPasswordChangedEvent(t *testing.T) {
type args struct {
user *Human
pw *Password
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append init user code event",
args: args{
user: &Human{},
pw: &Password{ChangeRequired: true},
event: &es_models.Event{},
},
result: &Human{Password: &Password{ChangeRequired: true}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.pw != nil {
data, _ := json.Marshal(tt.args.pw)
tt.args.event.Data = data
}
tt.args.user.appendUserPasswordChangedEvent(tt.args.event)
if tt.args.user.Password.ChangeRequired != tt.result.Password.ChangeRequired {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendPasswordSetRequestedEvent(t *testing.T) {
type args struct {
user *Human
code *PasswordCode
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user phone code added event",
args: args{
user: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
code: &PasswordCode{Expiry: time.Hour * 1},
event: &es_models.Event{},
},
result: &Human{PasswordCode: &PasswordCode{Expiry: time.Hour * 1}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.code != nil {
data, _ := json.Marshal(tt.args.code)
tt.args.event.Data = data
}
tt.args.user.appendPasswordSetRequestedEvent(tt.args.event)
if tt.args.user.PasswordCode.Expiry != tt.result.PasswordCode.Expiry {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@@ -0,0 +1,72 @@
package model
import (
"encoding/json"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/crypto"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Phone struct {
es_models.ObjectRoot
PhoneNumber string `json:"phone,omitempty"`
IsPhoneVerified bool `json:"-"`
}
type PhoneCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
}
func (p *Phone) Changes(changed *Phone) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if changed.PhoneNumber != "" && p.PhoneNumber != changed.PhoneNumber {
changes["phone"] = changed.PhoneNumber
}
return changes
}
func (u *Human) appendUserPhoneChangedEvent(event *es_models.Event) error {
u.Phone = new(Phone)
return u.Phone.setData(event)
}
func (u *Human) appendUserPhoneCodeAddedEvent(event *es_models.Event) error {
u.PhoneCode = new(PhoneCode)
return u.PhoneCode.SetData(event)
}
func (u *Human) appendUserPhoneVerifiedEvent() {
u.IsPhoneVerified = true
}
func (u *Human) appendUserPhoneRemovedEvent() {
u.Phone = nil
u.PhoneCode = nil
}
func (p *Phone) setData(event *es_models.Event) error {
p.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, p); err != nil {
logging.Log("EVEN-lco9s").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-lre56", "could not unmarshal event")
}
return nil
}
func (c *PhoneCode) SetData(event *es_models.Event) error {
c.ObjectRoot.AppendEvent(event)
c.CreationDate = event.CreationDate
if err := json.Unmarshal(event.Data, c); err != nil {
logging.Log("EVEN-sk8ws").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-7hdj3", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,153 @@
package model
import (
"encoding/json"
"testing"
"time"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
func TestPhoneChanges(t *testing.T) {
type args struct {
existingPhone *Phone
newPhone *Phone
}
type res struct {
changesLen int
}
tests := []struct {
name string
args args
res res
}{
{
name: "all fields changed",
args: args{
existingPhone: &Phone{PhoneNumber: "Phone", IsPhoneVerified: true},
newPhone: &Phone{PhoneNumber: "PhoneChanged", IsPhoneVerified: false},
},
res: res{
changesLen: 1,
},
},
{
name: "no fields changed",
args: args{
existingPhone: &Phone{PhoneNumber: "Phone", IsPhoneVerified: true},
newPhone: &Phone{PhoneNumber: "Phone", IsPhoneVerified: false},
},
res: res{
changesLen: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := tt.args.existingPhone.Changes(tt.args.newPhone)
if len(changes) != tt.res.changesLen {
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
}
})
}
}
func TestAppendUserPhoneChangedEvent(t *testing.T) {
type args struct {
user *Human
phone *Phone
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user phone event",
args: args{
user: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
phone: &Phone{PhoneNumber: "PhoneNumberChanged"},
event: &es_models.Event{},
},
result: &Human{Phone: &Phone{PhoneNumber: "PhoneNumberChanged"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.phone != nil {
data, _ := json.Marshal(tt.args.phone)
tt.args.event.Data = data
}
tt.args.user.appendUserPhoneChangedEvent(tt.args.event)
if tt.args.user.Phone.PhoneNumber != tt.result.Phone.PhoneNumber {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUserPhoneCodeAddedEvent(t *testing.T) {
type args struct {
user *Human
code *PhoneCode
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user phone code added event",
args: args{
user: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
code: &PhoneCode{Expiry: time.Hour * 1},
event: &es_models.Event{},
},
result: &Human{PhoneCode: &PhoneCode{Expiry: time.Hour * 1}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.code != nil {
data, _ := json.Marshal(tt.args.code)
tt.args.event.Data = data
}
tt.args.user.appendUserPhoneCodeAddedEvent(tt.args.event)
if tt.args.user.PhoneCode.Expiry != tt.result.PhoneCode.Expiry {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUserPhoneVerifiedEvent(t *testing.T) {
type args struct {
user *Human
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user phone event",
args: args{
user: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
event: &es_models.Event{},
},
result: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber", IsPhoneVerified: true}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendUserPhoneVerifiedEvent()
if tt.args.user.Phone.IsPhoneVerified != tt.result.Phone.IsPhoneVerified {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@@ -0,0 +1,73 @@
package model
import (
"encoding/json"
"golang.org/x/text/language"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
type Profile struct {
es_models.ObjectRoot
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
NickName string `json:"nickName,omitempty"`
DisplayName string `json:"displayName,omitempty"`
PreferredLanguage LanguageTag `json:"preferredLanguage,omitempty"`
Gender int32 `json:"gender,omitempty"`
}
func (p *Profile) Changes(changed *Profile) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if changed.FirstName != "" && p.FirstName != changed.FirstName {
changes["firstName"] = changed.FirstName
}
if changed.LastName != "" && p.LastName != changed.LastName {
changes["lastName"] = changed.LastName
}
if changed.NickName != p.NickName {
changes["nickName"] = changed.NickName
}
if changed.DisplayName != "" && p.DisplayName != changed.DisplayName {
changes["displayName"] = changed.DisplayName
}
if language.Tag(changed.PreferredLanguage) != language.Und && changed.PreferredLanguage != p.PreferredLanguage {
changes["preferredLanguage"] = changed.PreferredLanguage
}
if changed.Gender != p.Gender {
changes["gender"] = changed.Gender
}
return changes
}
type LanguageTag language.Tag
func (t *LanguageTag) UnmarshalJSON(data []byte) error {
var tag string
err := json.Unmarshal(data, &tag)
if err != nil {
return err
}
*t = LanguageTag(language.Make(tag))
return nil
}
func (t LanguageTag) MarshalJSON() ([]byte, error) {
return json.Marshal(language.Tag(t))
}
func (t *LanguageTag) MarshalBinary() ([]byte, error) {
if t == nil {
return nil, nil
}
return []byte(language.Tag(*t).String()), nil
}
// UnmarshalBinary modifies the receiver so it must take a pointer receiver.
func (t *LanguageTag) UnmarshalBinary(data []byte) error {
*t = LanguageTag(language.Make(string(data)))
return nil
}

View File

@@ -0,0 +1,63 @@
package model
import (
"testing"
"golang.org/x/text/language"
user_model "github.com/zitadel/zitadel/internal/user/model"
)
func TestProfileChanges(t *testing.T) {
type args struct {
existingProfile *Profile
newProfile *Profile
}
type res struct {
changesLen int
}
tests := []struct {
name string
args args
res res
}{
{
name: "all attributes changed",
args: args{
existingProfile: &Profile{FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
newProfile: &Profile{FirstName: "FirstNameChanged", LastName: "LastNameChanged", NickName: "NickNameChanged", DisplayName: "DisplayNameChanged", PreferredLanguage: LanguageTag(language.English), Gender: int32(user_model.GenderMale)},
},
res: res{
changesLen: 6,
},
},
{
name: "no changes",
args: args{
existingProfile: &Profile{FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
newProfile: &Profile{FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
},
res: res{
changesLen: 0,
},
},
{
name: "empty names",
args: args{
existingProfile: &Profile{FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
newProfile: &Profile{FirstName: "", LastName: "", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
},
res: res{
changesLen: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := tt.args.existingProfile.Changes(tt.args.newProfile)
if len(changes) != tt.res.changesLen {
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
}
})
}
}

View File

@@ -0,0 +1,55 @@
package model
import (
"encoding/json"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
user_repo "github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Token struct {
es_models.ObjectRoot
TokenID string `json:"tokenId" gorm:"column:token_id"`
ApplicationID string `json:"applicationId" gorm:"column:application_id"`
UserAgentID string `json:"userAgentId" gorm:"column:user_agent_id"`
Audience database.TextArray[string] `json:"audience" gorm:"column:audience"`
Scopes database.TextArray[string] `json:"scopes" gorm:"column:scopes"`
Expiration time.Time `json:"expiration" gorm:"column:expiration"`
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
}
func (t *Token) AppendEvents(events ...*es_models.Event) error {
for _, event := range events {
if err := t.AppendEvent(event); err != nil {
return err
}
}
return nil
}
func (t *Token) AppendEvent(event *es_models.Event) error {
if event.Typ == user_repo.UserTokenAddedType {
err := t.setData(event)
if err != nil {
return err
}
t.CreationDate = event.CreationDate
}
return nil
}
func (t *Token) setData(event *es_models.Event) error {
t.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, t); err != nil {
logging.Log("EVEN-4Fm9s").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-5Gms9", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,111 @@
package model
import (
"encoding/json"
"strings"
"github.com/zitadel/logging"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
UserVersion = "v2"
)
type User struct {
es_models.ObjectRoot
State int32 `json:"-"`
UserName string `json:"userName"`
*Human
*Machine
}
func (u *User) AppendEvents(events ...*es_models.Event) error {
for _, event := range events {
if err := u.AppendEvent(event); err != nil {
return err
}
}
return nil
}
func (u *User) AppendEvent(event *es_models.Event) error {
u.ObjectRoot.AppendEvent(event)
switch event.Type() {
case user.UserV1AddedType,
user.HumanAddedType,
user.MachineAddedEventType,
user.UserV1RegisteredType,
user.HumanRegisteredType,
user.UserV1ProfileChangedType,
user.UserDomainClaimedType,
user.UserUserNameChangedType:
err := u.setData(event)
if err != nil {
return err
}
case user.UserDeactivatedType:
u.appendDeactivatedEvent()
case user.UserReactivatedType:
u.appendReactivatedEvent()
case user.UserLockedType:
u.appendLockedEvent()
case user.UserUnlockedType:
u.appendUnlockedEvent()
case user.UserRemovedType:
u.appendRemovedEvent()
}
if u.Human != nil {
u.Human.user = u
return u.Human.AppendEvent(event)
} else if u.Machine != nil {
u.Machine.user = u
return u.Machine.AppendEvent(event)
}
if strings.HasPrefix(string(event.Typ), "user.human") || event.AggregateVersion == "v1" {
u.Human = &Human{user: u}
return u.Human.AppendEvent(event)
}
if strings.HasPrefix(string(event.Typ), "user.machine") {
u.Machine = &Machine{user: u}
return u.Machine.AppendEvent(event)
}
return zerrors.ThrowNotFound(nil, "MODEL-x9TaX", "Errors.UserType.Undefined")
}
func (u *User) setData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, u); err != nil {
logging.Log("EVEN-ZDzQy").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-yGmhh", "could not unmarshal event")
}
return nil
}
func (u *User) appendDeactivatedEvent() {
u.State = int32(model.UserStateInactive)
}
func (u *User) appendReactivatedEvent() {
u.State = int32(model.UserStateActive)
}
func (u *User) appendLockedEvent() {
u.State = int32(model.UserStateLocked)
}
func (u *User) appendUnlockedEvent() {
u.State = int32(model.UserStateActive)
}
func (u *User) appendRemovedEvent() {
u.State = int32(model.UserStateDeleted)
}

View File

@@ -0,0 +1,185 @@
package model
import (
"encoding/json"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/crypto"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Human struct {
user *User `json:"-"`
*Password
*Profile
*Email
*Phone
*Address
ExternalIDPs []*ExternalIDP `json:"-"`
InitCode *InitUserCode `json:"-"`
EmailCode *EmailCode `json:"-"`
PhoneCode *PhoneCode `json:"-"`
PasswordCode *PasswordCode `json:"-"`
OTP *OTP `json:"-"`
U2FTokens []*WebAuthNToken `json:"-"`
PasswordlessTokens []*WebAuthNToken `json:"-"`
U2FLogins []*WebAuthNLogin `json:"-"`
PasswordlessLogins []*WebAuthNLogin `json:"-"`
}
type InitUserCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
}
func (p *Human) AppendEvents(events ...*es_models.Event) error {
for _, event := range events {
if err := p.AppendEvent(event); err != nil {
return err
}
}
return nil
}
func (h *Human) AppendEvent(event *es_models.Event) (err error) {
switch event.Type() {
case user.UserV1AddedType,
user.UserV1RegisteredType,
user.UserV1ProfileChangedType,
user.HumanAddedType,
user.HumanRegisteredType,
user.HumanProfileChangedType:
err = h.setData(event)
case user.UserV1InitialCodeAddedType,
user.HumanInitialCodeAddedType:
err = h.appendInitUsercodeCreatedEvent(event)
case user.UserV1PasswordChangedType,
user.HumanPasswordChangedType:
err = h.appendUserPasswordChangedEvent(event)
case user.UserV1PasswordCodeAddedType,
user.HumanPasswordCodeAddedType:
err = h.appendPasswordSetRequestedEvent(event)
case user.UserV1EmailChangedType,
user.HumanEmailChangedType:
err = h.appendUserEmailChangedEvent(event)
case user.UserV1EmailCodeAddedType,
user.HumanEmailCodeAddedType:
err = h.appendUserEmailCodeAddedEvent(event)
case user.UserV1EmailVerifiedType,
user.HumanEmailVerifiedType:
h.appendUserEmailVerifiedEvent()
case user.UserV1PhoneChangedType,
user.HumanPhoneChangedType:
err = h.appendUserPhoneChangedEvent(event)
case user.UserV1PhoneCodeAddedType,
user.HumanPhoneCodeAddedType:
err = h.appendUserPhoneCodeAddedEvent(event)
case user.UserV1PhoneVerifiedType,
user.HumanPhoneVerifiedType:
h.appendUserPhoneVerifiedEvent()
case user.UserV1PhoneRemovedType,
user.HumanPhoneRemovedType:
h.appendUserPhoneRemovedEvent()
case user.UserV1AddressChangedType,
user.HumanAddressChangedType:
err = h.appendUserAddressChangedEvent(event)
case user.UserV1MFAOTPAddedType,
user.HumanMFAOTPAddedType:
err = h.appendOTPAddedEvent(event)
case user.UserV1MFAOTPVerifiedType,
user.HumanMFAOTPVerifiedType:
h.appendOTPVerifiedEvent()
case user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPRemovedType:
h.appendOTPRemovedEvent()
case user.UserIDPLinkAddedType:
err = h.appendExternalIDPAddedEvent(event)
case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType:
err = h.appendExternalIDPRemovedEvent(event)
case user.HumanU2FTokenAddedType:
err = h.appendU2FAddedEvent(event)
case user.HumanU2FTokenVerifiedType:
err = h.appendU2FVerifiedEvent(event)
case user.HumanU2FTokenSignCountChangedType:
err = h.appendU2FChangeSignCountEvent(event)
case user.HumanU2FTokenRemovedType:
err = h.appendU2FRemovedEvent(event)
case user.HumanPasswordlessTokenAddedType:
err = h.appendPasswordlessAddedEvent(event)
case user.HumanPasswordlessTokenVerifiedType:
err = h.appendPasswordlessVerifiedEvent(event)
case user.HumanPasswordlessTokenSignCountChangedType:
err = h.appendPasswordlessChangeSignCountEvent(event)
case user.HumanPasswordlessTokenRemovedType:
err = h.appendPasswordlessRemovedEvent(event)
case user.HumanU2FTokenBeginLoginType:
err = h.appendU2FLoginEvent(event)
case user.HumanPasswordlessTokenBeginLoginType:
err = h.appendPasswordlessLoginEvent(event)
}
if err != nil {
return err
}
h.ComputeObject()
return nil
}
func (h *Human) ComputeObject() {
if h.user.State == int32(model.UserStateUnspecified) || h.user.State == int32(model.UserStateInitial) {
if h.Email != nil && h.IsEmailVerified {
h.user.State = int32(model.UserStateActive)
} else {
h.user.State = int32(model.UserStateInitial)
}
}
if h.Password != nil && h.Password.ObjectRoot.IsZero() {
h.Password.ObjectRoot = h.user.ObjectRoot
}
if h.Profile != nil && h.Profile.ObjectRoot.IsZero() {
h.Profile.ObjectRoot = h.user.ObjectRoot
}
if h.Email != nil && h.Email.ObjectRoot.IsZero() {
h.Email.ObjectRoot = h.user.ObjectRoot
}
if h.Phone != nil && h.Phone.ObjectRoot.IsZero() {
h.Phone.ObjectRoot = h.user.ObjectRoot
}
if h.Address != nil && h.Address.ObjectRoot.IsZero() {
h.Address.ObjectRoot = h.user.ObjectRoot
}
}
func (u *Human) setData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, u); err != nil {
logging.Log("EVEN-8ujgd").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-sj4jd", "could not unmarshal event")
}
return nil
}
func (u *Human) appendInitUsercodeCreatedEvent(event *es_models.Event) error {
initCode := new(InitUserCode)
err := initCode.SetData(event)
if err != nil {
return err
}
initCode.ObjectRoot.CreationDate = event.CreationDate
u.InitCode = initCode
return nil
}
func (c *InitUserCode) SetData(event *es_models.Event) error {
c.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, c); err != nil {
logging.Log("EVEN-7duwe").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-lo34s", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,78 @@
package model
import (
"encoding/json"
"time"
"github.com/zitadel/logging"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
user_repo "github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Machine struct {
user *User `json:"-"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
}
func (sa *Machine) AppendEvents(events ...*es_models.Event) error {
for _, event := range events {
if err := sa.AppendEvent(event); err != nil {
return err
}
}
return nil
}
func (sa *Machine) AppendEvent(event *es_models.Event) (err error) {
switch event.Type() {
case user_repo.MachineAddedEventType, user_repo.MachineChangedEventType:
err = sa.setData(event)
}
return err
}
func (sa *Machine) setData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, sa); err != nil {
logging.Log("EVEN-8ujgd").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-GwjY9", "could not unmarshal event")
}
return nil
}
type MachineKey struct {
es_models.ObjectRoot `json:"-"`
KeyID string `json:"keyId,omitempty"`
Type int32 `json:"type,omitempty"`
ExpirationDate time.Time `json:"expirationDate,omitempty"`
PublicKey []byte `json:"publicKey,omitempty"`
privateKey []byte
}
func (key *MachineKey) AppendEvents(events ...*es_models.Event) error {
for _, event := range events {
err := key.AppendEvent(event)
if err != nil {
return err
}
}
return nil
}
func (key *MachineKey) AppendEvent(event *es_models.Event) (err error) {
key.ObjectRoot.AppendEvent(event)
switch event.Type() {
case user_repo.MachineKeyAddedEventType:
err = json.Unmarshal(event.Data, key)
if err != nil {
return zerrors.ThrowInternal(err, "MODEL-SjI4S", "Errors.Internal")
}
case user_repo.MachineKeyRemovedEventType:
key.ExpirationDate = event.CreationDate
}
return err
}

View File

@@ -0,0 +1,257 @@
package model
import (
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/eventstore"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
type WebAuthNToken struct {
es_models.ObjectRoot
WebauthNTokenID string `json:"webAuthNTokenId"`
Challenge string `json:"challenge"`
State int32 `json:"-"`
KeyID []byte `json:"keyId"`
PublicKey []byte `json:"publicKey"`
AttestationType string `json:"attestationType"`
AAGUID []byte `json:"aaguid"`
SignCount uint32 `json:"signCount"`
WebAuthNTokenName string `json:"webAuthNTokenName"`
}
type WebAuthNVerify struct {
WebAuthNTokenID string `json:"webAuthNTokenId"`
KeyID []byte `json:"keyId"`
PublicKey []byte `json:"publicKey"`
AttestationType string `json:"attestationType"`
AAGUID []byte `json:"aaguid"`
SignCount uint32 `json:"signCount"`
WebAuthNTokenName string `json:"webAuthNTokenName"`
UserAgentID string `json:"userAgentID,omitempty"`
}
type WebAuthNSignCount struct {
WebauthNTokenID string `json:"webAuthNTokenId"`
SignCount uint32 `json:"signCount"`
}
type WebAuthNTokenID struct {
WebauthNTokenID string `json:"webAuthNTokenId"`
}
type WebAuthNLogin struct {
es_models.ObjectRoot
WebauthNTokenID string `json:"webAuthNTokenId"`
Challenge string `json:"challenge"`
*AuthRequest
}
func GetWebauthn(webauthnTokens []*WebAuthNToken, id string) (int, *WebAuthNToken) {
for i, webauthn := range webauthnTokens {
if webauthn.WebauthNTokenID == id {
return i, webauthn
}
}
return -1, nil
}
func (w *WebAuthNVerify) SetData(event eventstore.Event) error {
if err := event.Unmarshal(w); err != nil {
logging.Log("EVEN-G342rf").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-B6641", "could not unmarshal event")
}
return nil
}
func (u *Human) appendU2FAddedEvent(event eventstore.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)
if err != nil {
return err
}
webauthn.ObjectRoot.CreationDate = event.CreatedAt()
webauthn.State = int32(model.MFAStateNotReady)
for i, token := range u.U2FTokens {
if token.State == int32(model.MFAStateNotReady) {
u.U2FTokens[i] = webauthn
return nil
}
}
u.U2FTokens = append(u.U2FTokens, webauthn)
return nil
}
func (u *Human) appendU2FVerifiedEvent(event eventstore.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)
if err != nil {
return err
}
if _, token := GetWebauthn(u.U2FTokens, webauthn.WebauthNTokenID); token != nil {
err := token.setData(event)
if err != nil {
return err
}
token.State = int32(model.MFAStateReady)
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "MODEL-4hu9s", "Errors.Users.MFA.U2F.NotExisting")
}
func (u *Human) appendU2FChangeSignCountEvent(event eventstore.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)
if err != nil {
return err
}
if _, token := GetWebauthn(u.U2FTokens, webauthn.WebauthNTokenID); token != nil {
token.setData(event)
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "MODEL-5Ms8h", "Errors.Users.MFA.U2F.NotExisting")
}
func (u *Human) appendU2FRemovedEvent(event eventstore.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)
if err != nil {
return err
}
for i := len(u.U2FTokens) - 1; i >= 0; i-- {
if u.U2FTokens[i].WebauthNTokenID == webauthn.WebauthNTokenID {
copy(u.U2FTokens[i:], u.U2FTokens[i+1:])
u.U2FTokens[len(u.U2FTokens)-1] = nil
u.U2FTokens = u.U2FTokens[:len(u.U2FTokens)-1]
return nil
}
}
return nil
}
func (u *Human) appendPasswordlessAddedEvent(event eventstore.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)
if err != nil {
return err
}
webauthn.ObjectRoot.CreationDate = event.CreatedAt()
webauthn.State = int32(model.MFAStateNotReady)
for i, token := range u.PasswordlessTokens {
if token.State == int32(model.MFAStateNotReady) {
u.PasswordlessTokens[i] = webauthn
return nil
}
}
u.PasswordlessTokens = append(u.PasswordlessTokens, webauthn)
return nil
}
func (u *Human) appendPasswordlessVerifiedEvent(event eventstore.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)
if err != nil {
return err
}
if _, token := GetWebauthn(u.PasswordlessTokens, webauthn.WebauthNTokenID); token != nil {
err := token.setData(event)
if err != nil {
return err
}
token.State = int32(model.MFAStateReady)
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "MODEL-mKns8", "Errors.Users.MFA.Passwordless.NotExisting")
}
func (u *Human) appendPasswordlessChangeSignCountEvent(event eventstore.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)
if err != nil {
return err
}
if _, token := GetWebauthn(u.PasswordlessTokens, webauthn.WebauthNTokenID); token != nil {
err := token.setData(event)
if err != nil {
return err
}
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "MODEL-2Mv9s", "Errors.Users.MFA.Passwordless.NotExisting")
}
func (u *Human) appendPasswordlessRemovedEvent(event eventstore.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)
if err != nil {
return err
}
for i := len(u.PasswordlessTokens) - 1; i >= 0; i-- {
if u.PasswordlessTokens[i].WebauthNTokenID == webauthn.WebauthNTokenID {
copy(u.PasswordlessTokens[i:], u.PasswordlessTokens[i+1:])
u.PasswordlessTokens[len(u.PasswordlessTokens)-1] = nil
u.PasswordlessTokens = u.PasswordlessTokens[:len(u.PasswordlessTokens)-1]
return nil
}
}
return nil
}
func (w *WebAuthNToken) setData(event eventstore.Event) error {
w.ObjectRoot.AppendEvent(event)
if err := event.Unmarshal(w); err != nil {
logging.Log("EVEN-4M9is").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event")
}
return nil
}
func (u *Human) appendU2FLoginEvent(event eventstore.Event) error {
webauthn := new(WebAuthNLogin)
webauthn.ObjectRoot.AppendEvent(event)
err := webauthn.setData(event)
if err != nil {
return err
}
webauthn.ObjectRoot.CreationDate = event.CreatedAt()
for i, token := range u.U2FLogins {
if token.AuthRequest.ID == webauthn.AuthRequest.ID {
u.U2FLogins[i] = webauthn
return nil
}
}
u.U2FLogins = append(u.U2FLogins, webauthn)
return nil
}
func (u *Human) appendPasswordlessLoginEvent(event eventstore.Event) error {
webauthn := new(WebAuthNLogin)
webauthn.ObjectRoot.AppendEvent(event)
err := webauthn.setData(event)
if err != nil {
return err
}
webauthn.ObjectRoot.CreationDate = event.CreatedAt()
for i, token := range u.PasswordlessLogins {
if token.AuthRequest.ID == webauthn.AuthRequest.ID {
u.PasswordlessLogins[i] = webauthn
return nil
}
}
u.PasswordlessLogins = append(u.PasswordlessLogins, webauthn)
return nil
}
func (w *WebAuthNLogin) setData(event eventstore.Event) error {
w.ObjectRoot.AppendEvent(event)
if err := event.Unmarshal(w); err != nil {
logging.Log("EVEN-hmSlo").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event")
}
return nil
}

View File

@@ -0,0 +1,151 @@
package model
import (
"encoding/json"
"testing"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/pkg/grpc/user"
)
func TestAppendMFAU2FAddedEvent(t *testing.T) {
type args struct {
user *Human
u2f *WebAuthNToken
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append user u2f event",
args: args{
user: &Human{},
u2f: &WebAuthNToken{WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge"},
event: &es_models.Event{},
},
result: &Human{
U2FTokens: []*WebAuthNToken{
{WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge", State: int32(user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY)},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.u2f != nil {
data, _ := json.Marshal(tt.args.u2f)
tt.args.event.Data = data
}
tt.args.user.appendU2FAddedEvent(tt.args.event)
if tt.args.user.U2FTokens[0].State != tt.result.U2FTokens[0].State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].State, tt.args.user.U2FTokens[0].State)
}
})
}
}
func TestAppendMFAU2FVerifyEvent(t *testing.T) {
type args struct {
user *Human
u2f *WebAuthNVerify
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append u2f verify event",
args: args{
user: &Human{
U2FTokens: []*WebAuthNToken{
{WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge", State: int32(user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY)},
},
},
u2f: &WebAuthNVerify{WebAuthNTokenID: "WebauthNTokenID", KeyID: []byte("KeyID"), PublicKey: []byte("PublicKey"), AttestationType: "AttestationType", AAGUID: []byte("AAGUID"), SignCount: 1},
event: &es_models.Event{},
},
result: &Human{
U2FTokens: []*WebAuthNToken{
{
WebauthNTokenID: "WebauthNTokenID",
Challenge: "Challenge",
State: int32(user.AuthFactorState_AUTH_FACTOR_STATE_READY),
KeyID: []byte("KeyID"),
PublicKey: []byte("PublicKey"),
AttestationType: "AttestationType",
AAGUID: []byte("AAGUID"),
SignCount: 1,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.u2f != nil {
data, _ := json.Marshal(tt.args.u2f)
tt.args.event.Data = data
}
tt.args.user.appendU2FVerifiedEvent(tt.args.event)
if tt.args.user.U2FTokens[0].State != tt.result.U2FTokens[0].State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].State, tt.args.user.U2FTokens[0].State)
}
if tt.args.user.U2FTokens[0].AttestationType != tt.result.U2FTokens[0].AttestationType {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].AttestationType, tt.args.user.U2FTokens[0].AttestationType)
}
})
}
}
func TestAppendMFAU2FRemoveEvent(t *testing.T) {
type args struct {
user *Human
u2f *WebAuthNTokenID
event *es_models.Event
}
tests := []struct {
name string
args args
result *Human
}{
{
name: "append u2f remove event",
args: args{
user: &Human{
U2FTokens: []*WebAuthNToken{
{
WebauthNTokenID: "WebauthNTokenID",
Challenge: "Challenge",
State: int32(user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY),
KeyID: []byte("KeyID"),
PublicKey: []byte("PublicKey"),
AttestationType: "AttestationType",
AAGUID: []byte("AAGUID"),
SignCount: 1,
},
},
},
u2f: &WebAuthNTokenID{WebauthNTokenID: "WebauthNTokenID"},
event: &es_models.Event{},
},
result: &Human{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.u2f != nil {
data, _ := json.Marshal(tt.args.u2f)
tt.args.event.Data = data
}
tt.args.user.appendU2FRemovedEvent(tt.args.event)
if len(tt.args.user.U2FTokens) != 0 {
t.Errorf("got wrong result: actual: %v ", tt.result.U2FTokens)
}
})
}
}

View File

@@ -0,0 +1,12 @@
SELECT
s.user_agent_id,
s.user_id,
s.id
FROM auth.user_sessions s
JOIN auth.user_sessions s2
ON s.instance_id = s2.instance_id
AND s.user_agent_id = s2.user_agent_id
WHERE
s2.id = $1
AND s.instance_id = $2
AND s.state = 0;

View File

@@ -0,0 +1,182 @@
package model
import (
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
user_repo "github.com/zitadel/zitadel/internal/repository/user"
usr_model "github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
RefreshTokenKeyTokenID = "id"
RefreshTokenKeyUserID = "user_id"
RefreshTokenKeyApplicationID = "application_id"
RefreshTokenKeyUserAgentID = "user_agent_id"
RefreshTokenKeyExpiration = "expiration"
RefreshTokenKeyResourceOwner = "resource_owner"
RefreshTokenKeyInstanceID = "instance_id"
RefreshTokenKeyCreationDate = "creation_date"
RefreshTokenKeyChangeDate = "change_date"
RefreshTokenKeySequence = "sequence"
RefreshTokenKeyActor = "actor"
RefreshTokenKeyAMR = "amr"
RefreshTokenKeyAuthTime = "auth_time"
RefreshTokenKeyAudience = "audience"
RefreshTokenKeyClientID = "client_id"
RefreshTokenKeyIdleExpiration = "idle_expiration"
RefreshTokenKeyScopes = "scopes"
RefreshTokenKeyToken = "token"
)
type RefreshTokenView struct {
ID string `json:"tokenId" gorm:"column:id;primary_key"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
Token string `json:"-" gorm:"column:token"`
UserID string `json:"-" gorm:"column:user_id"`
ClientID string `json:"clientID" gorm:"column:client_id"`
UserAgentID string `json:"userAgentId" gorm:"column:user_agent_id"`
Audience database.TextArray[string] `json:"audience" gorm:"column:audience"`
Scopes database.TextArray[string] `json:"scopes" gorm:"column:scopes"`
AuthMethodsReferences database.TextArray[string] `json:"authMethodsReference" gorm:"column:amr"`
AuthTime time.Time `json:"authTime" gorm:"column:auth_time"`
IdleExpiration time.Time `json:"-" gorm:"column:idle_expiration"`
Expiration time.Time `json:"-" gorm:"column:expiration"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
Actor TokenActor `json:"actor" gorm:"column:actor"`
}
func RefreshTokenViewsToModel(tokens []*RefreshTokenView) []*usr_model.RefreshTokenView {
result := make([]*usr_model.RefreshTokenView, len(tokens))
for i, g := range tokens {
result[i] = RefreshTokenViewToModel(g)
}
return result
}
func RefreshTokenViewToModel(token *RefreshTokenView) *usr_model.RefreshTokenView {
return &usr_model.RefreshTokenView{
ID: token.ID,
CreationDate: token.CreationDate,
ChangeDate: token.ChangeDate,
ResourceOwner: token.ResourceOwner,
Token: token.Token,
UserID: token.UserID,
ClientID: token.ClientID,
UserAgentID: token.UserAgentID,
Audience: token.Audience,
Scopes: token.Scopes,
AuthMethodsReferences: token.AuthMethodsReferences,
AuthTime: token.AuthTime,
IdleExpiration: token.IdleExpiration,
Expiration: token.Expiration,
Sequence: token.Sequence,
Actor: token.Actor.TokenActor,
}
}
func (t *RefreshTokenView) AppendEventIfMyRefreshToken(event eventstore.Event) (err error) {
// in case anything needs to be change here check if the Reduce function needs the change as well
view := new(RefreshTokenView)
switch event.Type() {
case user_repo.HumanRefreshTokenAddedType:
view.setRootData(event)
err = view.appendAddedEvent(event)
if err != nil {
return err
}
case user_repo.HumanRefreshTokenRenewedType:
view.setRootData(event)
err = view.appendRenewedEvent(event)
if err != nil {
return err
}
case user_repo.HumanRefreshTokenRemovedType,
user_repo.UserRemovedType,
user_repo.UserDeactivatedType,
user_repo.UserLockedType:
view.appendRemovedEvent(event)
default:
return nil
}
if view.ID == t.ID {
return t.AppendEvent(event)
}
return nil
}
func (t *RefreshTokenView) AppendEvent(event eventstore.Event) error {
// in case anything needs to be change here check if the Reduce function needs the change as well
t.ChangeDate = event.CreatedAt()
t.Sequence = event.Sequence()
switch event.Type() {
case user_repo.HumanRefreshTokenAddedType:
t.setRootData(event)
return t.appendAddedEvent(event)
case user_repo.HumanRefreshTokenRenewedType:
t.setRootData(event)
return t.appendRenewedEvent(event)
}
return nil
}
func (t *RefreshTokenView) setRootData(event eventstore.Event) {
t.UserID = event.Aggregate().ID
t.ResourceOwner = event.Aggregate().ResourceOwner
t.InstanceID = event.Aggregate().InstanceID
}
func (t *RefreshTokenView) appendAddedEvent(event eventstore.Event) error {
e := new(user_repo.HumanRefreshTokenAddedEvent)
if err := event.Unmarshal(e); err != nil {
logging.WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-Bbr42", "could not unmarshal event")
}
t.ID = e.TokenID
t.CreationDate = event.CreatedAt()
t.AuthMethodsReferences = e.AuthMethodsReferences
t.AuthTime = e.AuthTime
t.Audience = e.Audience
t.ClientID = e.ClientID
t.Expiration = event.CreatedAt().Add(e.Expiration)
t.IdleExpiration = event.CreatedAt().Add(e.IdleExpiration)
t.Scopes = e.Scopes
t.Token = e.TokenID
t.UserAgentID = e.UserAgentID
t.Actor = TokenActor{e.Actor}
return nil
}
func (t *RefreshTokenView) appendRenewedEvent(event eventstore.Event) error {
e := new(user_repo.HumanRefreshTokenRenewedEvent)
if err := event.Unmarshal(e); err != nil {
logging.WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-Bbrn4", "could not unmarshal event")
}
t.ID = e.TokenID
t.IdleExpiration = event.CreatedAt().Add(e.IdleExpiration)
t.Token = e.RefreshToken
return nil
}
func (t *RefreshTokenView) appendRemovedEvent(event eventstore.Event) {
t.Expiration = event.CreatedAt()
}
func (t *RefreshTokenView) GetRelevantEventTypes() []eventstore.EventType {
return []eventstore.EventType{
user_repo.HumanRefreshTokenAddedType,
user_repo.HumanRefreshTokenRenewedType,
user_repo.HumanRefreshTokenRemovedType,
user_repo.UserRemovedType,
user_repo.UserDeactivatedType,
user_repo.UserLockedType,
}
}

View File

@@ -0,0 +1,71 @@
package model
import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/view/repository"
)
type RefreshTokenSearchRequest model.RefreshTokenSearchRequest
type RefreshTokenSearchQuery model.RefreshTokenSearchQuery
type RefreshTokenSearchKey model.RefreshTokenSearchKey
func (req RefreshTokenSearchRequest) GetLimit() uint64 {
return req.Limit
}
func (req RefreshTokenSearchRequest) GetOffset() uint64 {
return req.Offset
}
func (req RefreshTokenSearchRequest) GetSortingColumn() repository.ColumnKey {
if req.SortingColumn == model.RefreshTokenSearchKeyUnspecified {
return nil
}
return RefreshTokenSearchKey(req.SortingColumn)
}
func (req RefreshTokenSearchRequest) GetAsc() bool {
return req.Asc
}
func (req RefreshTokenSearchRequest) GetQueries() []repository.SearchQuery {
result := make([]repository.SearchQuery, len(req.Queries))
for i, q := range req.Queries {
result[i] = RefreshTokenSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
}
return result
}
func (req RefreshTokenSearchQuery) GetKey() repository.ColumnKey {
return RefreshTokenSearchKey(req.Key)
}
func (req RefreshTokenSearchQuery) GetMethod() domain.SearchMethod {
return req.Method
}
func (req RefreshTokenSearchQuery) GetValue() interface{} {
return req.Value
}
func (key RefreshTokenSearchKey) ToColumnName() string {
switch model.RefreshTokenSearchKey(key) {
case model.RefreshTokenSearchKeyRefreshTokenID:
return RefreshTokenKeyTokenID
case model.RefreshTokenSearchKeyUserAgentID:
return RefreshTokenKeyUserAgentID
case model.RefreshTokenSearchKeyUserID:
return RefreshTokenKeyUserID
case model.RefreshTokenSearchKeyApplicationID:
return RefreshTokenKeyApplicationID
case model.RefreshTokenSearchKeyExpiration:
return RefreshTokenKeyExpiration
case model.RefreshTokenSearchKeyResourceOwner:
return RefreshTokenKeyResourceOwner
case model.RefreshTokenSearchKeyInstanceID:
return RefreshTokenKeyInstanceID
default:
return ""
}
}

View File

@@ -0,0 +1,251 @@
package model
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
user_repo "github.com/zitadel/zitadel/internal/repository/user"
usr_model "github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
TokenKeyTokenID = "id"
TokenKeyUserID = "user_id"
TokenKeyRefreshTokenID = "refresh_token_id"
TokenKeyApplicationID = "application_id"
TokenKeyUserAgentID = "user_agent_id"
TokenKeyExpiration = "expiration"
TokenKeyResourceOwner = "resource_owner"
TokenKeyInstanceID = "instance_id"
TokenKeyCreationDate = "creation_date"
TokenKeyChangeDate = "change_date"
TokenKeySequence = "sequence"
TokenKeyActor = "actor"
TokenKeyID = "id"
TokenKeyAudience = "audience"
TokenKeyPreferredLanguage = "preferred_language"
TokenKeyScopes = "scopes"
TokenKeyIsPat = "is_pat"
)
type TokenView struct {
ID string `json:"tokenId" gorm:"column:id;primary_key"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
UserID string `json:"-" gorm:"column:user_id"`
ApplicationID string `json:"applicationId" gorm:"column:application_id"`
UserAgentID string `json:"userAgentId" gorm:"column:user_agent_id"`
Audience database.TextArray[string] `json:"audience" gorm:"column:audience"`
Scopes database.TextArray[string] `json:"scopes" gorm:"column:scopes"`
Expiration time.Time `json:"expiration" gorm:"column:expiration"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
RefreshTokenID string `json:"refreshTokenID,omitempty" gorm:"refresh_token_id"`
IsPAT bool `json:"-" gorm:"is_pat"`
Deactivated bool `json:"-" gorm:"-"`
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
Actor TokenActor `json:"actor" gorm:"column:actor"`
}
type TokenActor struct {
*domain.TokenActor
}
func (a *TokenActor) Scan(value any) error {
var data []byte
switch v := value.(type) {
case nil:
a.TokenActor = nil
case string:
data = []byte(v)
case []byte:
data = v
default:
return zerrors.ThrowInternalf(nil, "MODEL-yo8Ae", "cannot scan type %T into %T", v, a)
}
if err := json.Unmarshal(data, &a.TokenActor); err != nil {
return zerrors.ThrowInternal(nil, "MODEL-yo8Ae", "cannot unmarshal token actor")
}
return nil
}
func (a TokenActor) Value() (driver.Value, error) {
if a.TokenActor == nil {
return nil, nil
}
data, err := json.Marshal(a.TokenActor)
if err != nil {
return nil, zerrors.ThrowInternal(nil, "MODEL-oD2mi", "cannot marshal token actor")
}
return data, nil
}
func TokenViewToModel(token *TokenView) *usr_model.TokenView {
return &usr_model.TokenView{
ID: token.ID,
CreationDate: token.CreationDate,
ChangeDate: token.ChangeDate,
ResourceOwner: token.ResourceOwner,
UserID: token.UserID,
ApplicationID: token.ApplicationID,
UserAgentID: token.UserAgentID,
Audience: token.Audience,
Scopes: token.Scopes,
Expiration: token.Expiration,
Sequence: token.Sequence,
PreferredLanguage: token.PreferredLanguage,
RefreshTokenID: token.RefreshTokenID,
IsPAT: token.IsPAT,
Actor: token.Actor.TokenActor,
}
}
func (t *TokenView) AppendEventIfMyToken(event eventstore.Event) (err error) {
// in case anything needs to be change here check if the Reduce function needs the change as well
view := new(TokenView)
switch event.Type() {
case user_repo.UserTokenAddedType,
user_repo.PersonalAccessTokenAddedType:
view.setRootData(event)
err = view.setData(event)
case user_repo.UserTokenRemovedType:
return t.appendTokenRemoved(event)
case user_repo.HumanRefreshTokenRemovedType:
return t.appendRefreshTokenRemoved(event)
case user_repo.UserV1SignedOutType,
user_repo.HumanSignedOutType:
id, err := UserAgentIDFromEvent(event)
if err != nil {
return err
}
if t.UserAgentID == id {
t.Deactivated = true
}
return nil
case user_repo.UserRemovedType,
user_repo.UserDeactivatedType,
user_repo.UserLockedType:
t.Deactivated = true
return nil
case user_repo.UserUnlockedType,
user_repo.UserReactivatedType:
if t.ID != "" && event.CreatedAt().Before(t.CreationDate) {
t.Deactivated = false
}
return nil
case user_repo.PersonalAccessTokenRemovedType:
return t.appendPATRemoved(event)
default:
return nil
}
if err != nil {
return err
}
if view.ID == t.ID {
return t.AppendEvent(event)
}
return nil
}
func (t *TokenView) AppendEvent(event eventstore.Event) error {
// in case anything needs to be change here check if the Reduce function needs the change as well
t.ChangeDate = event.CreatedAt()
t.Sequence = event.Sequence()
switch event.Type() {
case user_repo.UserTokenAddedType,
user_repo.PersonalAccessTokenAddedType:
t.setRootData(event)
err := t.setData(event)
if err != nil {
return err
}
t.CreationDate = event.CreatedAt()
t.IsPAT = event.Type() == user_repo.PersonalAccessTokenAddedType
}
return nil
}
func (t *TokenView) setRootData(event eventstore.Event) {
t.UserID = event.Aggregate().ID
t.ResourceOwner = event.Aggregate().ResourceOwner
t.InstanceID = event.Aggregate().InstanceID
}
func (t *TokenView) setData(event eventstore.Event) error {
if err := event.Unmarshal(t); err != nil {
logging.WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(err, "MODEL-5Gms9", "could not unmarshal event")
}
return nil
}
func (t *TokenView) appendTokenRemoved(event eventstore.Event) error {
tokenID, err := tokenIDFromEvent(event)
if err != nil {
return err
}
if tokenID == t.ID {
t.Deactivated = true
}
return nil
}
func (t *TokenView) appendRefreshTokenRemoved(event eventstore.Event) error {
tokenID, err := tokenIDFromEvent(event)
if err != nil {
return err
}
if tokenID == t.RefreshTokenID {
t.Deactivated = true
}
return nil
}
func (t *TokenView) appendPATRemoved(event eventstore.Event) error {
tokenID, err := tokenIDFromEvent(event)
if err != nil {
return err
}
if tokenID == t.ID && t.IsPAT {
t.Deactivated = true
}
return nil
}
func (t *TokenView) GetRelevantEventTypes() []eventstore.EventType {
return []eventstore.EventType{
user_repo.UserTokenAddedType,
user_repo.PersonalAccessTokenAddedType,
user_repo.UserTokenRemovedType,
user_repo.HumanRefreshTokenRemovedType,
user_repo.UserV1SignedOutType,
user_repo.HumanSignedOutType,
user_repo.UserRemovedType,
user_repo.UserDeactivatedType,
user_repo.UserLockedType,
user_repo.UserLockedType,
user_repo.UserReactivatedType,
user_repo.PersonalAccessTokenRemovedType,
}
}
type tokenIDPayload struct {
ID string `json:"tokenId"`
}
func tokenIDFromEvent(event eventstore.Event) (string, error) {
m := new(tokenIDPayload)
if err := event.Unmarshal(&m); err != nil {
logging.WithError(err).Error("could not unmarshal event data")
return "", zerrors.ThrowInternal(nil, "MODEL-SDAfw", "could not unmarshal data")
}
return m.ID, nil
}

View File

@@ -0,0 +1,73 @@
package model
import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/view/repository"
)
type TokenSearchRequest model.TokenSearchRequest
type TokenSearchQuery model.TokenSearchQuery
type TokenSearchKey model.TokenSearchKey
func (req TokenSearchRequest) GetLimit() uint64 {
return req.Limit
}
func (req TokenSearchRequest) GetOffset() uint64 {
return req.Offset
}
func (req TokenSearchRequest) GetSortingColumn() repository.ColumnKey {
if req.SortingColumn == model.TokenSearchKeyUnspecified {
return nil
}
return TokenSearchKey(req.SortingColumn)
}
func (req TokenSearchRequest) GetAsc() bool {
return req.Asc
}
func (req TokenSearchRequest) GetQueries() []repository.SearchQuery {
result := make([]repository.SearchQuery, len(req.Queries))
for i, q := range req.Queries {
result[i] = TokenSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
}
return result
}
func (req TokenSearchQuery) GetKey() repository.ColumnKey {
return TokenSearchKey(req.Key)
}
func (req TokenSearchQuery) GetMethod() domain.SearchMethod {
return req.Method
}
func (req TokenSearchQuery) GetValue() interface{} {
return req.Value
}
func (key TokenSearchKey) ToColumnName() string {
switch model.TokenSearchKey(key) {
case model.TokenSearchKeyTokenID:
return TokenKeyTokenID
case model.TokenSearchKeyUserAgentID:
return TokenKeyUserAgentID
case model.TokenSearchKeyUserID:
return TokenKeyUserID
case model.TokenSearchKeyRefreshTokenID:
return TokenKeyRefreshTokenID
case model.TokenSearchKeyApplicationID:
return TokenKeyApplicationID
case model.TokenSearchKeyExpiration:
return TokenKeyExpiration
case model.TokenSearchKeyResourceOwner:
return TokenKeyResourceOwner
case model.TokenSearchKeyInstanceID:
return TokenKeyInstanceID
default:
return ""
}
}

View File

@@ -0,0 +1,616 @@
package model
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
org_model "github.com/zitadel/zitadel/internal/org/model"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/user/model"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
UserKeyUserID = "id"
UserKeyUserName = "user_name"
UserKeyFirstName = "first_name"
UserKeyLastName = "last_name"
UserKeyNickName = "nick_name"
UserKeyDisplayName = "display_name"
UserKeyEmail = "email"
UserKeyState = "user_state"
UserKeyResourceOwner = "resource_owner"
UserKeyLoginNames = "login_names"
UserKeyPreferredLoginName = "preferred_login_name"
UserKeyType = "user_type"
UserKeyInstanceID = "instance_id"
UserKeyOwnerRemoved = "owner_removed"
UserKeyPasswordSet = "password_set"
UserKeyPasswordInitRequired = "password_init_required"
UserKeyPasswordChange = "password_change"
UserKeyInitRequired = "init_required"
UserKeyPasswordlessInitRequired = "passwordless_init_required"
UserKeyMFAInitSkipped = "mfa_init_skipped"
UserKeyChangeDate = "change_date"
)
type UserView struct {
ID string `json:"-" gorm:"column:id;primary_key"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
State int32 `json:"-" gorm:"column:user_state"`
LastLogin time.Time `json:"-" gorm:"column:last_login"`
LoginNames database.TextArray[string] `json:"-" gorm:"column:login_names"`
PreferredLoginName string `json:"-" gorm:"column:preferred_login_name"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
UserName string `json:"userName" gorm:"column:user_name"`
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
*MachineView
*HumanView
}
type UserState int32
const (
UserStateUnspecified UserState = iota
UserStateActive
UserStateInactive
UserStateDeleted
UserStateLocked
UserStateSuspend
UserStateInitial
)
type HumanView struct {
FirstName string `json:"firstName" gorm:"column:first_name"`
LastName string `json:"lastName" gorm:"column:last_name"`
NickName string `json:"nickName" gorm:"column:nick_name"`
DisplayName string `json:"displayName" gorm:"column:display_name"`
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
Gender int32 `json:"gender" gorm:"column:gender"`
AvatarKey string `json:"storeKey" gorm:"column:avatar_key"`
Email string `json:"email" gorm:"column:email"`
IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"`
VerifiedEmail string `json:"-" gorm:"column:verified_email"`
Phone string `json:"phone" gorm:"column:phone"`
IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"`
Country string `json:"country" gorm:"column:country"`
Locality string `json:"locality" gorm:"column:locality"`
PostalCode string `json:"postalCode" gorm:"column:postal_code"`
Region string `json:"region" gorm:"column:region"`
StreetAddress string `json:"streetAddress" gorm:"column:street_address"`
OTPState int32 `json:"-" gorm:"column:otp_state"`
OTPSMSAdded bool `json:"-" gorm:"column:otp_sms_added"`
OTPEmailAdded bool `json:"-" gorm:"column:otp_email_added"`
U2FTokens WebAuthNTokens `json:"-" gorm:"column:u2f_tokens"`
MFAMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"`
MFAInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"`
InitRequired bool `json:"-" gorm:"column:init_required"`
PasswordlessInitRequired bool `json:"-" gorm:"column:passwordless_init_required"`
PasswordInitRequired bool `json:"-" gorm:"column:password_init_required"`
PasswordSet bool `json:"-" gorm:"column:password_set"`
PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"`
UsernameChangeRequired bool `json:"-" gorm:"column:username_change_required"`
PasswordChanged time.Time `json:"-" gorm:"column:password_change"`
PasswordlessTokens WebAuthNTokens `json:"-" gorm:"column:passwordless_tokens"`
}
type WebAuthNTokens []*WebAuthNView
type WebAuthNView struct {
ID string `json:"webAuthNTokenId"`
Name string `json:"webAuthNTokenName,omitempty"`
State int32 `json:"state,omitempty"`
}
func (t WebAuthNTokens) Value() (driver.Value, error) {
if t == nil {
return nil, nil
}
return json.Marshal(&t)
}
func (t *WebAuthNTokens) Scan(src interface{}) error {
if b, ok := src.([]byte); ok {
return json.Unmarshal(b, t)
}
if s, ok := src.(string); ok {
return json.Unmarshal([]byte(s), t)
}
return nil
}
func (h *HumanView) IsZero() bool {
return h == nil || h.FirstName == ""
}
type MachineView struct {
Name string `json:"name" gorm:"column:machine_name"`
Description string `json:"description" gorm:"column:machine_description"`
}
func (m *MachineView) IsZero() bool {
return m == nil || m.Name == ""
}
func UserToModel(user *UserView) *model.UserView {
userView := &model.UserView{
ID: user.ID,
UserName: user.UserName,
ChangeDate: user.ChangeDate,
CreationDate: user.CreationDate,
ResourceOwner: user.ResourceOwner,
State: model.UserState(user.State),
LastLogin: user.LastLogin,
PreferredLoginName: user.PreferredLoginName,
LoginNames: user.LoginNames,
Sequence: user.Sequence,
}
if !user.HumanView.IsZero() {
userView.HumanView = &model.HumanView{
PasswordSet: user.PasswordSet,
PasswordInitRequired: user.PasswordInitRequired,
PasswordChangeRequired: user.PasswordChangeRequired,
PasswordChanged: user.PasswordChanged,
PasswordlessTokens: WebauthnTokensToModel(user.PasswordlessTokens),
U2FTokens: WebauthnTokensToModel(user.U2FTokens),
FirstName: user.FirstName,
LastName: user.LastName,
NickName: user.NickName,
DisplayName: user.DisplayName,
AvatarKey: user.AvatarKey,
PreferredLanguage: user.PreferredLanguage,
Gender: model.Gender(user.Gender),
Email: user.Email,
IsEmailVerified: user.IsEmailVerified,
VerifiedEmail: user.VerifiedEmail,
Phone: user.Phone,
IsPhoneVerified: user.IsPhoneVerified,
Country: user.Country,
Locality: user.Locality,
PostalCode: user.PostalCode,
Region: user.Region,
StreetAddress: user.StreetAddress,
OTPState: model.MFAState(user.OTPState),
OTPSMSAdded: user.OTPSMSAdded,
OTPEmailAdded: user.OTPEmailAdded,
MFAMaxSetUp: domain.MFALevel(user.MFAMaxSetUp),
MFAInitSkipped: user.MFAInitSkipped,
InitRequired: user.InitRequired,
PasswordlessInitRequired: user.PasswordlessInitRequired,
}
}
if !user.MachineView.IsZero() {
userView.MachineView = &model.MachineView{
Description: user.MachineView.Description,
Name: user.MachineView.Name,
}
}
return userView
}
func WebauthnTokensToModel(tokens []*WebAuthNView) []*model.WebAuthNView {
if tokens == nil {
return nil
}
result := make([]*model.WebAuthNView, len(tokens))
for i, t := range tokens {
result[i] = WebauthnTokenToModel(t)
}
return result
}
func WebauthnTokenToModel(token *WebAuthNView) *model.WebAuthNView {
return &model.WebAuthNView{
TokenID: token.ID,
Name: token.Name,
State: model.MFAState(token.State),
}
}
func (u *UserView) GenerateLoginName(domain string, appendDomain bool) string {
if !appendDomain {
return u.UserName
}
return u.UserName + "@" + domain
}
func (u *UserView) SetLoginNames(userLoginMustBeDomain bool, domains []*org_model.OrgDomain) {
u.LoginNames = make([]string, 0, len(domains))
for _, d := range domains {
if d.Verified {
u.LoginNames = append(u.LoginNames, u.GenerateLoginName(d.Domain, true))
}
}
if !userLoginMustBeDomain {
u.LoginNames = append(u.LoginNames, u.GenerateLoginName(u.UserName, true))
}
}
func (u *UserView) AppendEvent(event eventstore.Event) (err error) {
// in case anything needs to be change here check if the Reduce function needs the change as well
u.ChangeDate = event.CreatedAt()
u.Sequence = event.Sequence()
switch event.Type() {
case user.MachineAddedEventType:
u.CreationDate = event.CreatedAt()
u.setRootData(event)
err = u.setData(event)
if err != nil {
return err
}
case user.UserV1AddedType,
user.UserV1RegisteredType,
user.HumanRegisteredType,
user.HumanAddedType:
u.CreationDate = event.CreatedAt()
u.setRootData(event)
err = u.setData(event)
if err != nil {
return err
}
err = u.setPasswordData(event)
case user.UserRemovedType:
u.State = int32(model.UserStateDeleted)
case user.UserV1PasswordChangedType,
user.HumanPasswordChangedType:
err = u.setPasswordData(event)
case user.HumanPasswordlessTokenAddedType:
err = u.addPasswordlessToken(event)
case user.HumanPasswordlessTokenVerifiedType:
err = u.updatePasswordlessToken(event)
case user.HumanPasswordlessTokenRemovedType:
err = u.removePasswordlessToken(event)
case user.UserV1ProfileChangedType,
user.HumanProfileChangedType,
user.UserV1AddressChangedType,
user.HumanAddressChangedType,
user.MachineChangedEventType:
err = u.setData(event)
case user.UserDomainClaimedType:
if u.HumanView != nil {
u.HumanView.UsernameChangeRequired = true
}
err = u.setData(event)
case user.UserUserNameChangedType:
if u.HumanView != nil {
u.HumanView.UsernameChangeRequired = false
}
err = u.setData(event)
case user.UserV1EmailChangedType,
user.HumanEmailChangedType:
u.IsEmailVerified = false
err = u.setData(event)
case user.UserV1EmailVerifiedType,
user.HumanEmailVerifiedType:
u.IsEmailVerified = true
case user.UserV1PhoneChangedType,
user.HumanPhoneChangedType:
u.IsPhoneVerified = false
err = u.setData(event)
case user.UserV1PhoneVerifiedType,
user.HumanPhoneVerifiedType:
u.IsPhoneVerified = true
case user.UserV1PhoneRemovedType,
user.HumanPhoneRemovedType:
u.Phone = ""
u.IsPhoneVerified = false
u.OTPSMSAdded = false
u.MFAInitSkipped = time.Time{}
case user.UserDeactivatedType:
u.State = int32(model.UserStateInactive)
case user.UserReactivatedType,
user.UserUnlockedType:
u.State = int32(model.UserStateActive)
case user.UserLockedType:
u.State = int32(model.UserStateLocked)
case user.UserV1MFAOTPAddedType,
user.HumanMFAOTPAddedType:
if u.HumanView == nil {
logging.WithFields("event_sequence", event.Sequence, "aggregate_id", event.Aggregate().ID, "instance", event.Aggregate().InstanceID).Warn("event is ignored because human not exists")
return zerrors.ThrowInvalidArgument(nil, "MODEL-p2BXx", "event ignored: human not exists")
}
u.OTPState = int32(model.MFAStateNotReady)
case user.UserV1MFAOTPVerifiedType,
user.HumanMFAOTPVerifiedType:
if u.HumanView == nil {
logging.WithFields("event_sequence", event.Sequence, "aggregate_id", event.Aggregate().ID, "instance", event.Aggregate().InstanceID).Warn("event is ignored because human not exists")
return zerrors.ThrowInvalidArgument(nil, "MODEL-o6Lcq", "event ignored: human not exists")
}
u.OTPState = int32(model.MFAStateReady)
u.MFAInitSkipped = time.Time{}
case user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPRemovedType:
u.OTPState = int32(model.MFAStateUnspecified)
case user.HumanOTPSMSAddedType:
u.OTPSMSAdded = true
case user.HumanOTPSMSRemovedType:
u.OTPSMSAdded = false
u.MFAInitSkipped = time.Time{}
case user.HumanOTPEmailAddedType:
u.OTPEmailAdded = true
case user.HumanOTPEmailRemovedType:
u.OTPEmailAdded = false
u.MFAInitSkipped = time.Time{}
case user.HumanU2FTokenAddedType:
err = u.addU2FToken(event)
case user.HumanU2FTokenVerifiedType:
err = u.updateU2FToken(event)
if err != nil {
return err
}
u.MFAInitSkipped = time.Time{}
case user.HumanU2FTokenRemovedType:
err = u.removeU2FToken(event)
case user.UserV1MFAInitSkippedType,
user.HumanMFAInitSkippedType:
u.MFAInitSkipped = event.CreatedAt()
case user.UserV1InitialCodeAddedType,
user.HumanInitialCodeAddedType:
u.InitRequired = true
case user.UserV1InitializedCheckSucceededType,
user.HumanInitializedCheckSucceededType:
u.InitRequired = false
case user.HumanAvatarAddedType:
err = u.setData(event)
case user.HumanAvatarRemovedType:
u.AvatarKey = ""
case user.HumanPasswordlessInitCodeAddedType,
user.HumanPasswordlessInitCodeRequestedType:
if u.HumanView == nil {
logging.WithFields("event_sequence", event.Sequence, "aggregate_id", event.Aggregate().ID, "instance", event.Aggregate().InstanceID).Warn("event is ignored because human not exists")
return zerrors.ThrowInvalidArgument(nil, "MODEL-MbyC0", "event ignored: human not exists")
}
if !u.PasswordSet {
u.PasswordlessInitRequired = true
u.PasswordInitRequired = false
}
}
u.ComputeObject()
return err
}
func (u *UserView) setRootData(event eventstore.Event) {
u.ID = event.Aggregate().ID
u.ResourceOwner = event.Aggregate().ResourceOwner
u.InstanceID = event.Aggregate().InstanceID
}
func (u *UserView) setData(event eventstore.Event) error {
if err := event.Unmarshal(u); err != nil {
logging.Log("MODEL-lso9e").WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(nil, "MODEL-8iows", "could not unmarshal data")
}
return nil
}
func (u *UserView) setPasswordData(event eventstore.Event) error {
password := new(es_model.Password)
if err := event.Unmarshal(password); err != nil {
logging.WithError(err).Error("could not unmarshal event data")
return zerrors.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
}
u.PasswordSet = password.Secret != nil || password.EncodedHash != ""
u.PasswordInitRequired = !u.PasswordSet
u.PasswordChangeRequired = password.ChangeRequired
u.PasswordChanged = event.CreatedAt()
return nil
}
func (u *UserView) addPasswordlessToken(event eventstore.Event) error {
token, err := webAuthNViewFromEvent(event)
if err != nil {
return err
}
for i, t := range u.PasswordlessTokens {
if t.State == int32(model.MFAStateNotReady) {
u.PasswordlessTokens[i].ID = token.ID
return nil
}
}
token.State = int32(model.MFAStateNotReady)
u.PasswordlessTokens = append(u.PasswordlessTokens, token)
return nil
}
func (u *UserView) updatePasswordlessToken(event eventstore.Event) error {
token, err := webAuthNViewFromEvent(event)
if err != nil {
return err
}
for i, t := range u.PasswordlessTokens {
if t.ID == token.ID {
u.PasswordlessTokens[i].Name = token.Name
u.PasswordlessTokens[i].State = int32(model.MFAStateReady)
return nil
}
}
return nil
}
func (u *UserView) removePasswordlessToken(event eventstore.Event) error {
token, err := webAuthNViewFromEvent(event)
if err != nil {
return err
}
for i, t := range u.PasswordlessTokens {
if t.ID == token.ID {
u.PasswordlessTokens[i] = u.PasswordlessTokens[len(u.PasswordlessTokens)-1]
u.PasswordlessTokens[len(u.PasswordlessTokens)-1] = nil
u.PasswordlessTokens = u.PasswordlessTokens[:len(u.PasswordlessTokens)-1]
return nil
}
}
return nil
}
func (u *UserView) addU2FToken(event eventstore.Event) error {
token, err := webAuthNViewFromEvent(event)
if err != nil {
return err
}
for i, t := range u.U2FTokens {
if t.State == int32(model.MFAStateNotReady) {
u.U2FTokens[i].ID = token.ID
return nil
}
}
token.State = int32(model.MFAStateNotReady)
u.U2FTokens = append(u.U2FTokens, token)
return nil
}
func (u *UserView) updateU2FToken(event eventstore.Event) error {
token, err := webAuthNViewFromEvent(event)
if err != nil {
return err
}
for i, t := range u.U2FTokens {
if t.ID == token.ID {
u.U2FTokens[i].Name = token.Name
u.U2FTokens[i].State = int32(model.MFAStateReady)
return nil
}
}
return nil
}
func (u *UserView) removeU2FToken(event eventstore.Event) error {
token, err := webAuthNViewFromEvent(event)
if err != nil {
return err
}
for i := len(u.U2FTokens) - 1; i >= 0; i-- {
if u.U2FTokens[i].ID == token.ID {
u.U2FTokens[i] = u.U2FTokens[len(u.U2FTokens)-1]
u.U2FTokens[len(u.U2FTokens)-1] = nil
u.U2FTokens = u.U2FTokens[:len(u.U2FTokens)-1]
}
}
return nil
}
func webAuthNViewFromEvent(event eventstore.Event) (*WebAuthNView, error) {
token := new(WebAuthNView)
err := event.Unmarshal(token)
if err != nil {
return nil, zerrors.ThrowInternal(err, "MODEL-FSaq1", "could not unmarshal data")
}
return token, err
}
func (u *UserView) ComputeObject() {
if !u.MachineView.IsZero() {
if u.State == int32(model.UserStateUnspecified) {
u.State = int32(model.UserStateActive)
}
return
}
if u.State == int32(model.UserStateUnspecified) || u.State == int32(model.UserStateInitial) {
if u.IsEmailVerified {
u.State = int32(model.UserStateActive)
} else {
u.State = int32(model.UserStateInitial)
}
}
u.ComputeMFAMaxSetUp()
}
func (u *UserView) ComputeMFAMaxSetUp() {
for _, token := range u.PasswordlessTokens {
if token.State == int32(model.MFAStateReady) {
u.MFAMaxSetUp = int32(domain.MFALevelMultiFactor)
u.PasswordlessInitRequired = false
return
}
}
for _, token := range u.U2FTokens {
if token.State == int32(model.MFAStateReady) {
u.MFAMaxSetUp = int32(domain.MFALevelSecondFactor)
return
}
}
if u.OTPState == int32(model.MFAStateReady) ||
u.OTPSMSAdded || u.OTPEmailAdded {
u.MFAMaxSetUp = int32(domain.MFALevelSecondFactor)
return
}
u.MFAMaxSetUp = int32(domain.MFALevelNotSetUp)
}
func (u *UserView) SetEmptyUserType() {
if u.MachineView != nil && u.MachineView.Name == "" {
u.MachineView = nil
} else {
u.HumanView = nil
}
}
func (u *UserView) EventTypes() []eventstore.EventType {
return []eventstore.EventType{
user.MachineAddedEventType,
user.UserV1AddedType,
user.UserV1RegisteredType,
user.HumanRegisteredType,
user.HumanAddedType,
user.UserRemovedType,
user.UserV1PasswordChangedType,
user.HumanPasswordChangedType,
user.HumanPasswordlessTokenAddedType,
user.HumanPasswordlessTokenVerifiedType,
user.HumanPasswordlessTokenRemovedType,
user.UserV1ProfileChangedType,
user.HumanProfileChangedType,
user.UserV1AddressChangedType,
user.HumanAddressChangedType,
user.MachineChangedEventType,
user.UserDomainClaimedType,
user.UserUserNameChangedType,
user.UserV1EmailChangedType,
user.HumanEmailChangedType,
user.UserV1EmailVerifiedType,
user.HumanEmailVerifiedType,
user.UserV1PhoneChangedType,
user.HumanPhoneChangedType,
user.UserV1PhoneVerifiedType,
user.HumanPhoneVerifiedType,
user.UserV1PhoneRemovedType,
user.HumanPhoneRemovedType,
user.UserDeactivatedType,
user.UserReactivatedType,
user.UserUnlockedType,
user.UserLockedType,
user.UserV1MFAOTPAddedType,
user.HumanMFAOTPAddedType,
user.UserV1MFAOTPVerifiedType,
user.HumanMFAOTPVerifiedType,
user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPRemovedType,
user.HumanOTPSMSAddedType,
user.HumanOTPSMSRemovedType,
user.HumanOTPEmailAddedType,
user.HumanOTPEmailRemovedType,
user.HumanU2FTokenAddedType,
user.HumanU2FTokenVerifiedType,
user.HumanU2FTokenRemovedType,
user.UserV1MFAInitSkippedType,
user.HumanMFAInitSkippedType,
user.UserV1InitialCodeAddedType,
user.HumanInitialCodeAddedType,
user.UserV1InitializedCheckSucceededType,
user.HumanInitializedCheckSucceededType,
user.HumanAvatarAddedType,
user.HumanAvatarRemovedType,
user.HumanPasswordlessInitCodeAddedType,
user.HumanPasswordlessInitCodeRequestedType,
}
}

View File

@@ -0,0 +1,85 @@
package model
import (
"github.com/zitadel/zitadel/internal/domain"
usr_model "github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/view/repository"
)
type UserSearchRequest usr_model.UserSearchRequest
type UserSearchQuery usr_model.UserSearchQuery
type UserSearchKey usr_model.UserSearchKey
func (req UserSearchRequest) GetLimit() uint64 {
return req.Limit
}
func (req UserSearchRequest) GetOffset() uint64 {
return req.Offset
}
func (req UserSearchRequest) GetSortingColumn() repository.ColumnKey {
if req.SortingColumn == usr_model.UserSearchKeyUnspecified {
return nil
}
return UserSearchKey(req.SortingColumn)
}
func (req UserSearchRequest) GetAsc() bool {
return req.Asc
}
func (req UserSearchRequest) GetQueries() []repository.SearchQuery {
result := make([]repository.SearchQuery, len(req.Queries))
for i, q := range req.Queries {
result[i] = UserSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
}
return result
}
func (req UserSearchQuery) GetKey() repository.ColumnKey {
return UserSearchKey(req.Key)
}
func (req UserSearchQuery) GetMethod() domain.SearchMethod {
return req.Method
}
func (req UserSearchQuery) GetValue() interface{} {
return req.Value
}
func (key UserSearchKey) ToColumnName() string {
switch usr_model.UserSearchKey(key) {
case usr_model.UserSearchKeyUserID:
return UserKeyUserID
case usr_model.UserSearchKeyUserName:
return UserKeyUserName
case usr_model.UserSearchKeyFirstName:
return UserKeyFirstName
case usr_model.UserSearchKeyLastName:
return UserKeyLastName
case usr_model.UserSearchKeyDisplayName:
return UserKeyDisplayName
case usr_model.UserSearchKeyNickName:
return UserKeyNickName
case usr_model.UserSearchKeyEmail:
return UserKeyEmail
case usr_model.UserSearchKeyState:
return UserKeyState
case usr_model.UserSearchKeyResourceOwner:
return UserKeyResourceOwner
case usr_model.UserSearchKeyLoginNames:
return UserKeyLoginNames
case usr_model.UserSearchKeyPreferredLoginName:
return UserKeyPreferredLoginName
case usr_model.UserSearchKeyType:
return UserKeyType
case usr_model.UserSearchKeyInstanceID:
return UserKeyInstanceID
case usr_model.UserSearchOwnerRemoved:
return UserKeyOwnerRemoved
default:
return ""
}
}

View File

@@ -0,0 +1,267 @@
package model
import (
"database/sql"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/user/model"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
UserSessionKeyUserAgentID = "user_agent_id"
UserSessionKeyUserID = "user_id"
UserSessionKeyState = "state"
UserSessionKeyResourceOwner = "resource_owner"
UserSessionKeyInstanceID = "instance_id"
UserSessionKeyOwnerRemoved = "owner_removed"
UserSessionKeyCreationDate = "creation_date"
UserSessionKeyChangeDate = "change_date"
UserSessionKeySequence = "sequence"
UserSessionKeyPasswordVerification = "password_verification"
UserSessionKeySecondFactorVerification = "second_factor_verification"
UserSessionKeySecondFactorVerificationType = "second_factor_verification_type"
UserSessionKeyMultiFactorVerification = "multi_factor_verification"
UserSessionKeyMultiFactorVerificationType = "multi_factor_verification_type"
UserSessionKeyPasswordlessVerification = "passwordless_verification"
UserSessionKeyExternalLoginVerification = "external_login_verification"
UserSessionKeySelectedIDPConfigID = "selected_idp_config_id"
UserSessionKeyID = "id"
)
type UserSessionView struct {
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
State sql.Null[domain.UserSessionState] `json:"-" gorm:"column:state"`
UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id;primary_key"`
UserID string `json:"userID" gorm:"column:user_id;primary_key"`
// As of https://github.com/zitadel/zitadel/pull/7199 the following 4 attributes
// are not projected in the user session handler anymore
// and are therefore annotated with a `gorm:"-"`.
// They will be read from the corresponding projection directly.
UserName sql.NullString `json:"-" gorm:"-"`
LoginName sql.NullString `json:"-" gorm:"-"`
DisplayName sql.NullString `json:"-" gorm:"-"`
AvatarKey sql.NullString `json:"-" gorm:"-"`
SelectedIDPConfigID sql.NullString `json:"selectedIDPConfigID" gorm:"column:selected_idp_config_id"`
PasswordVerification sql.NullTime `json:"-" gorm:"column:password_verification"`
PasswordlessVerification sql.NullTime `json:"-" gorm:"column:passwordless_verification"`
ExternalLoginVerification sql.NullTime `json:"-" gorm:"column:external_login_verification"`
SecondFactorVerification sql.NullTime `json:"-" gorm:"column:second_factor_verification"`
SecondFactorVerificationType sql.NullInt32 `json:"-" gorm:"column:second_factor_verification_type"`
MultiFactorVerification sql.NullTime `json:"-" gorm:"column:multi_factor_verification"`
MultiFactorVerificationType sql.NullInt32 `json:"-" gorm:"column:multi_factor_verification_type"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
ID sql.NullString `json:"id" gorm:"-"`
}
type ActiveUserAgentUserIDs struct {
UserAgentID string
UserIDs []string
}
type userAgentIDPayload struct {
ID string `json:"userAgentID"`
}
func UserAgentIDFromEvent(event eventstore.Event) (string, error) {
payload := new(userAgentIDPayload)
if err := event.Unmarshal(payload); err != nil {
logging.WithError(err).Error("could not unmarshal event data")
return "", zerrors.ThrowInternal(nil, "MODEL-HJwk9", "could not unmarshal data")
}
return payload.ID, nil
}
func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
return &model.UserSessionView{
ChangeDate: userSession.ChangeDate,
CreationDate: userSession.CreationDate,
ResourceOwner: userSession.ResourceOwner,
State: userSession.State.V,
UserAgentID: userSession.UserAgentID,
UserID: userSession.UserID,
UserName: userSession.UserName.String,
LoginName: userSession.LoginName.String,
DisplayName: userSession.DisplayName.String,
AvatarKey: userSession.AvatarKey.String,
SelectedIDPConfigID: userSession.SelectedIDPConfigID.String,
PasswordVerification: userSession.PasswordVerification.Time,
PasswordlessVerification: userSession.PasswordlessVerification.Time,
ExternalLoginVerification: userSession.ExternalLoginVerification.Time,
SecondFactorVerification: userSession.SecondFactorVerification.Time,
SecondFactorVerificationType: domain.MFAType(userSession.SecondFactorVerificationType.Int32),
MultiFactorVerification: userSession.MultiFactorVerification.Time,
MultiFactorVerificationType: domain.MFAType(userSession.MultiFactorVerificationType.Int32),
Sequence: userSession.Sequence,
ID: userSession.ID.String,
}
}
func UserSessionsToModel(userSessions []*UserSessionView) []*model.UserSessionView {
result := make([]*model.UserSessionView, len(userSessions))
for i, s := range userSessions {
result[i] = UserSessionToModel(s)
}
return result
}
func (v *UserSessionView) AppendEvent(event eventstore.Event) error {
// in case anything needs to be change here check if the Reduce function needs the change as well
v.Sequence = event.Sequence()
v.ChangeDate = event.CreatedAt()
switch event.Type() {
case user.UserV1PasswordCheckSucceededType,
user.HumanPasswordCheckSucceededType:
v.PasswordVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
v.State.V = domain.UserSessionStateActive
case user.UserIDPLoginCheckSucceededType:
data := new(es_model.AuthRequest)
err := data.SetData(event)
if err != nil {
return err
}
v.ExternalLoginVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
v.SelectedIDPConfigID = sql.NullString{String: data.SelectedIDPConfigID, Valid: true}
v.State.V = domain.UserSessionStateActive
case user.HumanPasswordlessTokenCheckSucceededType:
v.PasswordlessVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
v.MultiFactorVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
v.MultiFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFATypeU2FUserVerification)}
v.State.V = domain.UserSessionStateActive
case user.HumanPasswordlessTokenCheckFailedType,
user.HumanPasswordlessTokenRemovedType:
v.PasswordlessVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.MultiFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
case user.UserV1PasswordCheckFailedType,
user.HumanPasswordCheckFailedType:
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
case user.UserV1PasswordChangedType,
user.HumanPasswordChangedType:
data := new(es_model.PasswordChange)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID != data.UserAgentID {
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
}
case user.HumanMFAOTPVerifiedType:
data := new(es_model.OTPVerified)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID == data.UserAgentID {
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeTOTP)
}
case user.UserV1MFAOTPCheckSucceededType,
user.HumanMFAOTPCheckSucceededType:
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeTOTP)
case user.HumanOTPSMSCheckSucceededType:
data := new(es_model.OTPVerified)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID == data.UserAgentID {
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeOTPSMS)
}
case user.HumanOTPEmailCheckSucceededType:
data := new(es_model.OTPVerified)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID == data.UserAgentID {
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeOTPEmail)
}
case user.UserV1MFAOTPCheckFailedType,
user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPCheckFailedType,
user.HumanMFAOTPRemovedType,
user.HumanU2FTokenCheckFailedType,
user.HumanU2FTokenRemovedType,
user.HumanOTPSMSCheckFailedType,
user.HumanOTPEmailCheckFailedType:
v.SecondFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
case user.HumanU2FTokenVerifiedType:
data := new(es_model.WebAuthNVerify)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID == data.UserAgentID {
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeU2F)
}
case user.HumanU2FTokenCheckSucceededType:
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeU2F)
case user.UserV1SignedOutType,
user.HumanSignedOutType,
user.UserLockedType,
user.UserDeactivatedType,
user.UserRemovedType:
v.PasswordlessVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.SecondFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.SecondFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFALevelNotSetUp)}
v.MultiFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.MultiFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFALevelNotSetUp)}
v.ExternalLoginVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.State.V = domain.UserSessionStateTerminated
case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType:
v.ExternalLoginVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.SelectedIDPConfigID = sql.NullString{String: "", Valid: true}
}
return nil
}
func (v *UserSessionView) setSecondFactorVerification(verificationTime time.Time, mfaType domain.MFAType) {
v.SecondFactorVerification = sql.NullTime{Time: verificationTime, Valid: true}
v.SecondFactorVerificationType = sql.NullInt32{Int32: int32(mfaType)}
v.State.V = domain.UserSessionStateActive
}
func (v *UserSessionView) EventTypes() []eventstore.EventType {
return []eventstore.EventType{
user.UserV1PasswordCheckSucceededType,
user.HumanPasswordCheckSucceededType,
user.UserIDPLoginCheckSucceededType,
user.HumanPasswordlessTokenCheckSucceededType,
user.HumanPasswordlessTokenCheckFailedType,
user.HumanPasswordlessTokenRemovedType,
user.UserV1PasswordCheckFailedType,
user.HumanPasswordCheckFailedType,
user.UserV1PasswordChangedType,
user.HumanPasswordChangedType,
user.HumanMFAOTPVerifiedType,
user.UserV1MFAOTPCheckSucceededType,
user.HumanMFAOTPCheckSucceededType,
user.UserV1MFAOTPCheckFailedType,
user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPCheckFailedType,
user.HumanMFAOTPRemovedType,
user.HumanOTPSMSCheckSucceededType,
user.HumanOTPSMSCheckFailedType,
user.HumanOTPEmailCheckSucceededType,
user.HumanOTPEmailCheckFailedType,
user.HumanU2FTokenCheckFailedType,
user.HumanU2FTokenRemovedType,
user.HumanU2FTokenVerifiedType,
user.HumanU2FTokenCheckSucceededType,
user.UserV1SignedOutType,
user.HumanSignedOutType,
user.UserLockedType,
user.UserDeactivatedType,
user.UserIDPLinkRemovedType,
user.UserIDPLinkCascadeRemovedType,
}
}

View File

@@ -0,0 +1,69 @@
package model
import (
"github.com/zitadel/zitadel/internal/domain"
usr_model "github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/view/repository"
)
type UserSessionSearchRequest usr_model.UserSessionSearchRequest
type UserSessionSearchQuery usr_model.UserSessionSearchQuery
type UserSessionSearchKey usr_model.UserSessionSearchKey
func (req UserSessionSearchRequest) GetLimit() uint64 {
return req.Limit
}
func (req UserSessionSearchRequest) GetOffset() uint64 {
return req.Offset
}
func (req UserSessionSearchRequest) GetSortingColumn() repository.ColumnKey {
if req.SortingColumn == usr_model.UserSessionSearchKeyUnspecified {
return nil
}
return UserSessionSearchKey(req.SortingColumn)
}
func (req UserSessionSearchRequest) GetAsc() bool {
return req.Asc
}
func (req UserSessionSearchRequest) GetQueries() []repository.SearchQuery {
result := make([]repository.SearchQuery, len(req.Queries))
for i, q := range req.Queries {
result[i] = UserSessionSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
}
return result
}
func (req UserSessionSearchQuery) GetKey() repository.ColumnKey {
return UserSessionSearchKey(req.Key)
}
func (req UserSessionSearchQuery) GetMethod() domain.SearchMethod {
return req.Method
}
func (req UserSessionSearchQuery) GetValue() interface{} {
return req.Value
}
func (key UserSessionSearchKey) ToColumnName() string {
switch usr_model.UserSessionSearchKey(key) {
case usr_model.UserSessionSearchKeyUserAgentID:
return UserSessionKeyUserAgentID
case usr_model.UserSessionSearchKeyUserID:
return UserSessionKeyUserID
case usr_model.UserSessionSearchKeyState:
return UserSessionKeyState
case usr_model.UserSessionSearchKeyResourceOwner:
return UserSessionKeyResourceOwner
case usr_model.UserSessionSearchKeyInstanceID:
return UserSessionKeyInstanceID
case usr_model.UserSessionSearchKeyOwnerRemoved:
return UserSessionKeyOwnerRemoved
default:
return ""
}
}

View File

@@ -0,0 +1,242 @@
package model
import (
"database/sql"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
)
func now() time.Time {
return time.Now().UTC().Round(1 * time.Second)
}
func TestAppendEvent(t *testing.T) {
type args struct {
event *es_models.Event
userView *UserSessionView
}
tests := []struct {
name string
args args
result *UserSessionView
}{
{
name: "append user password check succeeded event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1PasswordCheckSucceededType},
userView: &UserSessionView{},
},
result: &UserSessionView{ChangeDate: now(), PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
},
{
name: "append human password check succeeded event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.HumanPasswordCheckSucceededType},
userView: &UserSessionView{},
},
result: &UserSessionView{ChangeDate: now(), PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
},
{
name: "append user password check failed event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1PasswordCheckFailedType},
userView: &UserSessionView{PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{ChangeDate: now(), PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
},
{
name: "append human password check failed event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.HumanPasswordCheckFailedType},
userView: &UserSessionView{PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{ChangeDate: now(), PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
},
{
name: "append user password changed event",
args: args{
event: &es_models.Event{
CreationDate: now(),
Typ: user.UserV1PasswordChangedType,
Data: func() []byte {
d, _ := json.Marshal(&es_model.Password{
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
})
return d
}(),
},
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
},
{
name: "append human password changed event",
args: args{
event: &es_models.Event{
CreationDate: now(),
Typ: user.HumanPasswordChangedType,
Data: func() []byte {
d, _ := json.Marshal(&es_model.PasswordChange{
Password: es_model.Password{
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
},
})
return d
}(),
},
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
},
{
name: "append human password changed event same user agent",
args: args{
event: &es_models.Event{
CreationDate: now(),
Typ: user.HumanPasswordChangedType,
Data: func() []byte {
d, _ := json.Marshal(&es_model.PasswordChange{
Password: es_model.Password{
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
},
UserAgentID: "id",
})
return d
}(),
},
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
},
{
name: "append user otp verified event",
args: args{
event: &es_models.Event{
CreationDate: now(),
Typ: user.HumanMFAOTPVerifiedType,
Data: nil,
},
userView: &UserSessionView{UserAgentID: "id"},
},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now()},
},
{
name: "append user otp verified event same user agent",
args: args{
event: &es_models.Event{
CreationDate: now(),
Typ: user.HumanMFAOTPVerifiedType,
Data: func() []byte {
d, _ := json.Marshal(&es_model.OTPVerified{
UserAgentID: "id",
})
return d
}(),
},
userView: &UserSessionView{UserAgentID: "id"},
},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
},
{
name: "append user otp check succeeded event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1MFAOTPCheckSucceededType},
userView: &UserSessionView{},
},
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
},
{
name: "append human otp check succeeded event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.HumanMFAOTPCheckSucceededType},
userView: &UserSessionView{},
},
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
},
{
name: "append user otp check failed event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1MFAOTPCheckFailedType},
userView: &UserSessionView{SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
},
{
name: "append human otp check failed event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.HumanMFAOTPCheckFailedType},
userView: &UserSessionView{SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
},
{
name: "append user otp removed event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1MFAOTPRemovedType},
userView: &UserSessionView{SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
},
{
name: "append human otp removed event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.HumanMFAOTPRemovedType},
userView: &UserSessionView{SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
},
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
},
{
name: "append user signed out event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1SignedOutType},
userView: &UserSessionView{
PasswordVerification: sql.NullTime{Time: now(), Valid: true},
SecondFactorVerification: sql.NullTime{Time: now(), Valid: true},
},
},
result: &UserSessionView{
ChangeDate: now(),
PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true},
SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
ExternalLoginVerification: sql.NullTime{Time: time.Time{}, Valid: true},
PasswordlessVerification: sql.NullTime{Time: time.Time{}, Valid: true},
MultiFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
State: sql.Null[domain.UserSessionState]{V: domain.UserSessionStateTerminated},
},
},
{
name: "append human signed out event",
args: args{
event: &es_models.Event{CreationDate: now(), Typ: user.HumanSignedOutType},
userView: &UserSessionView{
PasswordVerification: sql.NullTime{Time: now(), Valid: true},
SecondFactorVerification: sql.NullTime{Time: now(), Valid: true},
},
},
result: &UserSessionView{
ChangeDate: now(),
PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true},
SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
ExternalLoginVerification: sql.NullTime{Time: time.Time{}, Valid: true},
PasswordlessVerification: sql.NullTime{Time: time.Time{}, Valid: true},
MultiFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
State: sql.Null[domain.UserSessionState]{V: domain.UserSessionStateTerminated},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.userView.AppendEvent(tt.args.event)
assert.Equal(t, tt.result, tt.args.userView)
})
}
}

View File

@@ -0,0 +1,418 @@
package model
import (
"encoding/json"
"testing"
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/user/model"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
)
func mockUserData(user *es_model.User) []byte {
data, _ := json.Marshal(user)
return data
}
func mockPasswordData(password *es_model.Password) []byte {
data, _ := json.Marshal(password)
return data
}
func mockProfileData(profile *es_model.Profile) []byte {
data, _ := json.Marshal(profile)
return data
}
func mockEmailData(email *es_model.Email) []byte {
data, _ := json.Marshal(email)
return data
}
func mockPhoneData(phone *es_model.Phone) []byte {
data, _ := json.Marshal(phone)
return data
}
func mockAddressData(address *es_model.Address) []byte {
data, _ := json.Marshal(address)
return data
}
func getFullHuman(password *es_model.Password) *es_model.User {
return &es_model.User{
UserName: "UserName",
Human: &es_model.Human{
Profile: &es_model.Profile{
FirstName: "FirstName",
LastName: "LastName",
},
Email: &es_model.Email{
EmailAddress: "Email",
},
Phone: &es_model.Phone{
PhoneNumber: "Phone",
},
Address: &es_model.Address{
Country: "Country",
},
Password: password,
},
}
}
func getFullMachine() *es_model.User {
return &es_model.User{
UserName: "UserName",
Machine: &es_model.Machine{
Description: "Description",
Name: "Machine",
},
}
}
func TestUserAppendEvent(t *testing.T) {
type args struct {
event eventstore.Event
user *UserView
}
tests := []struct {
name string
args args
result *UserView
}{
{
name: "append added user event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1AddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(nil))},
user: &UserView{},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
},
{
name: "append added human event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanAddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(nil))},
user: &UserView{},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
},
{
name: "append added machine event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.MachineAddedEventType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullMachine())},
user: &UserView{},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", MachineView: &MachineView{Description: "Description", Name: "Machine"}, State: int32(model.UserStateActive)},
},
{
name: "append added user with password event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1AddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(&es_model.Password{Secret: &crypto.CryptoValue{}}))},
user: &UserView{},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", PasswordSet: true}, State: int32(model.UserStateInitial)},
},
{
name: "append added human with password event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanAddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(&es_model.Password{Secret: &crypto.CryptoValue{}}))},
user: &UserView{},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", PasswordSet: true}, State: int32(model.UserStateInitial)},
},
{
name: "append added user with password but change required event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1AddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}}))},
user: &UserView{},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", PasswordSet: true, PasswordChangeRequired: true}, State: int32(model.UserStateInitial)},
},
{
name: "append added human with password but change required event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanAddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}}))},
user: &UserView{},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", PasswordSet: true, PasswordChangeRequired: true}, State: int32(model.UserStateInitial)},
},
{
name: "append password change event on user",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1PasswordChangedType, ResourceOwner: "GrantedOrgID", Data: mockPasswordData(&es_model.Password{Secret: &crypto.CryptoValue{}})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", PasswordSet: true}, State: int32(model.UserStateActive)},
},
{
name: "append password change event on human",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanPasswordChangedType, ResourceOwner: "GrantedOrgID", Data: mockPasswordData(&es_model.Password{Secret: &crypto.CryptoValue{}})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", PasswordSet: true}, State: int32(model.UserStateActive)},
},
{
name: "append password change with change required event on user",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1PasswordChangedType, ResourceOwner: "GrantedOrgID", Data: mockPasswordData(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", PasswordSet: true, PasswordChangeRequired: true}, State: int32(model.UserStateActive)},
},
{
name: "append password change with change required event on human",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanPasswordChangedType, ResourceOwner: "GrantedOrgID", Data: mockPasswordData(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", PasswordSet: true, PasswordChangeRequired: true}, State: int32(model.UserStateActive)},
},
{
name: "append change user profile event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1ProfileChangedType, ResourceOwner: "GrantedOrgID", Data: mockProfileData(&es_model.Profile{FirstName: "FirstNameChanged"})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstNameChanged", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
},
{
name: "append change human profile event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanProfileChangedType, ResourceOwner: "GrantedOrgID", Data: mockProfileData(&es_model.Profile{FirstName: "FirstNameChanged"})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstNameChanged", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
},
{
name: "append change user email event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1EmailChangedType, ResourceOwner: "GrantedOrgID", Data: mockEmailData(&es_model.Email{EmailAddress: "EmailChanged"})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "EmailChanged", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append change human email event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanEmailChangedType, ResourceOwner: "GrantedOrgID", Data: mockEmailData(&es_model.Email{EmailAddress: "EmailChanged"})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "EmailChanged", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append verify user email event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1EmailVerifiedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append verify human email event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanEmailVerifiedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append change user phone event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1PhoneChangedType, ResourceOwner: "GrantedOrgID", Data: mockPhoneData(&es_model.Phone{PhoneNumber: "PhoneChanged"})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "PhoneChanged", Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append change human phone event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanPhoneChangedType, ResourceOwner: "GrantedOrgID", Data: mockPhoneData(&es_model.Phone{PhoneNumber: "PhoneChanged"})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "PhoneChanged", Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append verify user phone event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1PhoneVerifiedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", IsPhoneVerified: true, Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append verify human phone event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanPhoneVerifiedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", IsPhoneVerified: true, Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append change user address event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1AddressChangedType, ResourceOwner: "GrantedOrgID", Data: mockAddressData(&es_model.Address{Country: "CountryChanged"})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "CountryChanged"}, State: int32(model.UserStateActive)},
},
{
name: "append change human address event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanAddressChangedType, ResourceOwner: "GrantedOrgID", Data: mockAddressData(&es_model.Address{Country: "CountryChanged"})},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "CountryChanged"}, State: int32(model.UserStateActive)},
},
{
name: "append user deactivate event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserDeactivatedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInactive)},
},
{
name: "append user reactivate event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserReactivatedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInactive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append user lock event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserLockedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateLocked)},
},
{
name: "append user unlock event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserUnlockedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateLocked)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
{
name: "append user add otp event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1MFAOTPAddedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)},
},
{
name: "append human add otp event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanMFAOTPAddedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)},
},
{
name: "append user verify otp event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1MFAOTPVerifiedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)},
},
{
name: "append human verify otp event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanMFAOTPVerifiedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)},
},
{
name: "append user remove otp event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1MFAOTPRemovedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateUnspecified)}, State: int32(model.UserStateActive)},
},
{
name: "append human remove otp event",
args: args{
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanMFAOTPRemovedType, ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateUnspecified)}, State: int32(model.UserStateActive)},
},
{
name: "append user mfa init skipped event",
args: args{
event: &es_models.Event{Seq: 1, CreationDate: time.Now().UTC(), Typ: user.UserV1MFAInitSkippedType, AggregateID: "AggregateID", ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", MFAInitSkipped: time.Now().UTC()}, State: int32(model.UserStateActive)},
},
{
name: "append human mfa init skipped event",
args: args{
event: &es_models.Event{Seq: 1, CreationDate: time.Now().UTC(), Typ: user.HumanMFAInitSkippedType, AggregateID: "AggregateID", ResourceOwner: "GrantedOrgID"},
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
},
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", MFAInitSkipped: time.Now().UTC()}, State: int32(model.UserStateActive)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.AppendEvent(tt.args.event)
if tt.args.user.ID != tt.result.ID {
t.Errorf("got wrong result ID: expected: %v, actual: %v ", tt.result.ID, tt.args.user.ID)
}
if tt.args.user.ResourceOwner != tt.result.ResourceOwner {
t.Errorf("got wrong result ResourceOwner: expected: %v, actual: %v ", tt.result.ResourceOwner, tt.args.user.ResourceOwner)
}
if tt.args.user.State != tt.result.State {
t.Errorf("got wrong result state: expected: %v, actual: %v ", tt.result.State, tt.args.user.State)
}
if human := tt.args.user.HumanView; human != nil {
if human.FirstName != tt.result.FirstName {
t.Errorf("got wrong result FirstName: expected: %v, actual: %v ", tt.result.FirstName, tt.args.user.FirstName)
}
if human.LastName != tt.result.LastName {
t.Errorf("got wrong result FirstName: expected: %v, actual: %v ", tt.result.FirstName, human.FirstName)
}
if human.Email != tt.result.Email {
t.Errorf("got wrong result email: expected: %v, actual: %v ", tt.result.Email, human.Email)
}
if human.IsEmailVerified != tt.result.IsEmailVerified {
t.Errorf("got wrong result IsEmailVerified: expected: %v, actual: %v ", tt.result.IsEmailVerified, human.IsEmailVerified)
}
if human.Phone != tt.result.Phone {
t.Errorf("got wrong result Phone: expected: %v, actual: %v ", tt.result.Phone, human.Phone)
}
if human.IsPhoneVerified != tt.result.IsPhoneVerified {
t.Errorf("got wrong result IsPhoneVerified: expected: %v, actual: %v ", tt.result.IsPhoneVerified, human.IsPhoneVerified)
}
if human.Country != tt.result.Country {
t.Errorf("got wrong result Country: expected: %v, actual: %v ", tt.result.Country, human.Country)
}
if human.OTPState != tt.result.OTPState {
t.Errorf("got wrong result OTPState: expected: %v, actual: %v ", tt.result.OTPState, human.OTPState)
}
if human.MFAInitSkipped.Round(1*time.Second) != tt.result.MFAInitSkipped.Round(1*time.Second) {
t.Errorf("got wrong result MFAInitSkipped: expected: %v, actual: %v ", tt.result.MFAInitSkipped.Round(1*time.Second), human.MFAInitSkipped.Round(1*time.Second))
}
if human.PasswordSet != tt.result.PasswordSet {
t.Errorf("got wrong result PasswordSet: expected: %v, actual: %v ", tt.result.PasswordSet, human.PasswordSet)
}
if human.PasswordChangeRequired != tt.result.PasswordChangeRequired {
t.Errorf("got wrong result PasswordChangeRequired: expected: %v, actual: %v ", tt.result.PasswordChangeRequired, human.PasswordChangeRequired)
}
}
})
}
}

View File

@@ -0,0 +1,24 @@
package view
import (
"time"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func UserByIDQuery(id, instanceID string, changeDate time.Time, eventTypes []eventstore.EventType) (*eventstore.SearchQueryBuilder, error) {
if id == "" {
return nil, zerrors.ThrowPreconditionFailed(nil, "EVENT-d8isw", "Errors.User.UserIDMissing")
}
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AwaitOpenTransactions().
InstanceID(instanceID).
CreationDateAfter(changeDate.Add(-1 * time.Microsecond)). // to simulate CreationDate >=
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(id).
EventTypes(eventTypes...).
Builder(), nil
}

View File

@@ -0,0 +1,50 @@
package view
import (
"github.com/jinzhu/gorm"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/user/model"
usr_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/view/repository"
"github.com/zitadel/zitadel/internal/zerrors"
)
func RefreshTokenByID(db *gorm.DB, table, tokenID, instanceID string) (*usr_model.RefreshTokenView, error) {
token := new(usr_model.RefreshTokenView)
query := repository.PrepareGetByQuery(table,
&usr_model.RefreshTokenSearchQuery{Key: model.RefreshTokenSearchKeyRefreshTokenID, Method: domain.SearchMethodEquals, Value: tokenID},
&usr_model.RefreshTokenSearchQuery{Key: model.RefreshTokenSearchKeyInstanceID, Method: domain.SearchMethodEquals, Value: instanceID},
)
err := query(db, token)
if zerrors.IsNotFound(err) {
return nil, zerrors.ThrowNotFound(nil, "VIEW-6ub3p", "Errors.RefreshToken.NotFound")
}
return token, err
}
func RefreshTokensByUserID(db *gorm.DB, table, userID, instanceID string) ([]*usr_model.RefreshTokenView, error) {
tokens := make([]*usr_model.RefreshTokenView, 0)
userIDQuery := &model.RefreshTokenSearchQuery{
Key: model.RefreshTokenSearchKeyUserID,
Method: domain.SearchMethodEquals,
Value: userID,
}
instanceIDQuery := &model.RefreshTokenSearchQuery{
Key: model.RefreshTokenSearchKeyInstanceID,
Method: domain.SearchMethodEquals,
Value: instanceID,
}
query := repository.PrepareSearchQuery(table, usr_model.RefreshTokenSearchRequest{
Queries: []*model.RefreshTokenSearchQuery{userIDQuery, instanceIDQuery},
})
_, err := query(db, &tokens)
return tokens, err
}
func SearchRefreshTokens(db *gorm.DB, table string, req *model.RefreshTokenSearchRequest) ([]*usr_model.RefreshTokenView, uint64, error) {
tokens := make([]*usr_model.RefreshTokenView, 0)
query := repository.PrepareSearchQuery(table, usr_model.RefreshTokenSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
count, err := query(db, &tokens)
return tokens, count, err
}

View File

@@ -0,0 +1,49 @@
package view
import (
"github.com/jinzhu/gorm"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/user/model"
usr_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/view/repository"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TokenByIDs(db *gorm.DB, table, tokenID, userID, instanceID string) (*usr_model.TokenView, error) {
token := new(usr_model.TokenView)
query := repository.PrepareGetByQuery(table,
&usr_model.TokenSearchQuery{Key: model.TokenSearchKeyTokenID, Method: domain.SearchMethodEquals, Value: tokenID},
&usr_model.TokenSearchQuery{Key: model.TokenSearchKeyUserID, Method: domain.SearchMethodEquals, Value: userID},
&usr_model.TokenSearchQuery{Key: model.TokenSearchKeyInstanceID, Method: domain.SearchMethodEquals, Value: instanceID},
)
err := query(db, token)
if zerrors.IsNotFound(err) {
return nil, zerrors.ThrowNotFound(nil, "VIEW-6ub3p", "Errors.Token.NotFound")
}
return token, err
}
func TokensByUserID(db *gorm.DB, table, userID, instanceID string) ([]*usr_model.TokenView, error) {
tokens := make([]*usr_model.TokenView, 0)
userIDQuery := &model.TokenSearchQuery{
Key: model.TokenSearchKeyUserID,
Method: domain.SearchMethodEquals,
Value: userID,
}
instanceIDQuery := &model.TokenSearchQuery{
Key: model.TokenSearchKeyInstanceID,
Method: domain.SearchMethodEquals,
Value: instanceID,
}
expirationQuery := &model.TokenSearchQuery{
Key: model.TokenSearchKeyExpiration,
Method: domain.SearchMethodGreaterThan,
Value: "now()",
}
query := repository.PrepareSearchQuery(table, usr_model.TokenSearchRequest{
Queries: []*model.TokenSearchQuery{userIDQuery, instanceIDQuery, expirationQuery},
})
_, err := query(db, &tokens)
return tokens, err
}

View File

@@ -0,0 +1,7 @@
SELECT
s.user_agent_id
FROM auth.user_sessions s
WHERE
s.id = $1
AND s.instance_id = $2
LIMIT 1;

View File

@@ -0,0 +1,97 @@
WITH auth_methods AS (
SELECT
user_id
, method_type
, token_id
, state
, instance_id
, name
FROM
projections.user_auth_methods5
WHERE
instance_id = $1
AND user_id = $2
),
verified_auth_methods AS (
SELECT
method_type
FROM
auth_methods
WHERE state = 2
)
SELECT
u.id
, u.creation_date
, LEAST(u.change_date, au.change_date) AS change_date
, u.resource_owner
, u.state AS user_state
, au.password_set
, h.password_change_required
, au.password_change
, au.last_login
, u.username AS user_name
, (SELECT array_agg(ll.login_name) login_names FROM projections.login_names3 ll
WHERE u.instance_id = ll.instance_id AND u.id = ll.user_id
GROUP BY ll.user_id, ll.instance_id) AS login_names
, l.login_name as preferred_login_name
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.email
, h.is_email_verified
, n.verified_email
, h.phone
, h.is_phone_verified
, (SELECT COALESCE((SELECT state FROM auth_methods WHERE method_type = 1), 0)) AS otp_state
, CASE
WHEN EXISTS (SELECT true FROM verified_auth_methods WHERE method_type = 3) THEN 2
WHEN EXISTS (SELECT true FROM verified_auth_methods WHERE method_type = 2) THEN 1
ELSE 0
END AS mfa_max_set_up
, au.mfa_init_skipped
, u.sequence
, au.init_required
, au.username_change_required
, m.name AS machine_name
, m.description AS machine_description
, u.type AS user_type
, (SELECT
JSONB_AGG(json_build_object('webAuthNTokenId', token_id, 'webAuthNTokenName', name, 'state', state))
FROM auth_methods
WHERE method_type = 2
) AS u2f_tokens
, (SELECT
JSONB_AGG(json_build_object('webAuthNTokenId', token_id, 'webAuthNTokenName', name, 'state', state))
FROM auth_methods
WHERE method_type = 3
) AS passwordless_tokens
, h.avatar_key
, au.passwordless_init_required
, au.password_init_required
, u.instance_id
, (SELECT EXISTS (SELECT true FROM verified_auth_methods WHERE method_type = 6)) AS otp_sms_added
, (SELECT EXISTS (SELECT true FROM verified_auth_methods WHERE method_type = 7)) AS otp_email_added
FROM projections.users14 u
LEFT JOIN projections.users14_humans h
ON u.instance_id = h.instance_id
AND u.id = h.user_id
LEFT JOIN projections.users14_notifications n
ON u.instance_id = n.instance_id
AND u.id = n.user_id
LEFT JOIN projections.login_names3 l
ON u.instance_id = l.instance_id
AND u.id = l.user_id
AND l.is_primary = true
LEFT JOIN projections.users14_machines m
ON u.instance_id = m.instance_id
AND u.id = m.user_id
LEFT JOIN auth.users3 au
ON u.instance_id = au.instance_id
AND u.id = au.id
WHERE
u.instance_id = $1
AND u.id = $2
LIMIT 1;

View File

@@ -0,0 +1,29 @@
SELECT s.creation_date,
s.change_date,
s.resource_owner,
s.state,
s.user_agent_id,
s.user_id,
u.username,
l.login_name,
h.display_name,
h.avatar_key,
s.selected_idp_config_id,
s.password_verification,
s.passwordless_verification,
s.external_login_verification,
s.second_factor_verification,
s.second_factor_verification_type,
s.multi_factor_verification,
s.multi_factor_verification_type,
s.sequence,
s.instance_id,
s.id
FROM auth.user_sessions s
LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id
LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
WHERE (s.id = $1)
AND (s.instance_id = $2)
LIMIT 1
;

View File

@@ -0,0 +1,30 @@
SELECT s.creation_date,
s.change_date,
s.resource_owner,
s.state,
s.user_agent_id,
s.user_id,
u.username,
l.login_name,
h.display_name,
h.avatar_key,
s.selected_idp_config_id,
s.password_verification,
s.passwordless_verification,
s.external_login_verification,
s.second_factor_verification,
s.second_factor_verification_type,
s.multi_factor_verification,
s.multi_factor_verification_type,
s.sequence,
s.instance_id,
s.id
FROM auth.user_sessions s
LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id
LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
WHERE (s.user_agent_id = $1)
AND (s.user_id = $2)
AND (s.instance_id = $3)
LIMIT 1
;

View File

@@ -0,0 +1,188 @@
package view
import (
"context"
"database/sql"
_ "embed"
"errors"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
//go:embed user_session_by_id.sql
var userSessionByIDsQuery string
//go:embed user_session.sql
var userSessionByIDQuery string
//go:embed user_sessions_by_user_agent.sql
var userSessionsByUserAgentQuery string
//go:embed user_agent_by_user_session_id.sql
var userAgentByUserSessionIDQuery string
//go:embed active_user_sessions_by_session_id.sql
var activeUserSessionsBySessionIDQuery string
func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, instanceID string) (userSession *model.UserSessionView, err error) {
err = db.QueryRowContext(
ctx,
func(row *sql.Row) error {
userSession, err = scanUserSession(row)
return err
},
userSessionByIDsQuery,
agentID,
userID,
instanceID,
)
return userSession, err
}
func UserSessionByID(ctx context.Context, db *database.DB, userSessionID, instanceID string) (userSession *model.UserSessionView, err error) {
err = db.QueryRowContext(
ctx,
func(row *sql.Row) error {
userSession, err = scanUserSession(row)
return err
},
userSessionByIDQuery,
userSessionID,
instanceID,
)
return userSession, err
}
func UserSessionsByAgentID(ctx context.Context, db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) {
err = db.QueryContext(
ctx,
func(rows *sql.Rows) error {
userSessions, err = scanUserSessions(rows)
return err
},
userSessionsByUserAgentQuery,
agentID,
instanceID,
)
return userSessions, err
}
func UserAgentIDBySessionID(ctx context.Context, db *database.DB, sessionID, instanceID string) (userAgentID string, err error) {
err = db.QueryRowContext(
ctx,
func(row *sql.Row) error {
return row.Scan(&userAgentID)
},
userAgentByUserSessionIDQuery,
sessionID,
instanceID,
)
return userAgentID, err
}
// ActiveUserSessionsBySessionID returns all sessions (sessionID:userID map) with an active session on the same user agent (its id is also returned) based on a sessionID
func ActiveUserSessionsBySessionID(ctx context.Context, db *database.DB, sessionID, instanceID string) (userAgentID string, sessions map[string]string, err error) {
err = db.QueryContext(
ctx,
func(rows *sql.Rows) error {
userAgentID, sessions, err = scanActiveUserAgentUserIDs(rows)
return err
},
activeUserSessionsBySessionIDQuery,
sessionID,
instanceID,
)
return userAgentID, sessions, err
}
func scanActiveUserAgentUserIDs(rows *sql.Rows) (userAgentID string, sessions map[string]string, err error) {
sessions = make(map[string]string)
for rows.Next() {
var userID, sessionID string
err := rows.Scan(
&userAgentID,
&userID,
&sessionID,
)
if err != nil {
return "", nil, err
}
sessions[sessionID] = userID
}
if err := rows.Close(); err != nil {
return "", nil, zerrors.ThrowInternal(err, "VIEW-Sbrws", "Errors.Query.CloseRows")
}
return userAgentID, sessions, nil
}
func scanUserSession(row *sql.Row) (*model.UserSessionView, error) {
session := new(model.UserSessionView)
err := row.Scan(
&session.CreationDate,
&session.ChangeDate,
&session.ResourceOwner,
&session.State,
&session.UserAgentID,
&session.UserID,
&session.UserName,
&session.LoginName,
&session.DisplayName,
&session.AvatarKey,
&session.SelectedIDPConfigID,
&session.PasswordVerification,
&session.PasswordlessVerification,
&session.ExternalLoginVerification,
&session.SecondFactorVerification,
&session.SecondFactorVerificationType,
&session.MultiFactorVerification,
&session.MultiFactorVerificationType,
&session.Sequence,
&session.InstanceID,
&session.ID,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(nil, "VIEW-NGBs1", "Errors.UserSession.NotFound")
}
return session, err
}
func scanUserSessions(rows *sql.Rows) ([]*model.UserSessionView, error) {
sessions := make([]*model.UserSessionView, 0)
for rows.Next() {
session := new(model.UserSessionView)
err := rows.Scan(
&session.CreationDate,
&session.ChangeDate,
&session.ResourceOwner,
&session.State,
&session.UserAgentID,
&session.UserID,
&session.UserName,
&session.LoginName,
&session.DisplayName,
&session.AvatarKey,
&session.SelectedIDPConfigID,
&session.PasswordVerification,
&session.PasswordlessVerification,
&session.ExternalLoginVerification,
&session.SecondFactorVerification,
&session.SecondFactorVerificationType,
&session.MultiFactorVerification,
&session.MultiFactorVerificationType,
&session.Sequence,
&session.InstanceID,
&session.ID,
)
if err != nil {
return nil, err
}
sessions = append(sessions, session)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "VIEW-FSF3g", "Errors.Query.CloseRows")
}
return sessions, nil
}

View File

@@ -0,0 +1,28 @@
SELECT s.creation_date,
s.change_date,
s.resource_owner,
s.state,
s.user_agent_id,
s.user_id,
u.username,
l.login_name,
h.display_name,
h.avatar_key,
s.selected_idp_config_id,
s.password_verification,
s.passwordless_verification,
s.external_login_verification,
s.second_factor_verification,
s.second_factor_verification_type,
s.multi_factor_verification,
s.multi_factor_verification_type,
s.sequence,
s.instance_id,
s.id
FROM auth.user_sessions s
LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id
LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
WHERE (s.user_agent_id = $1 and s.user_agent_id <> '')
AND (s.instance_id = $2)
;

View File

@@ -0,0 +1,42 @@
package view
import (
"context"
"database/sql"
_ "embed"
"errors"
"github.com/jinzhu/gorm"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
//go:embed user_by_id.sql
var userByIDQuery string
func UserByID(ctx context.Context, db *gorm.DB, userID, instanceID string) (*model.UserView, error) {
user := new(model.UserView)
query := db.Raw(userByIDQuery, instanceID, userID)
tx := query.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
defer func() {
if err := tx.Commit().Error; err != nil {
logging.OnError(err).Info("commit failed")
}
tx.RollbackUnlessCommitted()
}()
err := tx.Scan(user).Error
if err == nil {
user.SetEmptyUserType()
return user, nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, zerrors.ThrowNotFound(err, "VIEW-hodc6", "Errors.User.NotFound")
}
logging.WithError(err).Warn("unable to get user by id")
return nil, zerrors.ThrowInternal(err, "VIEW-qJBg9", "unable to get user by id")
}