mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-10 08:05:12 +00:00
Merge branch 'main' into grcp-server-reflect
This commit is contained in:
@@ -65,16 +65,16 @@ func (_ *Styling) AggregateTypes() []models.AggregateType {
|
||||
return []models.AggregateType{org.AggregateType, instance.AggregateType}
|
||||
}
|
||||
|
||||
func (m *Styling) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := m.view.GetLatestStylingSequence(instanceID)
|
||||
func (m *Styling) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) {
|
||||
sequence, err := m.view.GetLatestStylingSequence(ctx, instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (m *Styling) EventQuery(instanceIDs []string) (*models.SearchQuery, error) {
|
||||
sequences, err := m.view.GetLatestStylingSequences(instanceIDs)
|
||||
func (m *Styling) EventQuery(ctx context.Context, instanceIDs []string) (*models.SearchQuery, error) {
|
||||
sequences, err := m.view.GetLatestStylingSequences(ctx, instanceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ type EsRepository struct {
|
||||
eventstore.AdministratorRepo
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, conf Config, static static.Storage, dbClient *database.DB, esV2 *eventstore2.Eventstore) (*EsRepository, error) {
|
||||
es, err := v1.Start(dbClient)
|
||||
func Start(ctx context.Context, conf Config, static static.Storage, dbClient *database.DB, esV2 *eventstore2.Eventstore, allowOrderByCreationDate bool) (*EsRepository, error) {
|
||||
es, err := v1.Start(dbClient, allowOrderByCreationDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
@@ -15,12 +16,12 @@ func (v *View) saveCurrentSequence(viewName string, event *models.Event) error {
|
||||
return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate)
|
||||
}
|
||||
|
||||
func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID)
|
||||
func (v *View) latestSequence(ctx context.Context, viewName, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequence(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) latestSequences(viewName string, instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequences(v.Db, sequencesTable, viewName, instanceIDs)
|
||||
func (v *View) latestSequences(ctx context.Context, viewName string, instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequences(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) AllCurrentSequences(db, instanceID string) ([]*repository.CurrentSequence, error) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/iam/repository/view"
|
||||
"github.com/zitadel/zitadel/internal/iam/repository/view/model"
|
||||
@@ -39,12 +41,12 @@ func (v *View) UpdateOrgOwnerRemovedStyling(event *models.Event) error {
|
||||
return v.ProcessedStylingSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestStylingSequence(instanceID string) (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(stylingTyble, instanceID)
|
||||
func (v *View) GetLatestStylingSequence(ctx context.Context, instanceID string) (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(ctx, stylingTyble, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestStylingSequences(instanceIDs []string) ([]*global_view.CurrentSequence, error) {
|
||||
return v.latestSequences(stylingTyble, instanceIDs)
|
||||
func (v *View) GetLatestStylingSequences(ctx context.Context, instanceIDs []string) ([]*global_view.CurrentSequence, error) {
|
||||
return v.latestSequences(ctx, stylingTyble, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedStylingSequence(event *models.Event) error {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
)
|
||||
|
||||
type View struct {
|
||||
Db *gorm.DB
|
||||
Db *gorm.DB
|
||||
client *database.DB
|
||||
}
|
||||
|
||||
func StartView(sqlClient *database.DB) (*View, error) {
|
||||
@@ -15,10 +20,15 @@ func StartView(sqlClient *database.DB) (*View, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &View{
|
||||
Db: gorm,
|
||||
Db: gorm,
|
||||
client: sqlClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *View) Health() (err error) {
|
||||
return v.Db.DB().Ping()
|
||||
}
|
||||
|
||||
func (v *View) TimeTravel(ctx context.Context, tableName string) string {
|
||||
return tableName + v.client.Timetravel(call.Took(ctx))
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ func (a *API) RegisterService(ctx context.Context, grpcServer server.Server) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleFunc allows registering a [http.HandlerFunc] on an exact
|
||||
// path, instead of prefix like RegisterHandlerOnPrefix.
|
||||
func (a *API) HandleFunc(path string, f http.HandlerFunc) {
|
||||
a.router.HandleFunc(path, f)
|
||||
}
|
||||
|
||||
// RegisterHandlerOnPrefix registers a http handler on a path prefix
|
||||
// the prefix will not be passed to the actual handler
|
||||
func (a *API) RegisterHandlerOnPrefix(prefix string, handler http.Handler) {
|
||||
|
||||
@@ -14,11 +14,11 @@ const (
|
||||
authenticated = "authenticated"
|
||||
)
|
||||
|
||||
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
|
||||
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgIDHeader string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
|
||||
ctx, span := tracing.NewServerInterceptorSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, verifier, method)
|
||||
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgIDHeader, verifier, method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
|
||||
}, nil
|
||||
}
|
||||
|
||||
requestedPermissions, allPermissions, err := getUserMethodPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig, ctxData)
|
||||
requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -110,18 +110,6 @@ func HasGlobalPermission(perms []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func HasGlobalExplicitPermission(perms []string, permToCheck string) bool {
|
||||
for _, perm := range perms {
|
||||
p, ctxID := SplitPermission(perm)
|
||||
if p == permToCheck {
|
||||
if ctxID == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetAllPermissionCtxIDs(perms []string) []string {
|
||||
ctxIDs := make([]string, 0)
|
||||
for _, perm := range perms {
|
||||
@@ -132,16 +120,3 @@ func GetAllPermissionCtxIDs(perms []string) []string {
|
||||
}
|
||||
return ctxIDs
|
||||
}
|
||||
|
||||
func GetExplicitPermissionCtxIDs(perms []string, searchPerm string) []string {
|
||||
ctxIDs := make([]string, 0)
|
||||
for _, perm := range perms {
|
||||
p, ctxID := SplitPermission(perm)
|
||||
if p == searchPerm {
|
||||
if ctxID != "" {
|
||||
ctxIDs = append(ctxIDs, ctxID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctxIDs
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ type MethodMapping map[string]Option
|
||||
type Option struct {
|
||||
Permission string
|
||||
CheckParam string
|
||||
Feature string
|
||||
AllowSelf bool
|
||||
}
|
||||
|
||||
func (a *Config) getPermissionsFromRole(role string) []string {
|
||||
for _, roleMap := range a.RolePermissionMappings {
|
||||
func getPermissionsFromRole(rolePermissionMappings []RoleMapping, role string) []string {
|
||||
for _, roleMap := range rolePermissionMappings {
|
||||
if roleMap.Role == role {
|
||||
return roleMap.Permissions
|
||||
}
|
||||
|
||||
@@ -7,7 +7,28 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPerm string, authConfig Config, ctxData CtxData) (requestedPermissions, allPermissions []string, err error) {
|
||||
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||
ctxData := GetCtxData(ctx)
|
||||
if allowSelf && ctxData.UserID == resourceID {
|
||||
return nil
|
||||
}
|
||||
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, ctxData, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, userPermissionSpan := tracing.NewNamedSpan(ctx, "checkUserPermissions")
|
||||
err = checkUserResourcePermissions(requestedPermissions, resourceID)
|
||||
userPermissionSpan.EndWithError(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level),
|
||||
// and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately.
|
||||
func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -16,13 +37,13 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, dataKey, ctxData)
|
||||
memberships, err := t.SearchMyMemberships(ctx)
|
||||
memberships, err := resolver.SearchMyMemberships(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(memberships) == 0 {
|
||||
err = retry(func() error {
|
||||
memberships, err = t.SearchMyMemberships(ctx)
|
||||
memberships, err = resolver.SearchMyMemberships(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -35,24 +56,56 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer
|
||||
return nil, nil, nil
|
||||
}
|
||||
}
|
||||
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, authConfig)
|
||||
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, roleMappings)
|
||||
return requestedPermissions, allPermissions, nil
|
||||
}
|
||||
|
||||
func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, authConfig Config) (requestPermissions, allPermissions []string) {
|
||||
// checkUserResourcePermissions checks that if a user i granted either the requested permission globally (project.write)
|
||||
// or the specific resource (project.write:123)
|
||||
func checkUserResourcePermissions(userPerms []string, resourceID string) error {
|
||||
if len(userPerms) == 0 {
|
||||
return errors.ThrowPermissionDenied(nil, "AUTH-AWfge", "No matching permissions found")
|
||||
}
|
||||
|
||||
if resourceID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if HasGlobalPermission(userPerms) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if hasContextResourcePermission(userPerms, resourceID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.ThrowPermissionDenied(nil, "AUTH-Swrgg2", "No matching permissions found")
|
||||
}
|
||||
|
||||
func hasContextResourcePermission(permissions []string, resourceID string) bool {
|
||||
for _, perm := range permissions {
|
||||
_, ctxID := SplitPermission(perm)
|
||||
if resourceID == ctxID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, roleMappings []RoleMapping) (requestPermissions, allPermissions []string) {
|
||||
requestPermissions = make([]string, 0)
|
||||
allPermissions = make([]string, 0)
|
||||
for _, membership := range memberships {
|
||||
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, authConfig, requestPermissions, allPermissions)
|
||||
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, roleMappings, requestPermissions, allPermissions)
|
||||
}
|
||||
|
||||
return requestPermissions, allPermissions
|
||||
}
|
||||
|
||||
func mapMembershipToPerm(requiredPerm string, membership *Membership, authConfig Config, requestPermissions, allPermissions []string) ([]string, []string) {
|
||||
func mapMembershipToPerm(requiredPerm string, membership *Membership, roleMappings []RoleMapping, requestPermissions, allPermissions []string) ([]string, []string) {
|
||||
roleNames, roleContextID := roleWithContext(membership)
|
||||
for _, roleName := range roleNames {
|
||||
perms := authConfig.getPermissionsFromRole(roleName)
|
||||
perms := getPermissionsFromRole(roleMappings, roleName)
|
||||
|
||||
for _, p := range perms {
|
||||
permWithCtx := addRoleContextIDToPerm(p, roleContextID)
|
||||
|
||||
@@ -18,7 +18,7 @@ type testVerifier struct {
|
||||
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
|
||||
return "userID", "agentID", "clientID", "de", "orgID", nil
|
||||
}
|
||||
func (v *testVerifier) SearchMyMemberships(ctx context.Context) ([]*Membership, error) {
|
||||
func (v *testVerifier) SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error) {
|
||||
return v.memberships, nil
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func equalStringArray(a, b []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
func Test_GetUserPermissions(t *testing.T) {
|
||||
type args struct {
|
||||
ctxData CtxData
|
||||
verifier *TokenVerifier
|
||||
@@ -139,7 +139,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, perms, err := getUserMethodPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig, tt.args.ctxData)
|
||||
_, perms, err := getUserPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
|
||||
|
||||
if tt.wantErr && err == nil {
|
||||
t.Errorf("got wrong result, should get err: actual: %v ", err)
|
||||
@@ -295,7 +295,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig)
|
||||
requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings)
|
||||
if !equalStringArray(requestPerms, tt.requestPerms) {
|
||||
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
|
||||
}
|
||||
@@ -435,7 +435,7 @@ func Test_MapMembershipToPerm(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig, tt.args.requestPerms, tt.args.allPerms)
|
||||
requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings, tt.args.requestPerms, tt.args.allPerms)
|
||||
if !equalStringArray(requestPerms, tt.requestPerms) {
|
||||
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
|
||||
}
|
||||
@@ -519,3 +519,109 @@ func Test_ExistisPerm(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CheckUserResourcePermissions(t *testing.T) {
|
||||
type args struct {
|
||||
perms []string
|
||||
resourceID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no permissions",
|
||||
args: args{
|
||||
perms: []string{},
|
||||
resourceID: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "has permission and no context requested",
|
||||
args: args{
|
||||
perms: []string{"project.read"},
|
||||
resourceID: "",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "context requested and has global permission",
|
||||
args: args{
|
||||
perms: []string{"project.read", "project.read:1"},
|
||||
resourceID: "Test",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "context requested and has specific permission",
|
||||
args: args{
|
||||
perms: []string{"project.read:Test"},
|
||||
resourceID: "Test",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "context requested and has no permission",
|
||||
args: args{
|
||||
perms: []string{"project.read:Test"},
|
||||
resourceID: "Hodor",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := checkUserResourcePermissions(tt.args.perms, tt.args.resourceID)
|
||||
if tt.wantErr && err == nil {
|
||||
t.Errorf("got wrong result, should get err: actual: %v ", err)
|
||||
}
|
||||
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Errorf("shouldn't get err: %v ", err)
|
||||
}
|
||||
|
||||
if tt.wantErr && !caos_errs.IsPermissionDenied(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_HasContextResourcePermission(t *testing.T) {
|
||||
type args struct {
|
||||
perms []string
|
||||
resourceID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
name: "existing context permission",
|
||||
args: args{
|
||||
perms: []string{"test:wrong", "test:right"},
|
||||
resourceID: "right",
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "not existing context permission",
|
||||
args: args{
|
||||
perms: []string{"test:wrong", "test:wrong2"},
|
||||
resourceID: "test",
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := hasContextResourcePermission(tt.args.perms, tt.args.resourceID)
|
||||
if result != tt.result {
|
||||
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,14 @@ type TokenVerifier struct {
|
||||
systemJWTProfile op.JWTProfileVerifier
|
||||
}
|
||||
|
||||
type MembershipsResolver interface {
|
||||
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
||||
}
|
||||
|
||||
type authZRepo interface {
|
||||
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
|
||||
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
|
||||
SearchMyMemberships(ctx context.Context) ([]*Membership, error)
|
||||
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
||||
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
|
||||
ExistsOrg(ctx context.Context, orgID string) error
|
||||
}
|
||||
@@ -127,10 +131,10 @@ func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings Me
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context) (_ []*Membership, err error) {
|
||||
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context, orgID string) (_ []*Membership, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
return v.authZRepo.SearchMyMemberships(ctx)
|
||||
return v.authZRepo.SearchMyMemberships(ctx, orgID)
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
|
||||
|
||||
@@ -71,7 +71,7 @@ func (s *Server) AppName() string {
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return admin.AdminService_MethodPrefix
|
||||
return admin.AdminService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
|
||||
@@ -69,7 +69,7 @@ func (s *Server) AppName() string {
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return auth.AuthService_MethodPrefix
|
||||
return auth.AuthService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
|
||||
@@ -63,7 +63,7 @@ func (s *Server) AppName() string {
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return management.ManagementService_MethodPrefix
|
||||
return management.ManagementService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
|
||||
@@ -210,17 +210,14 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe
|
||||
}
|
||||
|
||||
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
|
||||
details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToAddHuman(req))
|
||||
human := AddHumanUserRequestToAddHuman(req)
|
||||
err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mgmt_pb.AddHumanUserResponse{
|
||||
UserId: details.ID,
|
||||
Details: obj_grpc.AddToDetailsPb(
|
||||
details.Sequence,
|
||||
details.EventDate,
|
||||
details.ResourceOwner,
|
||||
),
|
||||
UserId: human.ID,
|
||||
Details: obj_grpc.DomainToAddDetailsPb(human.Details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
)
|
||||
|
||||
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
||||
details := &object.Details{
|
||||
Sequence: objectDetail.Sequence,
|
||||
ResourceOwner: objectDetail.ResourceOwner,
|
||||
}
|
||||
if !objectDetail.EventDate.IsZero() {
|
||||
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
|
||||
}
|
||||
return details
|
||||
}
|
||||
@@ -136,6 +136,8 @@ func OIDCGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app_pb.OIDCGra
|
||||
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT
|
||||
case domain.OIDCGrantTypeRefreshToken:
|
||||
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN
|
||||
case domain.OIDCGrantTypeDeviceCode:
|
||||
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE
|
||||
}
|
||||
}
|
||||
return oidcGrantTypes
|
||||
@@ -154,6 +156,8 @@ func OIDCGrantTypesToDomain(grantTypes []app_pb.OIDCGrantType) []domain.OIDCGran
|
||||
oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit
|
||||
case app_pb.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN:
|
||||
oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken
|
||||
case app_pb.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE:
|
||||
oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode
|
||||
}
|
||||
}
|
||||
return oidcGrantTypes
|
||||
|
||||
@@ -34,6 +34,9 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
||||
}
|
||||
|
||||
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
|
||||
if o, ok := req.(AuthContext); ok {
|
||||
orgID = o.AuthContext()
|
||||
}
|
||||
|
||||
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod)
|
||||
if err != nil {
|
||||
@@ -42,3 +45,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
||||
span.End()
|
||||
return handler(ctxSetter(ctx), req)
|
||||
}
|
||||
|
||||
type AuthContext interface {
|
||||
AuthContext() string
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type verifierMock struct{}
|
||||
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
|
||||
return "", "", "", "", "", nil
|
||||
}
|
||||
func (v *verifierMock) SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) {
|
||||
func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string) ([]*authz.Membership, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -50,13 +50,13 @@ func CreateServer(
|
||||
middleware.MetricsHandler(metricTypes, grpc_api.Probes...),
|
||||
middleware.NoCacheInterceptor(),
|
||||
middleware.ErrorHandler(),
|
||||
middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_MethodPrefix, healthpb.Health_ServiceDesc.ServiceName),
|
||||
middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName),
|
||||
middleware.AccessStorageInterceptor(accessSvc),
|
||||
middleware.AuthorizationInterceptor(verifier, authConfig),
|
||||
middleware.TranslationHandler(),
|
||||
middleware.ValidationHandler(),
|
||||
middleware.ServiceHandler(),
|
||||
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_MethodPrefix),
|
||||
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/admin/repository"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
@@ -60,7 +59,7 @@ func (s *Server) AppName() string {
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return system.SystemService_MethodPrefix
|
||||
return system.SystemService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
||||
var resourceOwner string // TODO: check if still needed
|
||||
var email *domain.Email
|
||||
|
||||
switch v := req.GetVerification().(type) {
|
||||
case *user.SetEmailRequest_SendCode:
|
||||
email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
|
||||
case *user.SetEmailRequest_ReturnCode:
|
||||
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
|
||||
case *user.SetEmailRequest_IsVerified:
|
||||
if v.IsVerified {
|
||||
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), resourceOwner, req.GetEmail())
|
||||
} else {
|
||||
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
|
||||
}
|
||||
case nil:
|
||||
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
|
||||
default:
|
||||
err = caos_errs.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.SetEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: email.Sequence,
|
||||
ChangeDate: timestamppb.New(email.ChangeDate),
|
||||
ResourceOwner: email.ResourceOwner,
|
||||
},
|
||||
VerificationCode: email.PlainCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) {
|
||||
details, err := s.command.VerifyUserEmail(ctx,
|
||||
req.GetUserId(),
|
||||
"", // TODO: check if still needed
|
||||
req.GetVerificationCode(),
|
||||
s.userCodeAlg,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.VerifyEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: details.Sequence,
|
||||
ChangeDate: timestamppb.New(details.EventDate),
|
||||
ResourceOwner: details.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -6,27 +6,27 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
var _ user.UserServiceServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
user.UnimplementedUserServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
) *Server {
|
||||
func CreateServer(command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
command: command,
|
||||
query: query,
|
||||
userCodeAlg: userCodeAlg,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func (s *Server) TestGet(ctx context.Context, req *user.TestGetRequest) (*user.TestGetResponse, error) {
|
||||
return &user.TestGetResponse{
|
||||
Ctx: req.Ctx.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) TestPost(ctx context.Context, req *user.TestPostRequest) (*user.TestPostResponse, error) {
|
||||
return &user.TestPostResponse{
|
||||
Ctx: req.Ctx.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) TestAuth(ctx context.Context, req *user.TestAuthRequest) (*user.TestAuthResponse, error) {
|
||||
reqCtx, err := authDemo(ctx, req.Ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.TestAuthResponse{
|
||||
User: &user.User{Id: authz.GetCtxData(ctx).UserID},
|
||||
Ctx: reqCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func authDemo(ctx context.Context, reqCtx *user.Context) (*user.Context, error) {
|
||||
ro := authz.GetCtxData(ctx).ResourceOwner
|
||||
if reqCtx == nil {
|
||||
return &user.Context{Ctx: &user.Context_OrgId{OrgId: ro}}, nil
|
||||
}
|
||||
switch c := reqCtx.Ctx.(type) {
|
||||
case *user.Context_OrgId:
|
||||
if c.OrgId == ro {
|
||||
return reqCtx, nil
|
||||
}
|
||||
return nil, errors.ThrowPermissionDenied(nil, "USER-dg4g", "Errors.User.NotAllowedOrg")
|
||||
case *user.Context_OrgDomain:
|
||||
if c.OrgDomain == "forbidden.com" {
|
||||
return nil, errors.ThrowPermissionDenied(nil, "USER-SDg4g", "Errors.User.NotAllowedOrg")
|
||||
}
|
||||
return reqCtx, nil
|
||||
case *user.Context_Instance:
|
||||
return reqCtx, nil
|
||||
default:
|
||||
return reqCtx, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
||||
human, err := addUserRequestToAddHuman(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orgID := req.GetOrganisation().GetOrgId()
|
||||
if orgID == "" {
|
||||
orgID = authz.GetCtxData(ctx).OrgID
|
||||
}
|
||||
err = s.command.AddHuman(ctx, orgID, human, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddHumanUserResponse{
|
||||
UserId: human.ID,
|
||||
Details: object.DomainToDetailsPb(human.Details),
|
||||
EmailCode: human.EmailCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
|
||||
username := req.GetUsername()
|
||||
if username == "" {
|
||||
username = req.GetEmail().GetEmail()
|
||||
}
|
||||
var urlTemplate string
|
||||
if req.GetEmail().GetSendCode() != nil {
|
||||
urlTemplate = req.GetEmail().GetSendCode().GetUrlTemplate()
|
||||
// test the template execution so the async notification will not fail because of it and the user won't realize
|
||||
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
bcryptedPassword, err := hashedPasswordToCommand(req.GetHashedPassword())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired()
|
||||
metadata := make([]*command.AddMetadataEntry, len(req.Metadata))
|
||||
for i, metadataEntry := range req.Metadata {
|
||||
metadata[i] = &command.AddMetadataEntry{
|
||||
Key: metadataEntry.GetKey(),
|
||||
Value: metadataEntry.GetValue(),
|
||||
}
|
||||
}
|
||||
return &command.AddHuman{
|
||||
ID: req.GetUserId(),
|
||||
Username: username,
|
||||
FirstName: req.GetProfile().GetFirstName(),
|
||||
LastName: req.GetProfile().GetLastName(),
|
||||
NickName: req.GetProfile().GetNickName(),
|
||||
DisplayName: req.GetProfile().GetDisplayName(),
|
||||
Email: command.Email{
|
||||
Address: domain.EmailAddress(req.GetEmail().GetEmail()),
|
||||
Verified: req.GetEmail().GetIsVerified(),
|
||||
ReturnCode: req.GetEmail().GetReturnCode() != nil,
|
||||
URLTemplate: urlTemplate,
|
||||
},
|
||||
PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()),
|
||||
Gender: genderToDomain(req.GetProfile().GetGender()),
|
||||
Phone: command.Phone{}, // TODO: add as soon as possible
|
||||
Password: req.GetPassword().GetPassword(),
|
||||
BcryptedPassword: bcryptedPassword,
|
||||
PasswordChangeRequired: passwordChangeRequired,
|
||||
Passwordless: false,
|
||||
ExternalIDP: false,
|
||||
Register: false,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func genderToDomain(gender user.Gender) domain.Gender {
|
||||
switch gender {
|
||||
case user.Gender_GENDER_UNSPECIFIED:
|
||||
return domain.GenderUnspecified
|
||||
case user.Gender_GENDER_FEMALE:
|
||||
return domain.GenderFemale
|
||||
case user.Gender_GENDER_MALE:
|
||||
return domain.GenderMale
|
||||
case user.Gender_GENDER_DIVERSE:
|
||||
return domain.GenderDiverse
|
||||
default:
|
||||
return domain.GenderUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func hashedPasswordToCommand(hashed *user.HashedPassword) (string, error) {
|
||||
if hashed == nil {
|
||||
return "", nil
|
||||
}
|
||||
// we currently only handle bcrypt
|
||||
if hashed.GetAlgorithm() != "bcrypt" {
|
||||
return "", errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument")
|
||||
}
|
||||
return hashed.GetHash(), nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func Test_hashedPasswordToCommand(t *testing.T) {
|
||||
type args struct {
|
||||
hashed *user.HashedPassword
|
||||
}
|
||||
type res struct {
|
||||
want string
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"not hashed",
|
||||
args{
|
||||
hashed: nil,
|
||||
},
|
||||
res{
|
||||
"",
|
||||
nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"hashed, not bcrypt",
|
||||
args{
|
||||
hashed: &user.HashedPassword{
|
||||
Hash: "hash",
|
||||
Algorithm: "custom",
|
||||
},
|
||||
},
|
||||
res{
|
||||
"",
|
||||
func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"hashed, bcrypt",
|
||||
args{
|
||||
hashed: &user.HashedPassword{
|
||||
Hash: "hash",
|
||||
Algorithm: "bcrypt",
|
||||
},
|
||||
},
|
||||
res{
|
||||
"hash",
|
||||
nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := hashedPasswordToCommand(tt.args.hashed)
|
||||
if tt.res.err == nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if tt.res.err != nil && !tt.res.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
if tt.res.err == nil {
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -99,15 +99,6 @@ func (a *AuthRequest) GetSubject() string {
|
||||
return a.UserID
|
||||
}
|
||||
|
||||
func (a *AuthRequest) Done() bool {
|
||||
for _, step := range a.PossibleSteps {
|
||||
if step.Type() == domain.NextStepRedirectToCallback {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AuthRequest) oidc() *domain.AuthRequestOIDC {
|
||||
return a.Request.(*domain.AuthRequestOIDC)
|
||||
}
|
||||
|
||||
@@ -200,6 +200,8 @@ func grantTypeToOIDC(grantType domain.OIDCGrantType) oidc.GrantType {
|
||||
return oidc.GrantTypeImplicit
|
||||
case domain.OIDCGrantTypeRefreshToken:
|
||||
return oidc.GrantTypeRefreshToken
|
||||
case domain.OIDCGrantTypeDeviceCode:
|
||||
return oidc.GrantTypeDeviceCode
|
||||
default:
|
||||
return oidc.GrantTypeCode
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v2/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
const (
|
||||
DeviceAuthDefaultLifetime = 5 * time.Minute
|
||||
DeviceAuthDefaultPollInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
type DeviceAuthorizationConfig struct {
|
||||
Lifetime time.Duration
|
||||
PollInterval time.Duration
|
||||
UserCode *UserCodeConfig
|
||||
}
|
||||
|
||||
type UserCodeConfig struct {
|
||||
CharSet string
|
||||
CharAmount int
|
||||
DashInterval int
|
||||
}
|
||||
|
||||
// toOPConfig converts DeviceAuthorizationConfig to a [op.DeviceAuthorizationConfig],
|
||||
// setting sane defaults for empty values.
|
||||
// Safe to call when c is nil.
|
||||
func (c *DeviceAuthorizationConfig) toOPConfig() op.DeviceAuthorizationConfig {
|
||||
out := op.DeviceAuthorizationConfig{
|
||||
Lifetime: DeviceAuthDefaultLifetime,
|
||||
PollInterval: DeviceAuthDefaultPollInterval,
|
||||
UserFormPath: login.EndpointDeviceAuth,
|
||||
UserCode: op.UserCodeBase20,
|
||||
}
|
||||
if c == nil {
|
||||
return out
|
||||
}
|
||||
if c.Lifetime != 0 {
|
||||
out.Lifetime = c.Lifetime
|
||||
}
|
||||
if c.PollInterval != 0 {
|
||||
out.PollInterval = c.PollInterval
|
||||
}
|
||||
|
||||
if c.UserCode == nil {
|
||||
return out
|
||||
}
|
||||
if c.UserCode.CharSet != "" {
|
||||
out.UserCode.CharSet = c.UserCode.CharSet
|
||||
}
|
||||
if c.UserCode.CharAmount != 0 {
|
||||
out.UserCode.CharAmount = c.UserCode.CharAmount
|
||||
}
|
||||
if c.UserCode.DashInterval != 0 {
|
||||
out.UserCode.DashInterval = c.UserCode.CharAmount
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// StoreDeviceAuthorization creates a new Device Authorization request.
|
||||
// Implements the op.DeviceAuthorizationStorage interface.
|
||||
func (o *OPStorage) StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) (err error) {
|
||||
const logMsg = "store device authorization"
|
||||
logger := logging.WithFields("client_id", clientID, "device_code", deviceCode, "user_code", userCode, "expires", expires, "scopes", scopes)
|
||||
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() {
|
||||
logger.OnError(err).Error(logMsg)
|
||||
span.EndWithError(err)
|
||||
}()
|
||||
|
||||
// TODO(muhlemmer): Remove the following code block with oidc v3
|
||||
// https://github.com/zitadel/oidc/issues/370
|
||||
client, err := o.GetClientByClientID(ctx, clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !op.ValidateGrantType(client, oidc.GrantTypeDeviceCode) {
|
||||
return errors.ThrowPermissionDeniedf(nil, "OIDC-et1Ae", "grant type %q not allowed for client", oidc.GrantTypeDeviceCode)
|
||||
}
|
||||
|
||||
scopes, err = o.assertProjectRoleScopes(ctx, clientID, scopes)
|
||||
if err != nil {
|
||||
return errors.ThrowPreconditionFailed(err, "OIDC-She4t", "Errors.Internal")
|
||||
}
|
||||
aggrID, details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, expires, scopes)
|
||||
if err == nil {
|
||||
logger.SetFields("aggregate_id", aggrID, "details", details).Debug(logMsg)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func newDeviceAuthorizationState(d *domain.DeviceAuth) *op.DeviceAuthorizationState {
|
||||
return &op.DeviceAuthorizationState{
|
||||
ClientID: d.ClientID,
|
||||
Scopes: d.Scopes,
|
||||
Expires: d.Expires,
|
||||
Done: d.State.Done(),
|
||||
Subject: d.Subject,
|
||||
Denied: d.State.Denied(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetDeviceAuthorizatonState retieves the current state of the Device Authorization process.
|
||||
// It implements the [op.DeviceAuthorizationStorage] interface and is used by devices that
|
||||
// are polling until they successfully receive a token or we indicate a denied or expired state.
|
||||
// As generated user codes are of low entropy, this implementation also takes care or
|
||||
// device authorization request cleanup, when it has been Approved, Denied or Expired.
|
||||
func (o *OPStorage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (state *op.DeviceAuthorizationState, err error) {
|
||||
const logMsg = "get device authorization state"
|
||||
logger := logging.WithFields("client_id", clientID, "device_code", deviceCode)
|
||||
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logger.WithError(err).Error(logMsg)
|
||||
}
|
||||
span.EndWithError(err)
|
||||
}()
|
||||
|
||||
deviceAuth, err := o.query.DeviceAuthByDeviceCode(ctx, clientID, deviceCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.SetFields(
|
||||
"expires", deviceAuth.Expires, "scopes", deviceAuth.Scopes,
|
||||
"subject", deviceAuth.Subject, "state", deviceAuth.State,
|
||||
).Debug("device authorization state")
|
||||
|
||||
// Cancel the request if it is expired, only if it wasn't Done meanwhile
|
||||
if !deviceAuth.State.Done() && deviceAuth.Expires.Before(time.Now()) {
|
||||
_, err = o.command.CancelDeviceAuth(ctx, deviceAuth.AggregateID, domain.DeviceAuthCanceledExpired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deviceAuth.State = domain.DeviceAuthStateExpired
|
||||
}
|
||||
|
||||
// When the request is more then initiated, it has been either Approved, Denied or Expired.
|
||||
// At this point we should remove it from the DB to avoid user code conflicts.
|
||||
if deviceAuth.State > domain.DeviceAuthStateInitiated {
|
||||
_, err = o.command.RemoveDeviceAuth(ctx, deviceAuth.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return newDeviceAuthorizationState(deviceAuth), nil
|
||||
}
|
||||
|
||||
// TODO(muhlemmer): remove the following methods with oidc v3.
|
||||
// They are actually not used, but are required by the oidc device storage interface.
|
||||
// https://github.com/zitadel/oidc/issues/371
|
||||
func (o *OPStorage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) DenyDeviceAuthorization(ctx context.Context, userCode string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO end.
|
||||
@@ -176,7 +176,7 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm
|
||||
if errors.IsErrorAlreadyExists(err) {
|
||||
return nil
|
||||
}
|
||||
logging.OnError(err).Warn("initial lock failed")
|
||||
logging.OnError(err).Debug("initial lock failed")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ type Config struct {
|
||||
UserAgentCookieConfig *middleware.UserAgentCookieConfig
|
||||
Cache *middleware.CacheConfig
|
||||
CustomEndpoints *EndpointConfig
|
||||
DeviceAuth *DeviceAuthorizationConfig
|
||||
}
|
||||
|
||||
type EndpointConfig struct {
|
||||
@@ -50,6 +51,7 @@ type EndpointConfig struct {
|
||||
Revocation *Endpoint
|
||||
EndSession *Endpoint
|
||||
Keys *Endpoint
|
||||
DeviceAuth *Endpoint
|
||||
}
|
||||
|
||||
type Endpoint struct {
|
||||
@@ -108,6 +110,7 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []
|
||||
GrantTypeRefreshToken: config.GrantTypeRefreshToken,
|
||||
RequestObjectSupported: config.RequestObjectSupported,
|
||||
SupportedUILocales: supportedLanguages,
|
||||
DeviceAuthorization: config.DeviceAuth.toOPConfig(),
|
||||
}
|
||||
if cryptoLength := len(cryptoKey); cryptoLength != 32 {
|
||||
return nil, caos_errs.ThrowInternalf(nil, "OIDC-D43gf", "crypto key must be 32 bytes, but is %d", cryptoLength)
|
||||
@@ -165,6 +168,9 @@ func customEndpoints(endpointConfig *EndpointConfig) []op.Option {
|
||||
if endpointConfig.Keys != nil {
|
||||
options = append(options, op.WithCustomKeysEndpoint(op.NewEndpointWithURL(endpointConfig.Keys.Path, endpointConfig.Keys.URL)))
|
||||
}
|
||||
if endpointConfig.DeviceAuth != nil {
|
||||
options = append(options, op.WithCustomDeviceAuthorizationEndpoint(op.NewEndpointWithURL(endpointConfig.DeviceAuth.Path, endpointConfig.DeviceAuth.URL)))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
|
||||
@@ -63,14 +63,6 @@ func (a *AuthRequest) GetUserID() string {
|
||||
func (a *AuthRequest) GetUserName() string {
|
||||
return a.UserName
|
||||
}
|
||||
func (a *AuthRequest) Done() bool {
|
||||
for _, step := range a.PossibleSteps {
|
||||
if step.Type() == domain.NextStepRedirectToCallback {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ models.AuthRequestInt, err error) {
|
||||
if _, ok := authReq.Request.(*domain.AuthRequestSAML); !ok {
|
||||
|
||||
@@ -123,7 +123,7 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do
|
||||
if errors.IsErrorAlreadyExists(err) {
|
||||
return nil
|
||||
}
|
||||
logging.OnError(err).Warn("initial lock failed")
|
||||
logging.OnError(err).Debug("initial lock failed")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
errs "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
tmplDeviceAuthUserCode = "device-usercode"
|
||||
tmplDeviceAuthAction = "device-action"
|
||||
)
|
||||
|
||||
func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, err error) {
|
||||
var errID, errMessage string
|
||||
if err != nil {
|
||||
logging.WithError(err).Error()
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
|
||||
data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
|
||||
translator := l.getTranslator(r.Context(), nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) {
|
||||
data := &struct {
|
||||
baseData
|
||||
AuthRequestID string
|
||||
Username string
|
||||
ClientID string
|
||||
Scopes []string
|
||||
}{
|
||||
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
|
||||
AuthRequestID: authReq.ID,
|
||||
Username: authReq.UserName,
|
||||
ClientID: authReq.ApplicationID,
|
||||
Scopes: scopes,
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil)
|
||||
}
|
||||
|
||||
const (
|
||||
deviceAuthAllowed = "allowed"
|
||||
deviceAuthDenied = "denied"
|
||||
)
|
||||
|
||||
// renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied.
|
||||
func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) {
|
||||
data := &struct {
|
||||
baseData
|
||||
Message string
|
||||
}{
|
||||
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
switch action {
|
||||
case deviceAuthAllowed:
|
||||
data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplSuccess], data, nil)
|
||||
case deviceAuthDenied:
|
||||
data.ErrMessage = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Denied", nil)
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeviceUserCode serves the Device Authorization user code submission form.
|
||||
// The "user_code" may be submitted by URL (GET) or form (POST).
|
||||
// When a "user_code" is received and found through query,
|
||||
// handleDeviceAuthUserCode will create a new AuthRequest in the repository.
|
||||
// The user is then redirected to the /login endpoint to complete authentication.
|
||||
//
|
||||
// The agent ID from the context is set to the authentication request
|
||||
// to ensure the complete login flow is completed from the same browser.
|
||||
func (l *Login) handleDeviceAuthUserCode(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
l.renderDeviceAuthUserCode(w, r, err)
|
||||
return
|
||||
}
|
||||
userCode := r.Form.Get("user_code")
|
||||
if userCode == "" {
|
||||
if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" {
|
||||
err = errs.New(prompt)
|
||||
}
|
||||
l.renderDeviceAuthUserCode(w, r, err)
|
||||
return
|
||||
}
|
||||
deviceAuth, err := l.query.DeviceAuthByUserCode(ctx, userCode)
|
||||
if err != nil {
|
||||
l.renderDeviceAuthUserCode(w, r, err)
|
||||
return
|
||||
}
|
||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||
if !ok {
|
||||
l.renderDeviceAuthUserCode(w, r, errs.New("internal error: agent ID missing"))
|
||||
return
|
||||
}
|
||||
authRequest, err := l.authRepo.CreateAuthRequest(ctx, &domain.AuthRequest{
|
||||
CreationDate: time.Now(),
|
||||
AgentID: userAgentID,
|
||||
ApplicationID: deviceAuth.ClientID,
|
||||
InstanceID: authz.GetInstance(ctx).InstanceID(),
|
||||
Request: &domain.AuthRequestDevice{
|
||||
ID: deviceAuth.AggregateID,
|
||||
DeviceCode: deviceAuth.DeviceCode,
|
||||
UserCode: deviceAuth.UserCode,
|
||||
Scopes: deviceAuth.Scopes,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
l.renderDeviceAuthUserCode(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, l.renderer.pathPrefix+EndpointLogin+"?authRequestID="+authRequest.ID, http.StatusFound)
|
||||
}
|
||||
|
||||
// redirectDeviceAuthStart redirects the user to the start point of
|
||||
// the device authorization flow. A prompt can be set to inform the user
|
||||
// of the reason why they are redirected back.
|
||||
func (l *Login) redirectDeviceAuthStart(w http.ResponseWriter, r *http.Request, prompt string) {
|
||||
values := make(url.Values)
|
||||
values.Set("prompt", url.QueryEscape(prompt))
|
||||
|
||||
url := url.URL{
|
||||
Path: l.renderer.pathPrefix + EndpointDeviceAuth,
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
http.Redirect(w, r, url.String(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleDeviceAuthAction is the handler where the user is redirected after login.
|
||||
// The authRequest is checked if the login was indeed completed.
|
||||
// When the action of "allowed" or "denied", the device authorization is updated accordingly.
|
||||
// Else the user is presented with a page where they can choose / submit either action.
|
||||
func (l *Login) handleDeviceAuthAction(w http.ResponseWriter, r *http.Request) {
|
||||
authReq, err := l.getAuthRequest(r)
|
||||
if authReq == nil {
|
||||
err = errors.ThrowInvalidArgument(err, "LOGIN-OLah8", "invalid or missing auth request")
|
||||
l.redirectDeviceAuthStart(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
if !authReq.Done() {
|
||||
l.redirectDeviceAuthStart(w, r, "authentication not completed")
|
||||
return
|
||||
}
|
||||
authDev, ok := authReq.Request.(*domain.AuthRequestDevice)
|
||||
if !ok {
|
||||
l.redirectDeviceAuthStart(w, r, fmt.Sprintf("wrong auth request type: %T", authReq.Request))
|
||||
return
|
||||
}
|
||||
|
||||
action := mux.Vars(r)["action"]
|
||||
switch action {
|
||||
case deviceAuthAllowed:
|
||||
_, err = l.command.ApproveDeviceAuth(r.Context(), authDev.ID, authReq.UserID)
|
||||
case deviceAuthDenied:
|
||||
_, err = l.command.CancelDeviceAuth(r.Context(), authDev.ID, domain.DeviceAuthCanceledDenied)
|
||||
default:
|
||||
l.renderDeviceAuthAction(w, r, authReq, authDev.Scopes)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
l.redirectDeviceAuthStart(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
l.renderDeviceAuthDone(w, r, authReq, action)
|
||||
}
|
||||
|
||||
// deviceAuthCallbackURL creates the callback URL with which the user
|
||||
// is redirected back to the device authorization flow.
|
||||
func (l *Login) deviceAuthCallbackURL(authRequestID string) string {
|
||||
return l.renderer.pathPrefix + EndpointDeviceAuthAction + "?authRequestID=" + authRequestID
|
||||
}
|
||||
|
||||
// RedirectDeviceAuthToPrefix allows users to use https://domain.com/device without the /ui/login prefix
|
||||
// and redirects them to the prefixed endpoint.
|
||||
// [rfc 8628](https://www.rfc-editor.org/rfc/rfc8628#section-3.2) recommends the URL to be as short as possible.
|
||||
func RedirectDeviceAuthToPrefix(w http.ResponseWriter, r *http.Request) {
|
||||
target := gu.PtrCopy(r.URL)
|
||||
target.Path = HandlerPrefix + EndpointDeviceAuth
|
||||
http.Redirect(w, r, target.String(), http.StatusFound)
|
||||
}
|
||||
@@ -69,6 +69,8 @@ func (l *Login) authRequestCallback(ctx context.Context, authReq *domain.AuthReq
|
||||
return l.oidcAuthCallbackURL(ctx, authReq.ID), nil
|
||||
case *domain.AuthRequestSAML:
|
||||
return l.samlAuthCallbackURL(ctx, authReq.ID), nil
|
||||
case *domain.AuthRequestDevice:
|
||||
return l.deviceAuthCallbackURL(authReq.ID), nil
|
||||
default:
|
||||
return "", caos_errs.ThrowInternal(nil, "LOGIN-rhjQF", "Errors.AuthRequest.RequestTypeNotSupported")
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
tmplError = "error"
|
||||
tmplError = "error"
|
||||
tmplSuccess = "success"
|
||||
)
|
||||
|
||||
type Renderer struct {
|
||||
@@ -45,6 +46,7 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
||||
}
|
||||
tmplMapping := map[string]string{
|
||||
tmplError: "error.html",
|
||||
tmplSuccess: "success.html",
|
||||
tmplLogin: "login.html",
|
||||
tmplUserSelection: "select_user.html",
|
||||
tmplPassword: "password.html",
|
||||
@@ -77,6 +79,8 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
||||
tmplExternalNotFoundOption: "external_not_found_option.html",
|
||||
tmplLoginSuccess: "login_success.html",
|
||||
tmplLDAPLogin: "ldap_login.html",
|
||||
tmplDeviceAuthUserCode: "device_usercode.html",
|
||||
tmplDeviceAuthAction: "device_action.html",
|
||||
}
|
||||
funcs := map[string]interface{}{
|
||||
"resourceUrl": func(file string) string {
|
||||
@@ -323,6 +327,7 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
|
||||
func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
|
||||
var msg string
|
||||
if err != nil {
|
||||
logging.WithError(err).WithField("auth_req_id", authReq.ID).Error()
|
||||
_, msg = l.getErrorMessage(r, err)
|
||||
}
|
||||
data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg)
|
||||
|
||||
@@ -46,6 +46,9 @@ const (
|
||||
|
||||
EndpointResources = "/resources"
|
||||
EndpointDynamicResources = "/resources/dynamic"
|
||||
|
||||
EndpointDeviceAuth = "/device"
|
||||
EndpointDeviceAuthAction = "/device/{action}"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -107,5 +110,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
|
||||
router.HandleFunc(EndpointLDAPLogin, login.handleLDAP).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointLDAPCallback, login.handleLDAPCallback).Methods(http.MethodPost)
|
||||
router.SkipClean(true).Handle("", http.RedirectHandler(HandlerPrefix+"/", http.StatusMovedPermanently))
|
||||
router.HandleFunc(EndpointDeviceAuth, login.handleDeviceAuthUserCode).Methods(http.MethodGet, http.MethodPost)
|
||||
router.HandleFunc(EndpointDeviceAuthAction, login.handleDeviceAuthAction).Methods(http.MethodGet, http.MethodPost)
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ RegistrationUser:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
GenderLabel: Geschlecht
|
||||
Female: weiblich
|
||||
Male: männlich
|
||||
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
|
||||
TosConfirm: Ich akzeptiere die
|
||||
TosLinkText: AGBs
|
||||
@@ -313,6 +315,25 @@ ExternalNotFound:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
|
||||
DeviceAuth:
|
||||
Title: Geräteautorisierung
|
||||
UserCode:
|
||||
Label: Benutzercode
|
||||
Description: Geben Sie den auf dem Gerät angezeigten Benutzercode ein
|
||||
ButtonNext: weiter
|
||||
Action:
|
||||
Description: Gerätezugriff erlauben
|
||||
GrantDevice: Sie sind dabei, das Gerät zu erlauben
|
||||
AccessToScopes: Zugriff auf die folgenden Daten
|
||||
Button:
|
||||
Allow: erlauben
|
||||
Deny: verweigern
|
||||
Done:
|
||||
Description: Abgeschlossen
|
||||
Approved: Gerätezulassung genehmigt. Sie können jetzt zum Gerät zurückkehren.
|
||||
Denied: Geräteautorisierung verweigert. Sie können jetzt zum Gerät zurückkehren.
|
||||
|
||||
Footer:
|
||||
PoweredBy: Powered By
|
||||
@@ -422,5 +443,7 @@ Errors:
|
||||
Org:
|
||||
LoginPolicy:
|
||||
RegistrationNotAllowed: Registrierung ist nicht erlaubt
|
||||
DeviceAuth:
|
||||
NotExisting: Benutzercode existiert nicht
|
||||
|
||||
optional: (optional)
|
||||
|
||||
@@ -7,7 +7,7 @@ Login:
|
||||
UsernamePlaceHolder: username
|
||||
LoginnamePlaceHolder: username@domain
|
||||
ExternalUserDescription: Login with an external user.
|
||||
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organisation.
|
||||
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organization.
|
||||
RegisterButtonText: register
|
||||
NextButtonText: next
|
||||
|
||||
@@ -26,7 +26,7 @@ SelectAccount:
|
||||
OtherUser: Other User
|
||||
SessionState0: active
|
||||
SessionState1: inactive
|
||||
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organisation.
|
||||
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organization.
|
||||
|
||||
Password:
|
||||
Title: Password
|
||||
@@ -118,7 +118,7 @@ InitMFADone:
|
||||
MFAProvider:
|
||||
Provider0: Authenticator App (e.g Google/Microsoft Authenticator, Authy)
|
||||
Provider1: Device dependent (e.g FaceID, Windows Hello, Fingerprint)
|
||||
ChooseOther: or choose an other option
|
||||
ChooseOther: or choose another option
|
||||
|
||||
VerifyMFAOTP:
|
||||
Title: Verify 2-Factor
|
||||
@@ -143,7 +143,7 @@ Passwordless:
|
||||
|
||||
PasswordlessPrompt:
|
||||
Title: Passwordless setup
|
||||
Description: Would you like to setup passwordless login? (Authenticationmethods of your device like FaceID, Windows Hello or Fingerprint)
|
||||
Description: Would you like to setup passwordless login? (Authentication methods of your device like FaceID, Windows Hello or Fingerprint)
|
||||
DescriptionInit: You need to set up passwordless login. Use the link you were given to register your device.
|
||||
PasswordlessButtonText: Go passwordless
|
||||
NextButtonText: next
|
||||
@@ -221,6 +221,7 @@ RegistrationUser:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
GenderLabel: Gender
|
||||
Female: Female
|
||||
Male: Male
|
||||
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
|
||||
Chinese: 简体中文
|
||||
Japanese: 日本語
|
||||
Polish: Polski
|
||||
Spanish: Español
|
||||
TosAndPrivacyLabel: Terms and conditions
|
||||
TosConfirm: I accept the
|
||||
TosLinkText: TOS
|
||||
@@ -263,9 +265,9 @@ ExternalRegistrationUserOverview:
|
||||
NextButtonText: save
|
||||
|
||||
RegistrationOrg:
|
||||
Title: Organisation Registration
|
||||
Description: Enter your organisationname and userdata.
|
||||
OrgNameLabel: Organisationname
|
||||
Title: Organization Registration
|
||||
Description: Enter your organization name and userdata.
|
||||
OrgNameLabel: Organization name
|
||||
EmailLabel: E-Mail
|
||||
UsernameLabel: Username
|
||||
FirstnameLabel: First name
|
||||
@@ -313,6 +315,25 @@ ExternalNotFound:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
|
||||
DeviceAuth:
|
||||
Title: Device Authorization
|
||||
UserCode:
|
||||
Label: User Code
|
||||
Description: Enter the user code presented on the device.
|
||||
ButtonNext: next
|
||||
Action:
|
||||
Description: Grant device access.
|
||||
GrantDevice: you are about to grant device
|
||||
AccessToScopes: access to the following scopes
|
||||
Button:
|
||||
Allow: allow
|
||||
Deny: deny
|
||||
Done:
|
||||
Description: Done.
|
||||
Approved: Device authorization approved. You can now return to the device.
|
||||
Denied: Device authorization denied. You can now return to the device.
|
||||
|
||||
Footer:
|
||||
PoweredBy: Powered By
|
||||
@@ -334,8 +355,8 @@ Errors:
|
||||
NotFound: User could not be found
|
||||
AlreadyExists: User already exists
|
||||
Inactive: User is inactive
|
||||
NotFoundOnOrg: User could not be found on chosen organisation
|
||||
NotAllowedOrg: User is no member of the required organisation
|
||||
NotFoundOnOrg: User could not be found on chosen organization
|
||||
NotAllowedOrg: User is no member of the required organization
|
||||
NotMatchingUserID: User and user in authrequest don't match
|
||||
UserIDMissing: UserID is empty
|
||||
Invalid: Invalid userdata
|
||||
@@ -365,11 +386,11 @@ Errors:
|
||||
NotFound: Address not found
|
||||
NotChanged: Address not changed
|
||||
Username:
|
||||
AlreadyExists: Username already taken
|
||||
AlreadyExists: Username already taken
|
||||
Reserved: Username is already taken
|
||||
Empty: Username is empty
|
||||
Password:
|
||||
ConfirmationWrong: Passwordconfirmation is wrong
|
||||
ConfirmationWrong: Password confirmation is wrong
|
||||
Empty: Password is empty
|
||||
Invalid: Password is invalid
|
||||
InvalidAndLocked: Password is invalid and user is locked, contact your administrator.
|
||||
@@ -377,7 +398,7 @@ Errors:
|
||||
Invalid: Username or Password is invalid
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Password policy not found
|
||||
MinLength: Password is to short
|
||||
MinLength: Password is too short
|
||||
HasLower: Password must contain lower letter
|
||||
HasUpper: Password must contain upper letter
|
||||
HasNumber: Password must contain number
|
||||
@@ -413,7 +434,7 @@ Errors:
|
||||
CreationNotAllowed: Creation of a new user is not allowed on this Provider
|
||||
LinkingNotAllowed: Linking of a user is not allowed on this Provider
|
||||
GrantRequired: Login not possible. The user is required to have at least one grant on the application. Please contact your administrator.
|
||||
ProjectRequired: Login not possible. The organisation of the user must be granted to the project. Please contact your administrator.
|
||||
ProjectRequired: Login not possible. The organization of the user must be granted to the project. Please contact your administrator.
|
||||
IdentityProvider:
|
||||
InvalidConfig: Identity Provider configuration is invalid
|
||||
IAM:
|
||||
@@ -422,5 +443,7 @@ Errors:
|
||||
Org:
|
||||
LoginPolicy:
|
||||
RegistrationNotAllowed: Registration is not allowed
|
||||
DeviceAuth:
|
||||
NotExisting: User Code doesn't exist
|
||||
|
||||
optional: (optional)
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
Login:
|
||||
Title: ¡Hola de nuevo!
|
||||
Description: Introduce tus datos de inicio de sesión.
|
||||
TitleLinking: Inicio de sesión para vincular un usuario
|
||||
DescriptionLinking: Introduce tus datos de inicio de sesión para vincular tu usuario externo con un usuario ZITADEL.
|
||||
LoginNameLabel: Nombre de inicio de sesión
|
||||
UsernamePlaceHolder: username
|
||||
LoginnamePlaceHolder: username@dominio
|
||||
ExternalUserDescription: Inicia sesión con un usuario externo.
|
||||
MustBeMemberOfOrg: El usuario debe ser miembro de la organización {{.OrgName}}.
|
||||
RegisterButtonText: registrar
|
||||
NextButtonText: siguiente
|
||||
|
||||
LDAP:
|
||||
Title: Inicio de sesión
|
||||
Description: Introduce tus datos de inicio de sesión.
|
||||
LoginNameLabel: Nombre de inicio de sesión
|
||||
PasswordLabel: Contraseña
|
||||
NextButtonText: siguiente
|
||||
|
||||
SelectAccount:
|
||||
Title: Seleccionar cuenta
|
||||
Description: Utiliza tu cuenta ZITADEL
|
||||
TitleLinking: Selecciona tu cuenta para vincular el usuario
|
||||
DescriptionLinking: Selecciona tu cuenta para vincular con tu usuario externo.
|
||||
OtherUser: Otro Usuario
|
||||
SessionState0: activo
|
||||
SessionState1: inactivo
|
||||
MustBeMemberOfOrg: El usuario debe ser miembro de la organización {{.OrgName}}.
|
||||
|
||||
Password:
|
||||
Title: Contraseña
|
||||
Description: Introduce tus datos de inicio de sesión.
|
||||
PasswordLabel: Contraseña
|
||||
MinLength: Longitud mínima
|
||||
HasUppercase: Una letra mayúscula
|
||||
HasLowercase: Una letra minúscula
|
||||
HasNumber: Número
|
||||
HasSymbol: Símbolo
|
||||
Confirmation: Las contraseñas coinciden
|
||||
ResetLinkText: restablecer contraseña
|
||||
BackButtonText: atrás
|
||||
NextButtonText: siguiente
|
||||
|
||||
UsernameChange:
|
||||
Title: Cambiar nombre de usuario
|
||||
Description: Introduce tu nuevo nombre de usuario
|
||||
UsernameLabel: Nombre de usuario
|
||||
CancelButtonText: cancelar
|
||||
NextButtonText: siguiente
|
||||
|
||||
UsernameChangeDone:
|
||||
Title: Nombre de usuario cambiado
|
||||
Description: Tu nombre de usuario se cambió correctamente.
|
||||
NextButtonText: siguiente
|
||||
|
||||
InitPassword:
|
||||
Title: Establecer contraseña
|
||||
Description: Habrás recibido un código, que tendrás que introducir en el siguiente campo, para establecer tu nueva contraseña.
|
||||
CodeLabel: Código
|
||||
NewPasswordLabel: Nueva contraseña
|
||||
NewPasswordConfirmLabel: Confirmar contraseña
|
||||
ResendButtonText: reenviar código
|
||||
NextButtonText: siguiente
|
||||
|
||||
InitPasswordDone:
|
||||
Title: Contraseña establecida
|
||||
Description: La contraseña se estableció correctamente
|
||||
NextButtonText: siguiente
|
||||
CancelButtonText: cancelar
|
||||
|
||||
InitUser:
|
||||
Title: Activar usuario
|
||||
Description: Verifica tu email con el siguiente código y establece tu contraseña.
|
||||
CodeLabel: Código
|
||||
NewPasswordLabel: Nueva contraseña
|
||||
NewPasswordConfirm: Confirmar contraseña
|
||||
NextButtonText: siguiente
|
||||
ResendButtonText: reenviar código
|
||||
|
||||
InitUserDone:
|
||||
Title: Usuario activado
|
||||
Description: Email verificado y contraseña establecida correctamente
|
||||
NextButtonText: siguiente
|
||||
CancelButtonText: cancelar
|
||||
|
||||
InitMFAPrompt:
|
||||
Title: Configuración de doble factor
|
||||
Description: La autenticación de doble factor te proporciona seguridad adicional para tu cuenta de usuario. Ésta asegura que solo tú tienes acceso a tu cuenta.
|
||||
Provider0: App autenticadora (p.e Google/Microsoft Authenticator, Authy)
|
||||
Provider1: Dependiente de un dispositivo (p.e FaceID, Windows Hello, Huella dactilar)
|
||||
NextButtonText: siguiente
|
||||
SkipButtonText: saltar
|
||||
|
||||
InitMFAOTP:
|
||||
Title: Verificación de doble factor
|
||||
Description: Crea tu doble factor de autenticación. Descarga una aplicación autenticadora si todavía no tienes una.
|
||||
OTPDescription: Escanea el código con tu app autenticadora (p.e Google/Microsoft Authenticator, Authy) o copia el secreto e inserta el código generado más abajo.
|
||||
SecretLabel: Secreto
|
||||
CodeLabel: Código
|
||||
NextButtonText: siguiente
|
||||
CancelButtonText: cancelar
|
||||
|
||||
InitMFAU2F:
|
||||
Title: Añadir clave de seguridad
|
||||
Description: Una clave de seguridad es un método de verificación que puede integrarse en tu teléfono móvil, con Bluetooth, o conectándolo directamente en el puerto USB de tu ordenador.
|
||||
TokenNameLabel: Nombre de la clave de seguridad / dispositivo
|
||||
NotSupported: WebAuthN no está soportado por tu navegador. Por favor asegúrate de que está actualizado o utiliza uno diferente (p.e. Chrome, Safari, Firefox)
|
||||
RegisterTokenButtonText: Añadir clave de seguridad
|
||||
ErrorRetry: Reintentar, crear un nuevo challenge o elegir un método diferente.
|
||||
|
||||
InitMFADone:
|
||||
Title: Clave de seguridad verificada
|
||||
Description: ¡Genial! Acabas de configurar satisfactoriamente tu doble factor y has hecho que tu cuenta sea más segura. El doble factor tendrá que introducirse en cada inicio de sesión.
|
||||
NextButtonText: siguiente
|
||||
CancelButtonText: cancelar
|
||||
|
||||
MFAProvider:
|
||||
Provider0: App autenticadora (p.e Google/Microsoft Authenticator, Authy)
|
||||
Provider1: Dependiente de un dispositivo (p.e FaceID, Windows Hello, Huella dactilar)
|
||||
ChooseOther: o elige otra opción
|
||||
|
||||
VerifyMFAOTP:
|
||||
Title: Verificar doble factor
|
||||
Description: Verifica tu doble factor
|
||||
CodeLabel: Código
|
||||
NextButtonText: siguiente
|
||||
|
||||
VerifyMFAU2F:
|
||||
Title: Verificación de doble factor
|
||||
Description: Verifica tu doble factor de autenticación con el dispositivo registrado (p.e FaceID, Windows Hello, Huella dactilar)
|
||||
NotSupported: WebAuthN no está soportado por tu navegador. Por favor asegúrate de que está actualizado o utiliza uno diferente (p.e. Chrome, Safari, Firefox)
|
||||
ErrorRetry: Inténtalo nuevamente, crea una nueva petición o elige otro método.
|
||||
ValidateTokenButtonText: Verificar doble factor
|
||||
|
||||
Passwordless:
|
||||
Title: Inicio de sesión sin contraseña
|
||||
Description: Iniciar sesión con métodos de autenticación proporcionados por tu dispositivo como FaceID, Windows Hello o tu huella dactilar.
|
||||
NotSupported: WebAuthN no está soportado por tu navegador. Por favor asegúrate de que está actualizado o utiliza uno diferente (p.e. Chrome, Safari, Firefox)
|
||||
ErrorRetry: Inténtalo nuevamente, crea un nuevo reto (challenge) o elige un método diferente.
|
||||
LoginWithPwButtonText: Inicio de sesión con contraseña
|
||||
ValidateTokenButtonText: Inicio de sesión sin contraseña
|
||||
|
||||
PasswordlessPrompt:
|
||||
Title: Configuración de acceso sin contraseña
|
||||
Description: ¿Te gustaría configurar tu inicio de sesión sin contraseña? (métodos de autenticación de tu dispositivo como FaceID, Windows Hello o tu huella dactilar)
|
||||
DescriptionInit: Necesitas configurar tu inicio de sesión sin contraseña. Utiliza el enlace que se te ha proporcionado para registrar tu dispositivo.
|
||||
PasswordlessButtonText: Adelante con el inicio sin contraseñas
|
||||
NextButtonText: siguiente
|
||||
SkipButtonText: saltar
|
||||
|
||||
PasswordlessRegistration:
|
||||
Title: Configuración de acceso sin contraseña
|
||||
Description: Añade tu medio de autenticación proporcionando un nombre (p.e MyMobilePhone, MacBook, etc) y después haz clic en el botón 'Registrar acceso sin contraseña'.
|
||||
TokenNameLabel: Nombre del dispositivo
|
||||
NotSupported: WebAuthN no está soportado por tu navegador. Por favor asegúrate de que está actualizado o utiliza uno diferente (p.e. Chrome, Safari, Firefox)
|
||||
RegisterTokenButtonText: Registrar acceso sin contraseña
|
||||
ErrorRetry: Inténtalo nuevamente, crea un nuevo reto (challenge) o elige un método diferente.
|
||||
|
||||
PasswordlessRegistrationDone:
|
||||
Title: Configuración de acceso sin contraseña
|
||||
Description: Se añadió con éxito el dispositivo para iniciar sesión sin contraseña.
|
||||
DescriptionClose: Ya puedes cerrar esta ventana.
|
||||
NextButtonText: siguiente
|
||||
CancelButtonText: cancelar
|
||||
|
||||
PasswordChange:
|
||||
Title: Cambiar contraseña
|
||||
Description: Cambia tu contraseña. Introduce tu contraseña anterior y la nueva.
|
||||
OldPasswordLabel: Contraseña anterior
|
||||
NewPasswordLabel: Nueva contraseña
|
||||
NewPasswordConfirmLabel: Confirmación de contraseña
|
||||
CancelButtonText: cancelar
|
||||
NextButtonText: siguiente
|
||||
Footer: Pie
|
||||
|
||||
PasswordChangeDone:
|
||||
Title: Cambiar contraseña
|
||||
Description: Tu contraseña se cambió correctamente.
|
||||
NextButtonText: siguiente
|
||||
|
||||
PasswordResetDone:
|
||||
Title: Se ha enviado un enlace para restablecer la contraseña
|
||||
Description: Comprueba tu email para restablecer la contraseña.
|
||||
NextButtonText: siguiente
|
||||
|
||||
EmailVerification:
|
||||
Title: Verificación de email
|
||||
Description: Te hemos enviado un email para verificar tu dirección. Por favor introduce el código en el siguiente campo.
|
||||
CodeLabel: Código
|
||||
NextButtonText: siguiente
|
||||
ResendButtonText: reenviar código
|
||||
|
||||
EmailVerificationDone:
|
||||
Title: Verificación de email
|
||||
Description: Tu dirección de email se ha verificado correctamente.
|
||||
NextButtonText: siguiente
|
||||
CancelButtonText: cancelar
|
||||
LoginButtonText: iniciar sesión
|
||||
|
||||
RegisterOption:
|
||||
Title: Opciones de registro
|
||||
Description: Elige cómo te gustaría registrarte
|
||||
RegisterUsernamePasswordButtonText: Con nombre de usuario y contraseña
|
||||
ExternalLoginDescription: o regístrate con un usuario externo
|
||||
LoginButtonText: iniciar sesión
|
||||
|
||||
RegistrationUser:
|
||||
Title: Registro
|
||||
Description: Introduce tus datos de usuario. Tu email se utilizará como nombre de inicio de sesión.
|
||||
DescriptionOrgRegister: Introduce tus datos de usuario.
|
||||
EmailLabel: Email
|
||||
UsernameLabel: Nombre de usuario
|
||||
FirstnameLabel: Nombre
|
||||
LastnameLabel: Apellidos
|
||||
LanguageLabel: Idioma
|
||||
German: Deutsch
|
||||
English: English
|
||||
Italian: Italiano
|
||||
French: Français
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
GenderLabel: Género
|
||||
Female: Mujer
|
||||
Male: Hombre
|
||||
Diverse: Diverso / X
|
||||
PasswordLabel: Contraseña
|
||||
PasswordConfirmLabel: Confirmación de contraseña
|
||||
TosAndPrivacyLabel: Términos y condiciones
|
||||
TosConfirm: Acepto los
|
||||
TosLinkText: TDS
|
||||
PrivacyConfirm: Acepto la
|
||||
PrivacyLinkText: política de privacidad
|
||||
ExternalLogin: o regístrate con un usuario externo
|
||||
BackButtonText: inicio de sesión
|
||||
NextButtonText: siguiente
|
||||
|
||||
ExternalRegistrationUserOverview:
|
||||
Title: Registro de usuarios externos
|
||||
Description: Hemos tomado los detalles de tu usuario del proveedor seleccionado. Ahora puedes cambiarlos o completarlos.
|
||||
EmailLabel: Email
|
||||
UsernameLabel: Nombre de usuario
|
||||
FirstnameLabel: Nombre
|
||||
LastnameLabel: Apellidos
|
||||
NicknameLabel: Apodo
|
||||
PhoneLabel: Número de teléfono
|
||||
LanguageLabel: Idioma
|
||||
German: Deutsch
|
||||
English: English
|
||||
Italian: Italiano
|
||||
French: Français
|
||||
Chinese: 简体中文
|
||||
Japanese: 日本語
|
||||
Polish: Polski
|
||||
Spanish: Español
|
||||
TosAndPrivacyLabel: Términos y condiciones
|
||||
TosConfirm: Acepto los
|
||||
TosLinkText: TDS
|
||||
PrivacyConfirm: Acepto la
|
||||
PrivacyLinkText: política de privacidad
|
||||
ExternalLogin: o regístrate con un usuario externo
|
||||
BackButtonText: atrás
|
||||
NextButtonText: guardar
|
||||
|
||||
RegistrationOrg:
|
||||
Title: Registro de organización
|
||||
Description: Introduce el nombre de tu organización y tus datos de usuario.
|
||||
OrgNameLabel: Nombre de organización
|
||||
EmailLabel: Email
|
||||
UsernameLabel: Nombre de usuario
|
||||
FirstnameLabel: Nombre
|
||||
LastnameLabel: Apellidos
|
||||
PasswordLabel: Contraseña
|
||||
PasswordConfirmLabel: Confirmación de contraseña
|
||||
TosAndPrivacyLabel: Términos y condiciones
|
||||
TosConfirm: Acepto los
|
||||
TosLinkText: TDS
|
||||
PrivacyConfirm: Acepto la
|
||||
PrivacyLinkText: política de privacidad
|
||||
SaveButtonText: Crear organización
|
||||
|
||||
LoginSuccess:
|
||||
Title: Se inició sesión con éxito
|
||||
AutoRedirectDescription: Se te redirigirá a tu aplicación automáticamente. Si no fuera así, haz clic en el botón siguiente. Puedes cerrar esta ventana posteriormente.
|
||||
RedirectedDescription: Ya puedes cerrar esta ventana.
|
||||
NextButtonText: siguiente
|
||||
|
||||
LogoutDone:
|
||||
Title: Cerraste sesión
|
||||
Description: Cerraste la sesión con éxito.
|
||||
LoginButtonText: iniciar sesión
|
||||
|
||||
LinkingUsersDone:
|
||||
Title: Vinculación de usuario
|
||||
Description: usuario vinculado con éxito.
|
||||
CancelButtonText: cancelar
|
||||
NextButtonText: siguiente
|
||||
|
||||
ExternalNotFound:
|
||||
Title: Usuario externo no encontrado
|
||||
Description: Usuario externo no encontrado. ¿Quieres vincular tu usuario o autoregistrar uno nuevo?
|
||||
LinkButtonText: Vincular
|
||||
AutoRegisterButtonText: registrar
|
||||
TosAndPrivacyLabel: Términos y condiciones
|
||||
TosConfirm: Acepto los
|
||||
TosLinkText: TDS
|
||||
PrivacyConfirm: Acepto la
|
||||
PrivacyLinkText: política de privacidad
|
||||
German: Deutsch
|
||||
English: English
|
||||
Italian: Italiano
|
||||
French: Français
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
|
||||
Footer:
|
||||
PoweredBy: Powered By
|
||||
Tos: TDS
|
||||
PrivacyPolicy: Política de privacidad
|
||||
Help: Ayuda
|
||||
SupportEmail: Email de soporte
|
||||
|
||||
Errors:
|
||||
Internal: Se produjo un error interno
|
||||
AuthRequest:
|
||||
NotFound: No pude encontrar la petición de autenticación (authrequest)
|
||||
UserAgentNotCorresponding: El User Agent no se corresponde
|
||||
UserAgentNotFound: No se encontró el ID del User Agent
|
||||
TokenNotFound: No se encontró el Token
|
||||
RequestTypeNotSupported: El tipo de petición no está soportado
|
||||
MissingParameters: Faltan parámetros requeridos
|
||||
User:
|
||||
NotFound: El usuario no pudo ser encontrado
|
||||
AlreadyExists: El usuario ya existe
|
||||
Inactive: El usuario está inactivo
|
||||
NotFoundOnOrg: El usuario no pudo encontrarse en la organización elegida
|
||||
NotAllowedOrg: El usuario no es miembro de la organización requerida
|
||||
NotMatchingUserID: El usuario y el usuario contenido en la petición de autenticación (authrequest) no coinciden
|
||||
UserIDMissing: El ID de usuario está vacío
|
||||
Invalid: Datos de usuario no válidos
|
||||
DomainNotAllowedAsUsername: El dominio ya está reservado y no puede usarse
|
||||
NotAllowedToLink: El usuario no está autorizado para vincularse con un proveedor de inicio de sesión externo
|
||||
Profile:
|
||||
NotFound: Perfil no encontrado
|
||||
NotChanged: El perfil no ha cambiado
|
||||
Empty: El perfil está vacío
|
||||
FirstNameEmpty: El nombre del perfil está vacío
|
||||
LastNameEmpty: Los apellidos del perfil están vacíos
|
||||
IDMissing: Falta el ID del perfil
|
||||
Email:
|
||||
NotFound: Email no encontrado
|
||||
Invalid: El email no es válido
|
||||
AlreadyVerified: El email ya ha sido verificado
|
||||
NotChanged: El email no ha cambiado
|
||||
Empty: El email está vacío
|
||||
IDMissing: Falta el ID del email
|
||||
Phone:
|
||||
NotFound: Teléfono no encontrado
|
||||
Invalid: El teléfono no es válido
|
||||
AlreadyVerified: El teléfono ya ha sido verificado
|
||||
Empty: El teléfono está vacío
|
||||
NotChanged: El teléfono no ha cambiado
|
||||
Address:
|
||||
NotFound: Dirección no encontrada
|
||||
NotChanged: La dirección no cambió
|
||||
Username:
|
||||
AlreadyExists: El nombre de usuario ya está cogido
|
||||
Reserved: El nombre de usuario ya está cogido
|
||||
Empty: El nombre de usuario está vacío
|
||||
Password:
|
||||
ConfirmationWrong: La confirmación de la contraseña es incorrecta
|
||||
Empty: La contraseña está vacía
|
||||
Invalid: La contraseña no es válida
|
||||
InvalidAndLocked: La contraseña no es válida y el usuario está bloqueado, contacta con tu administrador.
|
||||
UsernameOrPassword:
|
||||
Invalid: El nombre de usuario o la contraseña no son válidos
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: No se encontró una política de contraseñas
|
||||
MinLength: La contraseña es demasiada corta
|
||||
HasLower: La contraseña debe contener una letra minúscula
|
||||
HasUpper: La contraseña debe contener una letra mayúscula
|
||||
HasNumber: La contraseña debe contener un número
|
||||
HasSymbol: La contraseña debe contener un símbolo
|
||||
Code:
|
||||
Expired: El código ha caducado
|
||||
Invalid: El código no es válido
|
||||
Empty: El código está vacío
|
||||
CryptoCodeNil: El código criptográfico es nulo
|
||||
NotFound: No pude encontrar el código
|
||||
GeneratorAlgNotSupported: Algoritmo de generación no soportado
|
||||
EmailVerify:
|
||||
UserIDEmpty: El ID de usuario está vacío
|
||||
ExternalData:
|
||||
CouldNotRead: Los datos externos no pudieron leerse correctamente
|
||||
MFA:
|
||||
NoProviders: No hay proveedores multifactor disponibles
|
||||
OTP:
|
||||
AlreadyReady: El multifactor OTP (OneTimePassword) ya está configurado
|
||||
NotExisting: El multifactor OTP (OneTimePassword) no existe
|
||||
InvalidCode: Código no válido
|
||||
NotReady: El multifactor OTP (OneTimePassword) no está listo
|
||||
Locked: El usuario está bloqueado
|
||||
SomethingWentWrong: Algo fue mal
|
||||
NotActive: El usuario no está activo
|
||||
ExternalIDP:
|
||||
IDPTypeNotImplemented: El tipo de IDP no está implementado
|
||||
NotAllowed: El proveedor de inicio de sesión externo no está permitido
|
||||
IDPConfigIDEmpty: El ID del proveedor de identidad está vacío
|
||||
ExternalUserIDEmpty: El ID de usuario externo está vacío
|
||||
UserDisplayNameEmpty: El nombre mostrado del usuario está vacío
|
||||
NoExternalUserData: No se recibieron datos del usuario externo
|
||||
CreationNotAllowed: La creación de un nuevo usuario no está permitida para este proveedor
|
||||
LinkingNotAllowed: La vinculación de un usuario no está permitida para este proveedor
|
||||
GrantRequired: El inicio de sesión no es posible. Se requiere que el usuario tenga al menos una concesión sobre la aplicación. Por favor contacta con tu administrador.
|
||||
ProjectRequired: El inicio de sesión no es posible. La organización del usuario debe tener el acceso concedido para el proyecto. Por favor contacta con tu administrador.
|
||||
IdentityProvider:
|
||||
InvalidConfig: La configuración del proveedor de identidades no es válida
|
||||
IAM:
|
||||
LockoutPolicy:
|
||||
NotExisting: No existe política de bloqueo
|
||||
Org:
|
||||
LoginPolicy:
|
||||
RegistrationNotAllowed: El registro no está permitido
|
||||
|
||||
optional: (opcional)
|
||||
@@ -221,6 +221,7 @@ RegistrationUser:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
GenderLabel: Genre
|
||||
Female: Femme
|
||||
Male: Homme
|
||||
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
TosAndPrivacyLabel: Termes et conditions
|
||||
TosConfirm: J'accepte les
|
||||
TosLinkText: TOS
|
||||
@@ -313,6 +315,25 @@ ExternalNotFound:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
|
||||
DeviceAuth:
|
||||
Title: Autorisation de l'appareil
|
||||
UserCode:
|
||||
Label: Code d'utilisateur
|
||||
Description: Saisissez le code utilisateur présenté sur l'appareil.
|
||||
ButtonNext: suivant
|
||||
Action:
|
||||
Description: Accordez l'accès à l'appareil.
|
||||
GrantDevice: vous êtes sur le point d'accorder un appareil
|
||||
AccessToScopes: accès aux périmètres suivants
|
||||
Button:
|
||||
Allow: permettre
|
||||
Deny: refuser
|
||||
Done:
|
||||
Description: Fait.
|
||||
Approved: Autorisation de l'appareil approuvée. Vous pouvez maintenant retourner à l'appareil.
|
||||
Denied: Autorisation de l'appareil refusée. Vous pouvez maintenant retourner à l'appareil.
|
||||
|
||||
Footer:
|
||||
PoweredBy: Promulgué par
|
||||
@@ -422,5 +443,7 @@ Errors:
|
||||
Org:
|
||||
LoginPolicy:
|
||||
RegistrationNotAllowed: L'enregistrement n'est pas autorisé
|
||||
DeviceAuth:
|
||||
NotExisting: Le code utilisateur n'existe pas
|
||||
|
||||
optional: (facultatif)
|
||||
|
||||
@@ -221,6 +221,7 @@ RegistrationUser:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
GenderLabel: Genere
|
||||
Female: Femminile
|
||||
Male: Maschile
|
||||
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
TosAndPrivacyLabel: Termini di servizio
|
||||
TosConfirm: Accetto i
|
||||
TosLinkText: Termini di servizio
|
||||
@@ -313,6 +315,25 @@ ExternalNotFound:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
|
||||
DeviceAuth:
|
||||
Title: Autorizzazione del dispositivo
|
||||
UserCode:
|
||||
Label: Codice utente
|
||||
Description: Inserire il codice utente presentato sul dispositivo.
|
||||
ButtonNext: prossimo
|
||||
Action:
|
||||
Description: Concedi l'accesso al dispositivo.
|
||||
GrantDevice: stai per concedere il dispositivo
|
||||
AccessToScopes: accesso ai seguenti ambiti
|
||||
Button:
|
||||
Allow: permettere
|
||||
Deny: negare
|
||||
Done:
|
||||
Description: Fatto.
|
||||
Approved: Autorizzazione del dispositivo approvata. Ora puoi tornare al dispositivo.
|
||||
Denied: Autorizzazione dispositivo negata. Ora puoi tornare al dispositivo.
|
||||
|
||||
Footer:
|
||||
PoweredBy: Alimentato da
|
||||
@@ -422,5 +443,7 @@ Errors:
|
||||
Org:
|
||||
LoginPolicy:
|
||||
RegistrationNotAllowed: la registrazione non è consentita.
|
||||
DeviceAuth:
|
||||
NotExisting: Il codice utente non esiste
|
||||
|
||||
optional: (opzionale)
|
||||
|
||||
@@ -213,6 +213,7 @@ RegistrationUser:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
GenderLabel: 性別
|
||||
Female: 女性
|
||||
Male: 男性
|
||||
@@ -245,6 +246,7 @@ ExternalRegistrationUserOverview:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
TosAndPrivacyLabel: 利用規約
|
||||
TosConfirm: 私は利用規約を承諾します。
|
||||
TosLinkText: TOS
|
||||
@@ -305,6 +307,25 @@ ExternalNotFound:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
|
||||
DeviceAuth:
|
||||
Title: デバイス認証
|
||||
UserCode:
|
||||
Label: ユーザーコード
|
||||
Description: デバイスに表示されたユーザー コードを入力します。
|
||||
ButtonNext: 次
|
||||
Action:
|
||||
Description: デバイスへのアクセスを許可します。
|
||||
GrantDevice: デバイスを許可しようとしています
|
||||
AccessToScopes: 次のスコープへのアクセス
|
||||
Button:
|
||||
Allow: 許可する
|
||||
Deny: 拒否
|
||||
Done:
|
||||
Description: 終わり。
|
||||
Approved: デバイス認証が承認されました。 これで、デバイスに戻ることができます。
|
||||
Denied: デバイス認証が拒否されました。 これで、デバイスに戻ることができます。
|
||||
|
||||
Footer:
|
||||
PoweredBy: Powered By
|
||||
@@ -382,5 +403,7 @@ Errors:
|
||||
IAM:
|
||||
LockoutPolicy:
|
||||
NotExisting: ロックアウトポリシーが存在しません
|
||||
DeviceAuth:
|
||||
NotExisting: ユーザーコードが存在しません
|
||||
|
||||
optional: "(オプション)"
|
||||
|
||||
@@ -221,6 +221,7 @@ RegistrationUser:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
GenderLabel: Płeć
|
||||
Female: Kobieta
|
||||
Male: Mężczyzna
|
||||
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
TosAndPrivacyLabel: Warunki i zasady
|
||||
TosConfirm: Akceptuję
|
||||
TosLinkText: Warunki korzystania
|
||||
@@ -313,6 +315,25 @@ ExternalNotFound:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
|
||||
DeviceAuth:
|
||||
Title: Autoryzacja urządzenia
|
||||
UserCode:
|
||||
Label: Kod użytkownika
|
||||
Description: Wprowadź kod użytkownika prezentowany na urządzeniu.
|
||||
ButtonNext: Następny
|
||||
Action:
|
||||
Description: Przyznaj dostęp do urządzenia.
|
||||
GrantDevice: zamierzasz przyznać urządzenie
|
||||
AccessToScopes: dostęp do następujących zakresów
|
||||
Button:
|
||||
Allow: umożliwić
|
||||
Deny: zaprzeczyć
|
||||
Done:
|
||||
Description: Zrobione.
|
||||
Approved: Zatwierdzono autoryzację urządzenia. Możesz teraz wrócić do urządzenia.
|
||||
Denied: Odmowa autoryzacji urządzenia. Możesz teraz wrócić do urządzenia.
|
||||
|
||||
Footer:
|
||||
PoweredBy: Obsługiwane przez
|
||||
@@ -422,5 +443,7 @@ Errors:
|
||||
Org:
|
||||
LoginPolicy:
|
||||
RegistrationNotAllowed: Rejestracja nie jest dozwolona
|
||||
DeviceAuth:
|
||||
NotExisting: Kod użytkownika nie istnieje
|
||||
|
||||
optional: (opcjonalny)
|
||||
|
||||
@@ -221,6 +221,7 @@ RegistrationUser:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
GenderLabel: 性别
|
||||
Female: 女性
|
||||
Male: 男性
|
||||
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
TosAndPrivacyLabel: 条款和条款
|
||||
TosConfirm: 我接受
|
||||
TosLinkText: 服务条款
|
||||
@@ -313,6 +315,25 @@ ExternalNotFound:
|
||||
Chinese: 简体中文
|
||||
Polish: Polski
|
||||
Japanese: 日本語
|
||||
Spanish: Español
|
||||
|
||||
DeviceAuth:
|
||||
Title: 设备授权
|
||||
UserCode:
|
||||
Label: 用户代码
|
||||
Description: 输入设备上显示的用户代码。
|
||||
ButtonNext: 下一个
|
||||
Action:
|
||||
Description: 授予设备访问权限。
|
||||
GrantDevice: 您即将授予设备
|
||||
AccessToScopes: 访问以下范围
|
||||
Button:
|
||||
Allow: 允许
|
||||
Deny: 否定
|
||||
Done:
|
||||
Description: 完毕。
|
||||
Approved: 设备授权已批准。 您现在可以返回设备。
|
||||
Denied: 设备授权被拒绝。 您现在可以返回设备。
|
||||
|
||||
Footer:
|
||||
PoweredBy: Powered By
|
||||
@@ -422,5 +443,7 @@ Errors:
|
||||
Org:
|
||||
LoginPolicy:
|
||||
RegistrationNotAllowed: 不允许注册
|
||||
DeviceAuth:
|
||||
NotExisting: 用户代码不存在
|
||||
|
||||
optional: (可选)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<h1>{{.Title}}</h1>
|
||||
<p>
|
||||
{{.Username}}, {{t "DeviceAuth.Action.GrantDevice"}} {{.ClientID}} {{t "DeviceAuth.Action.AccessToScopes"}}: {{.Scopes}}.
|
||||
</p>
|
||||
<form method="POST">
|
||||
{{ .CSRF }}
|
||||
<input type="hidden" name="authRequestID" value="{{.AuthRequestID}}">
|
||||
<button class="lgn-raised-button lgn-primary left" type="submit" formaction="./allowed">
|
||||
{{t "DeviceAuth.Action.Button.Allow"}}
|
||||
</button>
|
||||
<button class="lgn-raised-button lgn-warn right" type="submit" formaction="./denied">
|
||||
{{t "DeviceAuth.Action.Button.Deny"}}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
@@ -0,0 +1,21 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<h1>{{.Title}}</h1>
|
||||
<form method="POST">
|
||||
|
||||
{{ .CSRF }}
|
||||
|
||||
<div class="fields">
|
||||
<label class="lgn-label" for="user_code">{{t "DeviceAuth.UserCode.Label"}}</label>
|
||||
<input class="lgn-input" id="user_code" name="user_code" autofocus required{{if .ErrMessage}} shake{{end}}>
|
||||
</div>
|
||||
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="lgn-actions">
|
||||
<span class="fill-space"></span>
|
||||
<button id="submit-button" class="lgn-raised-button lgn-primary right" type="submit">{{t "DeviceAuth.UserCode.ButtonNext"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
@@ -64,6 +64,8 @@
|
||||
</option>
|
||||
<option value="en" id="en" {{if (selectedLanguage "en")}} selected {{end}}>{{t "ExternalNotFound.English"}}
|
||||
</option>
|
||||
<option value="es" id="es" {{if (selectedLanguage "es")}} selected {{end}}>{{t "ExternalNotFound.Spanish"}}
|
||||
</option>
|
||||
<option value="fr" id="fr" {{if (selectedLanguage "fr")}} selected {{end}}>{{t "ExternalNotFound.French"}}
|
||||
</option>
|
||||
<option value="it" id="it" {{if (selectedLanguage "it")}} selected {{end}}>{{t "ExternalNotFound.Italian"}}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<div class="lgn-head">
|
||||
<div class="lgn-actions">
|
||||
<i class="lgn-icon-check-solid lgn-primary"></i>
|
||||
<p class="lgn-error-message">
|
||||
{{ .Message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
@@ -1,3 +1,3 @@
|
||||
package statik
|
||||
|
||||
//go:generate statik -src=../static -dest=.. -ns=login
|
||||
//go:generate statik -f -src=../static -dest=.. -ns=login
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
user_model "github.com/zitadel/zitadel/internal/user/model"
|
||||
user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const unknownUserID = "UNKNOWN"
|
||||
@@ -64,7 +65,9 @@ type privacyPolicyProvider interface {
|
||||
type userSessionViewProvider interface {
|
||||
UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error)
|
||||
UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error)
|
||||
GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error)
|
||||
}
|
||||
|
||||
type userViewProvider interface {
|
||||
UserByID(string, string) (*user_view_model.UserView, error)
|
||||
}
|
||||
@@ -654,7 +657,7 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain
|
||||
preferredLoginName += "@" + request.RequestedPrimaryDomain
|
||||
}
|
||||
}
|
||||
user, err = repo.checkLoginNameInputForResourceOwner(request, preferredLoginName)
|
||||
user, err = repo.checkLoginNameInputForResourceOwner(ctx, request, preferredLoginName)
|
||||
} else {
|
||||
user, err = repo.checkLoginNameInput(ctx, request, preferredLoginName)
|
||||
}
|
||||
@@ -729,12 +732,12 @@ func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request *
|
||||
|
||||
func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) {
|
||||
// always check the loginname first
|
||||
user, err := repo.View.UserByLoginName(loginNameInput, request.InstanceID)
|
||||
user, err := repo.View.UserByLoginName(ctx, loginNameInput, request.InstanceID)
|
||||
if err == nil {
|
||||
// and take the user regardless if there would be a user with that email or phone
|
||||
return user, repo.checkLoginPolicyWithResourceOwner(ctx, request, user.ResourceOwner)
|
||||
}
|
||||
user, emailErr := repo.View.UserByEmail(loginNameInput, request.InstanceID)
|
||||
user, emailErr := repo.View.UserByEmail(ctx, loginNameInput, request.InstanceID)
|
||||
if emailErr == nil {
|
||||
// if there was a single user with the specified email
|
||||
// load and check the login policy
|
||||
@@ -747,7 +750,7 @@ func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *d
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
user, phoneErr := repo.View.UserByPhone(loginNameInput, request.InstanceID)
|
||||
user, phoneErr := repo.View.UserByPhone(ctx, loginNameInput, request.InstanceID)
|
||||
if phoneErr == nil {
|
||||
// if there was a single user with the specified phone
|
||||
// load and check the login policy
|
||||
@@ -765,9 +768,9 @@ func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *d
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) {
|
||||
func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(ctx context.Context, request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) {
|
||||
// always check the loginname first
|
||||
user, err := repo.View.UserByLoginNameAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID)
|
||||
user, err := repo.View.UserByLoginNameAndResourceOwner(ctx, loginNameInput, request.RequestedOrgID, request.InstanceID)
|
||||
if err == nil {
|
||||
// and take the user regardless if there would be a user with that email or phone
|
||||
return user, nil
|
||||
@@ -775,7 +778,7 @@ func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(request *domain
|
||||
if request.LoginPolicy != nil && !request.LoginPolicy.DisableLoginWithEmail {
|
||||
// if login by email is allowed and there was a single user with the specified email
|
||||
// take that user (and ignore possible phone number matches)
|
||||
user, emailErr := repo.View.UserByEmailAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID)
|
||||
user, emailErr := repo.View.UserByEmailAndResourceOwner(ctx, loginNameInput, request.RequestedOrgID, request.InstanceID)
|
||||
if emailErr == nil {
|
||||
return user, nil
|
||||
}
|
||||
@@ -783,7 +786,7 @@ func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(request *domain
|
||||
if request.LoginPolicy != nil && !request.LoginPolicy.DisableLoginWithPhone {
|
||||
// if login by phone is allowed and there was a single user with the specified phone
|
||||
// take that user
|
||||
user, phoneErr := repo.View.UserByPhoneAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID)
|
||||
user, phoneErr := repo.View.UserByPhoneAndResourceOwner(ctx, loginNameInput, request.RequestedOrgID, request.InstanceID)
|
||||
if phoneErr == nil {
|
||||
return user, nil
|
||||
}
|
||||
@@ -1298,12 +1301,20 @@ func userSessionsByUserAgentID(provider userSessionViewProvider, agentID, instan
|
||||
}
|
||||
|
||||
func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eventProvider userEventProvider, agentID string, user *user_model.UserView) (*user_model.UserSessionView, error) {
|
||||
session, err := provider.UserSessionByIDs(agentID, user.ID, authz.GetInstance(ctx).InstanceID())
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
session, err := provider.UserSessionByIDs(agentID, user.ID, instanceID)
|
||||
if err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
sequence, err := provider.GetLatestUserSessionSequence(ctx, instanceID)
|
||||
logging.WithFields("instanceID", instanceID, "userID", user.ID).
|
||||
OnError(err).
|
||||
Errorf("could not get current sequence for userSessionByIDs")
|
||||
session = &user_view_model.UserSessionView{UserAgentID: agentID, UserID: user.ID}
|
||||
if sequence != nil {
|
||||
session.Sequence = sequence.CurrentSequence
|
||||
}
|
||||
}
|
||||
events, err := eventProvider.UserEventsByID(ctx, user.ID, session.Sequence)
|
||||
if err != nil {
|
||||
@@ -1446,7 +1457,7 @@ func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*domain.ExternalUser,
|
||||
func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *user_model.UserView, userGrantProvider userGrantProvider) (_ bool, err error) {
|
||||
var project *query.Project
|
||||
switch request.Request.Type() {
|
||||
case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML:
|
||||
case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML, domain.AuthRequestTypeDevice:
|
||||
project, err = userGrantProvider.ProjectByClientID(ctx, request.ApplicationID, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -1467,13 +1478,13 @@ func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *u
|
||||
func projectRequired(ctx context.Context, request *domain.AuthRequest, projectProvider projectProvider) (missingGrant bool, err error) {
|
||||
var project *query.Project
|
||||
switch request.Request.Type() {
|
||||
case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML:
|
||||
case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML, domain.AuthRequestTypeDevice:
|
||||
project, err = projectProvider.ProjectByClientID(ctx, request.ApplicationID, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
default:
|
||||
return false, errors.ThrowPreconditionFailed(nil, "EVENT-dfrw2", "Errors.AuthRequest.RequestTypeNotSupported")
|
||||
return false, errors.ThrowPreconditionFailed(nil, "EVENT-ku4He", "Errors.AuthRequest.RequestTypeNotSupported")
|
||||
}
|
||||
// if the user and project are part of the same organisation we do not need to check if the project exists on that org
|
||||
if !project.HasProjectCheck || project.ResourceOwner == request.UserOrgID {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
user_model "github.com/zitadel/zitadel/internal/user/model"
|
||||
user_es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -35,6 +36,10 @@ func (m *mockViewNoUserSession) UserSessionsByAgentID(string, string) ([]*user_v
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockViewNoUserSession) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return &repository.CurrentSequence{}, nil
|
||||
}
|
||||
|
||||
type mockViewErrUserSession struct{}
|
||||
|
||||
func (m *mockViewErrUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) {
|
||||
@@ -45,6 +50,10 @@ func (m *mockViewErrUserSession) UserSessionsByAgentID(string, string) ([]*user_
|
||||
return nil, errors.ThrowInternal(nil, "id", "internal error")
|
||||
}
|
||||
|
||||
func (m *mockViewErrUserSession) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return &repository.CurrentSequence{}, nil
|
||||
}
|
||||
|
||||
type mockViewUserSession struct {
|
||||
ExternalLoginVerification time.Time
|
||||
PasswordlessVerification time.Time
|
||||
@@ -82,6 +91,10 @@ func (m *mockViewUserSession) UserSessionsByAgentID(string, string) ([]*user_vie
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (m *mockViewUserSession) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return &repository.CurrentSequence{}, nil
|
||||
}
|
||||
|
||||
type mockViewNoUser struct{}
|
||||
|
||||
func (m *mockViewNoUser) UserByID(string, string) (*user_view_model.UserView, error) {
|
||||
|
||||
@@ -42,15 +42,24 @@ func (r *RefreshTokenRepo) RefreshTokenByToken(ctx context.Context, refreshToken
|
||||
}
|
||||
|
||||
func (r *RefreshTokenRepo) RefreshTokenByID(ctx context.Context, tokenID, userID string) (*usr_model.RefreshTokenView, error) {
|
||||
tokenView, viewErr := r.View.RefreshTokenByID(tokenID, authz.GetInstance(ctx).InstanceID())
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
tokenView, viewErr := r.View.RefreshTokenByID(tokenID, instanceID)
|
||||
if viewErr != nil && !errors.IsNotFound(viewErr) {
|
||||
return nil, viewErr
|
||||
}
|
||||
if errors.IsNotFound(viewErr) {
|
||||
sequence, err := r.View.GetLatestRefreshTokenSequence(ctx, instanceID)
|
||||
logging.WithFields("instanceID", instanceID, "userID", userID, "tokenID", tokenID).
|
||||
OnError(err).
|
||||
Errorf("could not get current sequence for RefreshTokenByID")
|
||||
|
||||
tokenView = new(model.RefreshTokenView)
|
||||
tokenView.ID = tokenID
|
||||
tokenView.UserID = userID
|
||||
tokenView.InstanceID = authz.GetInstance(ctx).InstanceID()
|
||||
tokenView.InstanceID = instanceID
|
||||
if sequence != nil {
|
||||
tokenView.Sequence = sequence.CurrentSequence
|
||||
}
|
||||
}
|
||||
|
||||
events, esErr := r.getUserEvents(ctx, userID, tokenView.InstanceID, tokenView.Sequence)
|
||||
@@ -80,7 +89,7 @@ func (r *RefreshTokenRepo) SearchMyRefreshTokens(ctx context.Context, userID str
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sequence, err := r.View.GetLatestRefreshTokenSequence(authz.GetInstance(ctx).InstanceID())
|
||||
sequence, err := r.View.GetLatestRefreshTokenSequence(ctx, authz.GetInstance(ctx).InstanceID())
|
||||
logging.Log("EVENT-GBdn4").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Warn("could not read latest refresh token sequence")
|
||||
request.Queries = append(request.Queries, &usr_model.RefreshTokenSearchQuery{Key: usr_model.RefreshTokenSearchKeyUserID, Method: domain.SearchMethodEquals, Value: userID})
|
||||
tokens, count, err := r.View.SearchRefreshTokens(request)
|
||||
|
||||
@@ -34,15 +34,25 @@ func (repo *TokenRepo) IsTokenValid(ctx context.Context, userID, tokenID string)
|
||||
}
|
||||
|
||||
func (repo *TokenRepo) TokenByIDs(ctx context.Context, userID, tokenID string) (*usr_model.TokenView, error) {
|
||||
token, viewErr := repo.View.TokenByIDs(tokenID, userID, authz.GetInstance(ctx).InstanceID())
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
|
||||
token, viewErr := repo.View.TokenByIDs(tokenID, userID, instanceID)
|
||||
if viewErr != nil && !errors.IsNotFound(viewErr) {
|
||||
return nil, viewErr
|
||||
}
|
||||
if errors.IsNotFound(viewErr) {
|
||||
sequence, err := repo.View.GetLatestTokenSequence(ctx, instanceID)
|
||||
logging.WithFields("instanceID", instanceID, "userID", userID, "tokenID", tokenID).
|
||||
OnError(err).
|
||||
Errorf("could not get current sequence for TokenByIDs")
|
||||
|
||||
token = new(model.TokenView)
|
||||
token.ID = tokenID
|
||||
token.UserID = userID
|
||||
token.InstanceID = authz.GetInstance(ctx).InstanceID()
|
||||
token.InstanceID = instanceID
|
||||
if sequence != nil {
|
||||
token.Sequence = sequence.CurrentSequence
|
||||
}
|
||||
}
|
||||
|
||||
events, esErr := repo.getUserEvents(ctx, userID, token.InstanceID, token.Sequence)
|
||||
|
||||
@@ -62,16 +62,16 @@ func (t *RefreshToken) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{user.AggregateType, project.AggregateType, instance.AggregateType}
|
||||
}
|
||||
|
||||
func (t *RefreshToken) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := t.view.GetLatestRefreshTokenSequence(instanceID)
|
||||
func (t *RefreshToken) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) {
|
||||
sequence, err := t.view.GetLatestRefreshTokenSequence(ctx, instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (t *RefreshToken) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) {
|
||||
sequences, err := t.view.GetLatestRefreshTokenSequences(instanceIDs)
|
||||
func (t *RefreshToken) EventQuery(ctx context.Context, instanceIDs []string) (*es_models.SearchQuery, error) {
|
||||
sequences, err := t.view.GetLatestRefreshTokenSequences(ctx, instanceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -67,16 +67,16 @@ func (_ *Token) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{user.AggregateType, project.AggregateType, instance.AggregateType}
|
||||
}
|
||||
|
||||
func (t *Token) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := t.view.GetLatestTokenSequence(instanceID)
|
||||
func (t *Token) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) {
|
||||
sequence, err := t.view.GetLatestTokenSequence(ctx, instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (t *Token) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) {
|
||||
sequences, err := t.view.GetLatestTokenSequences(instanceIDs)
|
||||
func (t *Token) EventQuery(ctx context.Context, instanceIDs []string) (*es_models.SearchQuery, error) {
|
||||
sequences, err := t.view.GetLatestTokenSequences(ctx, instanceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -145,11 +145,13 @@ func (t *Token) Reduce(event *es_models.Event) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applicationsIDs := make([]string, 0, len(project.Applications))
|
||||
clientIDs := make([]string, 0, len(project.Applications))
|
||||
for _, app := range project.Applications {
|
||||
applicationsIDs = append(applicationsIDs, app.AppID)
|
||||
if app.OIDCConfig != nil {
|
||||
clientIDs = append(clientIDs, app.OIDCConfig.ClientID)
|
||||
}
|
||||
}
|
||||
return t.view.DeleteApplicationTokens(event, applicationsIDs...)
|
||||
return t.view.DeleteApplicationTokens(event, clientIDs...)
|
||||
case instance.InstanceRemovedEventType:
|
||||
return t.view.DeleteInstanceTokens(event)
|
||||
case org.OrgRemovedEventType:
|
||||
@@ -208,7 +210,7 @@ func (t *Token) OnSuccess(instanceIDs []string) error {
|
||||
}
|
||||
|
||||
func (t *Token) getProjectByID(ctx context.Context, projID, instanceID string) (*proj_model.Project, error) {
|
||||
query, err := proj_view.ProjectByIDQuery(projID, instanceID, 0)
|
||||
projectQuery, err := proj_view.ProjectByIDQuery(projID, instanceID, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -217,7 +219,7 @@ func (t *Token) getProjectByID(ctx context.Context, projID, instanceID string) (
|
||||
AggregateID: projID,
|
||||
},
|
||||
}
|
||||
err = es_sdk.Filter(ctx, t.Eventstore().FilterEvents, esProject.AppendEvents, query)
|
||||
err = es_sdk.Filter(ctx, t.Eventstore().FilterEvents, esProject.AppendEvents, projectQuery)
|
||||
if err != nil && !caos_errs.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -68,16 +68,16 @@ func (_ *User) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{user_repo.AggregateType, org.AggregateType, instance.AggregateType}
|
||||
}
|
||||
|
||||
func (u *User) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := u.view.GetLatestUserSequence(instanceID)
|
||||
func (u *User) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) {
|
||||
sequence, err := u.view.GetLatestUserSequence(ctx, instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (u *User) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) {
|
||||
sequences, err := u.view.GetLatestUserSequences(instanceIDs)
|
||||
func (u *User) EventQuery(ctx context.Context, instanceIDs []string) (*es_models.SearchQuery, error) {
|
||||
sequences, err := u.view.GetLatestUserSequences(ctx, instanceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -158,6 +158,11 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
logging.WithFields(
|
||||
"instance", event.InstanceID,
|
||||
"userID", event.AggregateID,
|
||||
"eventType", event.Type,
|
||||
).Info("user not found in view")
|
||||
query, err := usr_view.UserByIDQuery(event.AggregateID, event.InstanceID, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -181,6 +186,11 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
logging.WithFields(
|
||||
"instance", event.InstanceID,
|
||||
"userID", event.AggregateID,
|
||||
"eventType", event.Type,
|
||||
).Info("user not found in view")
|
||||
query, err := usr_view.UserByIDQuery(event.AggregateID, event.InstanceID, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -291,7 +301,7 @@ func (u *User) OnSuccess(instanceIDs []string) error {
|
||||
}
|
||||
|
||||
func (u *User) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_model.Org, error) {
|
||||
query, err := view.OrgByIDQuery(orgID, instanceID, 0)
|
||||
orgQuery, err := view.OrgByIDQuery(orgID, instanceID, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -301,7 +311,7 @@ func (u *User) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_m
|
||||
AggregateID: orgID,
|
||||
},
|
||||
}
|
||||
err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query)
|
||||
err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, orgQuery)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -65,16 +65,16 @@ func (_ *UserSession) AggregateTypes() []models.AggregateType {
|
||||
return []models.AggregateType{user.AggregateType, org.AggregateType, instance.AggregateType}
|
||||
}
|
||||
|
||||
func (u *UserSession) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := u.view.GetLatestUserSessionSequence(instanceID)
|
||||
func (u *UserSession) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) {
|
||||
sequence, err := u.view.GetLatestUserSessionSequence(ctx, instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (u *UserSession) EventQuery(instanceIDs []string) (*models.SearchQuery, error) {
|
||||
sequences, err := u.view.GetLatestUserSessionSequences(instanceIDs)
|
||||
func (u *UserSession) EventQuery(ctx context.Context, instanceIDs []string) (*models.SearchQuery, error) {
|
||||
sequences, err := u.view.GetLatestUserSessionSequences(ctx, instanceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -231,7 +231,7 @@ func (u *UserSession) loginNameInformation(ctx context.Context, orgID string, in
|
||||
}
|
||||
|
||||
func (u *UserSession) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_model.Org, error) {
|
||||
query, err := view.OrgByIDQuery(orgID, instanceID, 0)
|
||||
orgQuery, err := view.OrgByIDQuery(orgID, instanceID, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -241,7 +241,7 @@ func (u *UserSession) getOrgByID(ctx context.Context, orgID, instanceID string)
|
||||
AggregateID: orgID,
|
||||
},
|
||||
}
|
||||
err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query)
|
||||
err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, orgQuery)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ type EsRepository struct {
|
||||
eventstore.OrgRepository
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *database.DB, esV2 *eventstore2.Eventstore, oidcEncryption crypto.EncryptionAlgorithm, userEncryption crypto.EncryptionAlgorithm) (*EsRepository, error) {
|
||||
es, err := v1.Start(dbClient)
|
||||
func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *database.DB, esV2 *eventstore2.Eventstore, oidcEncryption crypto.EncryptionAlgorithm, userEncryption crypto.EncryptionAlgorithm, allowOrderByCreationDate bool) (*EsRepository, error) {
|
||||
es, err := v1.Start(dbClient, allowOrderByCreationDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
user_model "github.com/zitadel/zitadel/internal/user/model"
|
||||
@@ -81,12 +83,12 @@ func (v *View) DeleteOrgRefreshTokens(event *models.Event) error {
|
||||
return v.ProcessedRefreshTokenSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestRefreshTokenSequence(instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(refreshTokenTable, instanceID)
|
||||
func (v *View) GetLatestRefreshTokenSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(ctx, refreshTokenTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestRefreshTokenSequences(instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(refreshTokenTable, instanceIDs)
|
||||
func (v *View) GetLatestRefreshTokenSequences(ctx context.Context, instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(ctx, refreshTokenTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedRefreshTokenSequence(event *models.Event) error {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
@@ -15,12 +16,12 @@ func (v *View) saveCurrentSequence(viewName string, event *models.Event) error {
|
||||
return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate)
|
||||
}
|
||||
|
||||
func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID)
|
||||
func (v *View) latestSequence(ctx context.Context, viewName, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequence(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) latestSequences(viewName string, instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequences(v.Db, sequencesTable, viewName, instanceIDs)
|
||||
func (v *View) latestSequences(ctx context.Context, viewName string, instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequences(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) updateSpoolerRunSequence(viewName string, instanceIDs []string) error {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
usr_view "github.com/zitadel/zitadel/internal/user/repository/view"
|
||||
@@ -92,12 +94,12 @@ func (v *View) DeleteOrgTokens(event *models.Event) error {
|
||||
return v.ProcessedTokenSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestTokenSequence(instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(tokenTable, instanceID)
|
||||
func (v *View) GetLatestTokenSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(ctx, tokenTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestTokenSequences(instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(tokenTable, instanceIDs)
|
||||
func (v *View) GetLatestTokenSequences(ctx context.Context, instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(ctx, tokenTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedTokenSequence(event *models.Event) error {
|
||||
|
||||
@@ -3,7 +3,8 @@ package view
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
@@ -21,16 +22,16 @@ func (v *View) UserByID(userID, instanceID string) (*model.UserView, error) {
|
||||
return view.UserByID(v.Db, userTable, userID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) UserByLoginName(loginName, instanceID string) (*model.UserView, error) {
|
||||
func (v *View) UserByLoginName(ctx context.Context, loginName, instanceID string) (*model.UserView, error) {
|
||||
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.userByID(instanceID, loginNameQuery)
|
||||
return v.userByID(ctx, instanceID, loginNameQuery)
|
||||
}
|
||||
|
||||
func (v *View) UserByLoginNameAndResourceOwner(loginName, resourceOwner, instanceID string) (*model.UserView, error) {
|
||||
func (v *View) UserByLoginNameAndResourceOwner(ctx context.Context, loginName, resourceOwner, instanceID string) (*model.UserView, error) {
|
||||
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -40,18 +41,18 @@ func (v *View) UserByLoginNameAndResourceOwner(loginName, resourceOwner, instanc
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.userByID(instanceID, loginNameQuery, resourceOwnerQuery)
|
||||
return v.userByID(ctx, instanceID, loginNameQuery, resourceOwnerQuery)
|
||||
}
|
||||
|
||||
func (v *View) UserByEmail(email, instanceID string) (*model.UserView, error) {
|
||||
func (v *View) UserByEmail(ctx context.Context, email, instanceID string) (*model.UserView, error) {
|
||||
emailQuery, err := query.NewUserVerifiedEmailSearchQuery(email, query.TextEqualsIgnoreCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.userByID(instanceID, emailQuery)
|
||||
return v.userByID(ctx, instanceID, emailQuery)
|
||||
}
|
||||
|
||||
func (v *View) UserByEmailAndResourceOwner(email, resourceOwner, instanceID string) (*model.UserView, error) {
|
||||
func (v *View) UserByEmailAndResourceOwner(ctx context.Context, email, resourceOwner, instanceID string) (*model.UserView, error) {
|
||||
emailQuery, err := query.NewUserVerifiedEmailSearchQuery(email, query.TextEquals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -61,18 +62,18 @@ func (v *View) UserByEmailAndResourceOwner(email, resourceOwner, instanceID stri
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.userByID(instanceID, emailQuery, resourceOwnerQuery)
|
||||
return v.userByID(ctx, instanceID, emailQuery, resourceOwnerQuery)
|
||||
}
|
||||
|
||||
func (v *View) UserByPhone(phone, instanceID string) (*model.UserView, error) {
|
||||
func (v *View) UserByPhone(ctx context.Context, phone, instanceID string) (*model.UserView, error) {
|
||||
phoneQuery, err := query.NewUserVerifiedPhoneSearchQuery(phone, query.TextEquals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.userByID(instanceID, phoneQuery)
|
||||
return v.userByID(ctx, instanceID, phoneQuery)
|
||||
}
|
||||
|
||||
func (v *View) UserByPhoneAndResourceOwner(phone, resourceOwner, instanceID string) (*model.UserView, error) {
|
||||
func (v *View) UserByPhoneAndResourceOwner(ctx context.Context, phone, resourceOwner, instanceID string) (*model.UserView, error) {
|
||||
phoneQuery, err := query.NewUserVerifiedPhoneSearchQuery(phone, query.TextEquals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,12 +83,10 @@ func (v *View) UserByPhoneAndResourceOwner(phone, resourceOwner, instanceID stri
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.userByID(instanceID, phoneQuery, resourceOwnerQuery)
|
||||
return v.userByID(ctx, instanceID, phoneQuery, resourceOwnerQuery)
|
||||
}
|
||||
|
||||
func (v *View) userByID(instanceID string, queries ...query.SearchQuery) (*model.UserView, error) {
|
||||
ctx := authz.WithInstanceID(context.Background(), instanceID)
|
||||
|
||||
func (v *View) userByID(ctx context.Context, instanceID string, queries ...query.SearchQuery) (*model.UserView, error) {
|
||||
queriedUser, err := v.query.GetNotifyUser(ctx, true, false, queries...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -99,7 +98,14 @@ func (v *View) userByID(instanceID string, queries ...query.SearchQuery) (*model
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sequence, err := v.GetLatestUserSequence(ctx, instanceID)
|
||||
logging.WithFields("instanceID", instanceID).
|
||||
OnError(err).
|
||||
Errorf("could not get current sequence for userByID")
|
||||
user = new(model.UserView)
|
||||
if sequence != nil {
|
||||
user.Sequence = sequence.CurrentSequence
|
||||
}
|
||||
}
|
||||
|
||||
query, err := view.UserByIDQuery(queriedUser.ID, instanceID, user.Sequence)
|
||||
@@ -188,12 +194,12 @@ func (v *View) UpdateOrgOwnerRemovedUsers(event *models.Event) error {
|
||||
return v.ProcessedUserSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestUserSequence(instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(userTable, instanceID)
|
||||
func (v *View) GetLatestUserSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(ctx, userTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestUserSequences(instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(userTable, instanceIDs)
|
||||
func (v *View) GetLatestUserSequences(ctx context.Context, instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(ctx, userTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedUserSequence(event *models.Event) error {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view"
|
||||
@@ -72,12 +74,12 @@ func (v *View) DeleteOrgUserSessions(event *models.Event) error {
|
||||
return v.ProcessedUserSessionSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestUserSessionSequence(instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(userSessionTable, instanceID)
|
||||
func (v *View) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(ctx, userSessionTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestUserSessionSequences(instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(userSessionTable, instanceIDs)
|
||||
func (v *View) GetLatestUserSessionSequences(ctx context.Context, instanceIDs []string) ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(ctx, userSessionTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedUserSessionSequence(event *models.Event) error {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
eventstore "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
@@ -16,6 +19,7 @@ type View struct {
|
||||
idGenerator id.Generator
|
||||
query *query.Queries
|
||||
es eventstore.Eventstore
|
||||
client *database.DB
|
||||
}
|
||||
|
||||
func StartView(sqlClient *database.DB, keyAlgorithm crypto.EncryptionAlgorithm, queries *query.Queries, idGenerator id.Generator, es eventstore.Eventstore) (*View, error) {
|
||||
@@ -29,9 +33,14 @@ func StartView(sqlClient *database.DB, keyAlgorithm crypto.EncryptionAlgorithm,
|
||||
idGenerator: idGenerator,
|
||||
query: queries,
|
||||
es: es,
|
||||
client: sqlClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *View) Health() (err error) {
|
||||
return v.Db.DB().Ping()
|
||||
}
|
||||
|
||||
func (v *View) TimeTravel(ctx context.Context, tableName string) string {
|
||||
return tableName + v.client.Timetravel(call.Took(ctx))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func Start(queries *query.Queries, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure bool) (repository.Repository, error) {
|
||||
return eventsourcing.Start(queries, dbClient, keyEncryptionAlgorithm, externalSecure)
|
||||
func Start(queries *query.Queries, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure, allowOrderByCreationDate bool) (repository.Repository, error) {
|
||||
return eventsourcing.Start(queries, dbClient, keyEncryptionAlgorithm, externalSecure, allowOrderByCreationDate)
|
||||
}
|
||||
|
||||
@@ -44,16 +44,16 @@ func (repo *TokenVerifierRepo) tokenByID(ctx context.Context, tokenID, userID st
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
sequence, err := repo.View.GetLatestTokenSequence(instanceID)
|
||||
logging.WithFields("instanceID", instanceID, "userID", userID, "tokenID").
|
||||
OnError(err).
|
||||
Errorf("could not get current sequence for token check")
|
||||
|
||||
token, viewErr := repo.View.TokenByIDs(tokenID, userID, instanceID)
|
||||
if viewErr != nil && !caos_errs.IsNotFound(viewErr) {
|
||||
return nil, viewErr
|
||||
}
|
||||
if caos_errs.IsNotFound(viewErr) {
|
||||
sequence, err := repo.View.GetLatestTokenSequence(ctx, instanceID)
|
||||
logging.WithFields("instanceID", instanceID, "userID", userID, "tokenID", tokenID).
|
||||
OnError(err).
|
||||
Errorf("could not get current sequence for token check")
|
||||
|
||||
token = new(model.TokenView)
|
||||
token.ID = tokenID
|
||||
token.UserID = userID
|
||||
|
||||
@@ -12,17 +12,17 @@ type UserMembershipRepo struct {
|
||||
Queries *query.Queries
|
||||
}
|
||||
|
||||
func (repo *UserMembershipRepo) SearchMyMemberships(ctx context.Context) (_ []*authz.Membership, err error) {
|
||||
func (repo *UserMembershipRepo) SearchMyMemberships(ctx context.Context, orgID string) (_ []*authz.Membership, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
memberships, err := repo.searchUserMemberships(ctx)
|
||||
memberships, err := repo.searchUserMemberships(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return userMembershipsToMemberships(memberships), nil
|
||||
}
|
||||
|
||||
func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context) (_ []*query.Membership, err error) {
|
||||
func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context, orgID string) (_ []*query.Membership, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
@@ -30,11 +30,11 @@ func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context) (_ []
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orgIDsQuery, err := query.NewMembershipResourceOwnersSearchQuery(ctxData.OrgID, authz.GetInstance(ctx).InstanceID())
|
||||
orgIDsQuery, err := query.NewMembershipResourceOwnersSearchQuery(orgID, authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantedIDQuery, err := query.NewMembershipGrantedOrgIDSearchQuery(ctxData.OrgID)
|
||||
grantedIDQuery, err := query.NewMembershipGrantedOrgIDSearchQuery(orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ type EsRepository struct {
|
||||
eventstore.TokenVerifierRepo
|
||||
}
|
||||
|
||||
func Start(queries *query.Queries, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure bool) (repository.Repository, error) {
|
||||
es, err := v1.Start(dbClient)
|
||||
func Start(queries *query.Queries, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure, allowOrderByCreationDate bool) (repository.Repository, error) {
|
||||
es, err := v1.Start(dbClient, allowOrderByCreationDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
@@ -13,6 +15,6 @@ func (v *View) saveCurrentSequence(viewName string, event *models.Event) error {
|
||||
return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate)
|
||||
}
|
||||
|
||||
func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID)
|
||||
func (v *View) latestSequence(ctx context.Context, viewName, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequence(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceID)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
usr_view "github.com/zitadel/zitadel/internal/user/repository/view"
|
||||
@@ -40,8 +42,8 @@ func (v *View) DeleteSessionTokens(agentID, userID, instanceID string, event *mo
|
||||
return v.ProcessedTokenSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestTokenSequence(instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(tokenTable, instanceID)
|
||||
func (v *View) GetLatestTokenSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(ctx, tokenTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedTokenSequence(event *models.Event) error {
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
type View struct {
|
||||
Db *gorm.DB
|
||||
Query *query.Queries
|
||||
idGenerator id.Generator
|
||||
client *database.DB
|
||||
}
|
||||
|
||||
func StartView(sqlClient *database.DB, idGenerator id.Generator, queries *query.Queries) (*View, error) {
|
||||
@@ -23,9 +27,14 @@ func StartView(sqlClient *database.DB, idGenerator id.Generator, queries *query.
|
||||
Db: gorm,
|
||||
idGenerator: idGenerator,
|
||||
Query: queries,
|
||||
client: sqlClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *View) Health() (err error) {
|
||||
return v.Db.DB().Ping()
|
||||
}
|
||||
|
||||
func (v *View) TimeTravel(ctx context.Context, tableName string) string {
|
||||
return tableName + v.client.Timetravel(call.Took(ctx))
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ import (
|
||||
)
|
||||
|
||||
type UserMembershipRepository interface {
|
||||
SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error)
|
||||
SearchMyMemberships(ctx context.Context, orgID string) ([]*authz.Membership, error)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import (
|
||||
type Commands struct {
|
||||
httpClient *http.Client
|
||||
|
||||
checkPermission permissionCheck
|
||||
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||
|
||||
eventstore *eventstore.Eventstore
|
||||
static static.Storage
|
||||
idGenerator id.Generator
|
||||
@@ -59,7 +62,8 @@ type Commands struct {
|
||||
certificateLifetime time.Duration
|
||||
}
|
||||
|
||||
func StartCommands(es *eventstore.Eventstore,
|
||||
func StartCommands(
|
||||
es *eventstore.Eventstore,
|
||||
defaults sd.SystemDefaults,
|
||||
zitadelRoles []authz.RoleMapping,
|
||||
staticStore static.Storage,
|
||||
@@ -76,6 +80,7 @@ func StartCommands(es *eventstore.Eventstore,
|
||||
oidcEncryption,
|
||||
samlEncryption crypto.EncryptionAlgorithm,
|
||||
httpClient *http.Client,
|
||||
membershipsResolver authz.MembershipsResolver,
|
||||
) (repo *Commands, err error) {
|
||||
if externalDomain == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
||||
@@ -102,6 +107,10 @@ func StartCommands(es *eventstore.Eventstore,
|
||||
certificateAlgorithm: samlEncryption,
|
||||
webauthnConfig: webAuthN,
|
||||
httpClient: httpClient,
|
||||
checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||
return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf)
|
||||
},
|
||||
newEmailCode: newEmailCode,
|
||||
}
|
||||
|
||||
instance_repo.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
@@ -10,24 +10,33 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
||||
type CryptoCodeWithExpiry struct {
|
||||
Crypted *crypto.CryptoValue
|
||||
Plain string
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) {
|
||||
config, err := secretGeneratorConfig(ctx, filter, typ)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code := new(CryptoCodeWithExpiry)
|
||||
switch a := alg.(type) {
|
||||
case crypto.HashAlgorithm:
|
||||
value, _, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
|
||||
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
|
||||
case crypto.EncryptionAlgorithm:
|
||||
value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
|
||||
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
|
||||
default:
|
||||
return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
|
||||
return nil, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
return nil, err
|
||||
}
|
||||
return value, config.Expiry, nil
|
||||
|
||||
code.Expiry = config.Expiry
|
||||
return code, nil
|
||||
}
|
||||
|
||||
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
)
|
||||
|
||||
func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) (string, *domain.ObjectDetails, error) {
|
||||
aggrID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
aggr := deviceauth.NewAggregate(aggrID, authz.GetInstance(ctx).InstanceID())
|
||||
model := NewDeviceAuthWriteModel(aggrID, aggr.ResourceOwner)
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
aggr,
|
||||
clientID,
|
||||
deviceCode,
|
||||
userCode,
|
||||
expires,
|
||||
scopes,
|
||||
))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
err = AppendAndReduce(model, pushedEvents...)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return model.AggregateID, writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ApproveDeviceAuth(ctx context.Context, id, subject string) (*domain.ObjectDetails, error) {
|
||||
model, err := c.getDeviceAuthWriteModelByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !model.State.Exists() {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound")
|
||||
}
|
||||
aggr := deviceauth.NewAggregate(model.AggregateID, model.InstanceID)
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, aggr, subject))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(model, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domain.DeviceAuthCanceled) (*domain.ObjectDetails, error) {
|
||||
model, err := c.getDeviceAuthWriteModelByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !model.State.Exists() {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-gee5A", "Errors.DeviceAuth.NotFound")
|
||||
}
|
||||
aggr := deviceauth.NewAggregate(model.AggregateID, model.InstanceID)
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewCanceledEvent(ctx, aggr, reason))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(model, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveDeviceAuth(ctx context.Context, id string) (*domain.ObjectDetails, error) {
|
||||
model, err := c.getDeviceAuthWriteModelByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggr := deviceauth.NewAggregate(model.AggregateID, model.InstanceID)
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewRemovedEvent(ctx, aggr, model.ClientID, model.DeviceCode, model.UserCode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(model, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) getDeviceAuthWriteModelByID(ctx context.Context, id string) (*DeviceAuthWriteModel, error) {
|
||||
model := &DeviceAuthWriteModel{WriteModel: eventstore.WriteModel{AggregateID: id}}
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
)
|
||||
|
||||
type DeviceAuthWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
ClientID string
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Expires time.Time
|
||||
Scopes []string
|
||||
Subject string
|
||||
State domain.DeviceAuthState
|
||||
}
|
||||
|
||||
func NewDeviceAuthWriteModel(aggrID, resourceOwner string) *DeviceAuthWriteModel {
|
||||
return &DeviceAuthWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: aggrID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DeviceAuthWriteModel) Reduce() error {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *deviceauth.AddedEvent:
|
||||
m.ClientID = e.ClientID
|
||||
m.DeviceCode = e.DeviceCode
|
||||
m.UserCode = e.UserCode
|
||||
m.Expires = e.Expires
|
||||
m.Scopes = e.Scopes
|
||||
m.State = e.State
|
||||
case *deviceauth.ApprovedEvent:
|
||||
m.Subject = e.Subject
|
||||
m.State = domain.DeviceAuthStateApproved
|
||||
case *deviceauth.CanceledEvent:
|
||||
m.State = e.Reason.State()
|
||||
case *deviceauth.RemovedEvent:
|
||||
m.State = domain.DeviceAuthStateRemoved
|
||||
}
|
||||
}
|
||||
|
||||
return m.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *DeviceAuthWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(deviceauth.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
Builder()
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
id_mock "github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
)
|
||||
|
||||
func TestCommands_AddDeviceAuth(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
idErr := errors.New("idErr")
|
||||
pushErr := errors.New("pushErr")
|
||||
now := time.Now()
|
||||
|
||||
unique := deviceauth.NewAddUniqueConstraints("client_id", "123", "456")
|
||||
require.Len(t, unique, 2)
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
clientID string
|
||||
deviceCode string
|
||||
userCode string
|
||||
expires time.Time
|
||||
scopes []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantID string
|
||||
wantDetails *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "idGenerator error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: func() id.Generator {
|
||||
m := id_mock.NewMockGenerator(gomock.NewController(t))
|
||||
m.EXPECT().Next().Return("", idErr)
|
||||
return m
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
clientID: "client_id",
|
||||
deviceCode: "123",
|
||||
userCode: "456",
|
||||
expires: now,
|
||||
scopes: []string{"a", "b", "c"},
|
||||
},
|
||||
wantErr: idErr,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t, expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("instance1", deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
)),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]),
|
||||
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]),
|
||||
)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "1999"),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
clientID: "client_id",
|
||||
deviceCode: "123",
|
||||
userCode: "456",
|
||||
expires: now,
|
||||
scopes: []string{"a", "b", "c"},
|
||||
},
|
||||
wantID: "1999",
|
||||
wantDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t, expectPushFailed(pushErr,
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("instance1", deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
)),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]),
|
||||
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]),
|
||||
)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "1999"),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
clientID: "client_id",
|
||||
deviceCode: "123",
|
||||
userCode: "456",
|
||||
expires: now,
|
||||
scopes: []string{"a", "b", "c"},
|
||||
},
|
||||
wantErr: pushErr,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
gotID, gotDetails, err := c.AddDeviceAuth(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, tt.args.userCode, tt.args.expires, tt.args.scopes)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, gotID, tt.wantID)
|
||||
assert.Equal(t, gotDetails, tt.wantDetails)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ApproveDeviceAuth(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
now := time.Now()
|
||||
pushErr := errors.New("pushErr")
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
id string
|
||||
subject string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantDetails *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "not found error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID("instance1",
|
||||
deviceauth.NewRemovedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999", "subj"},
|
||||
wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
)),
|
||||
expectPushFailed(pushErr,
|
||||
[]*repository.Event{eventFromEventPusherWithInstanceID(
|
||||
"instance1", deviceauth.NewApprovedEvent(
|
||||
ctx, deviceauth.NewAggregate("1999", "instance1"), "subj",
|
||||
),
|
||||
)},
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999", "subj"},
|
||||
wantErr: pushErr,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
)),
|
||||
expectPush([]*repository.Event{eventFromEventPusherWithInstanceID(
|
||||
"instance1", deviceauth.NewApprovedEvent(
|
||||
ctx, deviceauth.NewAggregate("1999", "instance1"), "subj",
|
||||
),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999", "subj"},
|
||||
wantDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
}
|
||||
gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.subject)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, gotDetails, tt.wantDetails)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_CancelDeviceAuth(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
now := time.Now()
|
||||
pushErr := errors.New("pushErr")
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
id string
|
||||
reason domain.DeviceAuthCanceled
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantDetails *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "not found error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID("instance1",
|
||||
deviceauth.NewRemovedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999", domain.DeviceAuthCanceledDenied},
|
||||
wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-gee5A", "Errors.DeviceAuth.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
)),
|
||||
expectPushFailed(pushErr,
|
||||
[]*repository.Event{eventFromEventPusherWithInstanceID(
|
||||
"instance1", deviceauth.NewCanceledEvent(
|
||||
ctx, deviceauth.NewAggregate("1999", "instance1"),
|
||||
domain.DeviceAuthCanceledDenied,
|
||||
),
|
||||
)},
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999", domain.DeviceAuthCanceledDenied},
|
||||
wantErr: pushErr,
|
||||
},
|
||||
{
|
||||
name: "success/denied",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
)),
|
||||
expectPush([]*repository.Event{eventFromEventPusherWithInstanceID(
|
||||
"instance1", deviceauth.NewCanceledEvent(
|
||||
ctx, deviceauth.NewAggregate("1999", "instance1"),
|
||||
domain.DeviceAuthCanceledDenied,
|
||||
),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999", domain.DeviceAuthCanceledDenied},
|
||||
wantDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success/expired",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
)),
|
||||
expectPush([]*repository.Event{eventFromEventPusherWithInstanceID(
|
||||
"instance1", deviceauth.NewCanceledEvent(
|
||||
ctx, deviceauth.NewAggregate("1999", "instance1"),
|
||||
domain.DeviceAuthCanceledExpired,
|
||||
),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999", domain.DeviceAuthCanceledExpired},
|
||||
wantDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
}
|
||||
gotDetails, err := c.CancelDeviceAuth(tt.args.ctx, tt.args.id, tt.args.reason)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, gotDetails, tt.wantDetails)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_RemoveDeviceAuth(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
now := time.Now()
|
||||
pushErr := errors.New("pushErr")
|
||||
|
||||
unique := deviceauth.NewRemoveUniqueConstraints("client_id", "123", "456")
|
||||
require.Len(t, unique, 2)
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
id string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantDetails *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
)),
|
||||
expectPushFailed(pushErr,
|
||||
[]*repository.Event{eventFromEventPusherWithInstanceID(
|
||||
"instance1", deviceauth.NewRemovedEvent(
|
||||
ctx, deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456",
|
||||
),
|
||||
)},
|
||||
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]),
|
||||
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999"},
|
||||
wantErr: pushErr,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
),
|
||||
)),
|
||||
expectPush(
|
||||
[]*repository.Event{eventFromEventPusherWithInstanceID(
|
||||
"instance1", deviceauth.NewRemovedEvent(
|
||||
ctx, deviceauth.NewAggregate("1999", "instance1"),
|
||||
"client_id", "123", "456",
|
||||
),
|
||||
)},
|
||||
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]),
|
||||
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{ctx, "1999"},
|
||||
wantDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
}
|
||||
gotDetails, err := c.RemoveDeviceAuth(tt.args.ctx, tt.args.id)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, gotDetails, tt.wantDetails)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
@@ -12,12 +11,18 @@ import (
|
||||
type Email struct {
|
||||
Address domain.EmailAddress
|
||||
Verified bool
|
||||
|
||||
// ReturnCode is used if the Verified field is false
|
||||
ReturnCode bool
|
||||
|
||||
// URLTemplate can be used to specify a custom link to be sent in the mail verification
|
||||
URLTemplate string
|
||||
}
|
||||
|
||||
func (e *Email) Validate() error {
|
||||
return e.Address.Validate()
|
||||
}
|
||||
|
||||
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
||||
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
|
||||
}
|
||||
|
||||
@@ -333,8 +333,9 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize))
|
||||
}
|
||||
} else if setup.Org.Human != nil {
|
||||
setup.Org.Human.ID = userID
|
||||
validations = append(validations,
|
||||
AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption),
|
||||
c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordAlg, c.userEncryption, true),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
@@ -248,3 +249,15 @@ func (m *mockInstance) RequestedHost() string {
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMockPermissionCheckAllowed() permissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func newMockPermissionCheckNotAllowed() permissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||
return errors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")
|
||||
}
|
||||
}
|
||||
|
||||
+26
-41
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
user_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
type OrgSetup struct {
|
||||
@@ -22,22 +22,13 @@ type OrgSetup struct {
|
||||
Roles []string
|
||||
}
|
||||
|
||||
func (c *Commands) SetUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, *domain.ObjectDetails, error) {
|
||||
existingOrg, err := c.getOrgWriteModelByID(ctx, orgID)
|
||||
func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, userIDs ...string) (userID string, token string, machineKey *MachineKey, details *domain.ObjectDetails, err error) {
|
||||
userID, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return "", "", nil, nil, err
|
||||
}
|
||||
if existingOrg != nil {
|
||||
return "", nil, errors.ThrowPreconditionFailed(nil, "COMMAND-poaj2", "Errors.Org.AlreadyExisting")
|
||||
}
|
||||
|
||||
userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...)
|
||||
return userID, details, err
|
||||
}
|
||||
|
||||
func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, string, *MachineKey, *domain.ObjectDetails, error) {
|
||||
orgAgg := org.NewAggregate(orgID)
|
||||
userAgg := user_repo.NewAggregate(userID, orgID)
|
||||
userAgg := user.NewAggregate(userID, orgID)
|
||||
|
||||
roles := []string{domain.RoleOrgOwner}
|
||||
if len(o.Roles) > 0 {
|
||||
@@ -49,9 +40,9 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user
|
||||
}
|
||||
|
||||
var pat *PersonalAccessToken
|
||||
var machineKey *MachineKey
|
||||
if o.Human != nil {
|
||||
validations = append(validations, AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption))
|
||||
o.Human.ID = userID
|
||||
validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordAlg, c.userEncryption, true))
|
||||
} else if o.Machine != nil {
|
||||
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
|
||||
if o.Machine.Pat != nil {
|
||||
@@ -89,7 +80,6 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user
|
||||
return "", "", nil, nil, err
|
||||
}
|
||||
|
||||
var token string
|
||||
if pat != nil {
|
||||
token = pat.Token
|
||||
}
|
||||
@@ -107,12 +97,7 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, userIDs ...string)
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
userID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...)
|
||||
userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userIDs...)
|
||||
return userID, details, err
|
||||
}
|
||||
|
||||
@@ -365,9 +350,9 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce
|
||||
ResourceOwner(orgID).
|
||||
OrderAsc().
|
||||
AddQuery().
|
||||
AggregateTypes(user_repo.AggregateType).
|
||||
AggregateTypes(user.AggregateType).
|
||||
EventTypes(
|
||||
user_repo.UserIDPLinkAddedType, user_repo.UserIDPLinkRemovedType, user_repo.UserIDPLinkCascadeRemovedType,
|
||||
user.UserIDPLinkAddedType, user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType,
|
||||
).Builder())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -375,13 +360,13 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce
|
||||
links := make([]*domain.UserIDPLink, 0)
|
||||
for _, event := range events {
|
||||
switch eventTyped := event.(type) {
|
||||
case *user_repo.UserIDPLinkAddedEvent:
|
||||
case *user.UserIDPLinkAddedEvent:
|
||||
links = append(links, &domain.UserIDPLink{
|
||||
IDPConfigID: eventTyped.IDPConfigID,
|
||||
ExternalUserID: eventTyped.ExternalUserID,
|
||||
DisplayName: eventTyped.DisplayName,
|
||||
})
|
||||
case *user_repo.UserIDPLinkRemovedEvent:
|
||||
case *user.UserIDPLinkRemovedEvent:
|
||||
for i := range links {
|
||||
if links[i].ExternalUserID == eventTyped.ExternalUserID &&
|
||||
links[i].IDPConfigID == eventTyped.IDPConfigID {
|
||||
@@ -392,7 +377,7 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce
|
||||
}
|
||||
}
|
||||
|
||||
case *user_repo.UserIDPLinkCascadeRemovedEvent:
|
||||
case *user.UserIDPLinkCascadeRemovedEvent:
|
||||
for i := range links {
|
||||
if links[i].ExternalUserID == eventTyped.ExternalUserID &&
|
||||
links[i].IDPConfigID == eventTyped.IDPConfigID {
|
||||
@@ -495,14 +480,14 @@ func OrgUsers(ctx context.Context, filter preparation.FilterToQueryReducer, orgI
|
||||
ResourceOwner(orgID).
|
||||
OrderAsc().
|
||||
AddQuery().
|
||||
AggregateTypes(user_repo.AggregateType).
|
||||
AggregateTypes(user.AggregateType).
|
||||
EventTypes(
|
||||
user_repo.HumanAddedType,
|
||||
user_repo.MachineAddedEventType,
|
||||
user_repo.HumanRegisteredType,
|
||||
user_repo.UserDomainClaimedType,
|
||||
user_repo.UserUserNameChangedType,
|
||||
user_repo.UserRemovedType,
|
||||
user.HumanAddedType,
|
||||
user.MachineAddedEventType,
|
||||
user.HumanRegisteredType,
|
||||
user.UserDomainClaimedType,
|
||||
user.UserUserNameChangedType,
|
||||
user.UserRemovedType,
|
||||
).Builder())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -511,25 +496,25 @@ func OrgUsers(ctx context.Context, filter preparation.FilterToQueryReducer, orgI
|
||||
users := make([]userIDName, 0)
|
||||
for _, event := range events {
|
||||
switch eventTyped := event.(type) {
|
||||
case *user_repo.HumanAddedEvent:
|
||||
case *user.HumanAddedEvent:
|
||||
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
||||
case *user_repo.MachineAddedEvent:
|
||||
case *user.MachineAddedEvent:
|
||||
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
||||
case *user_repo.HumanRegisteredEvent:
|
||||
case *user.HumanRegisteredEvent:
|
||||
users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID})
|
||||
case *user_repo.DomainClaimedEvent:
|
||||
case *user.DomainClaimedEvent:
|
||||
for i := range users {
|
||||
if users[i].id == eventTyped.Aggregate().ID {
|
||||
users[i].name = eventTyped.UserName
|
||||
}
|
||||
}
|
||||
case *user_repo.UsernameChangedEvent:
|
||||
case *user.UsernameChangedEvent:
|
||||
for i := range users {
|
||||
if users[i].id == eventTyped.Aggregate().ID {
|
||||
users[i].name = eventTyped.UserName
|
||||
}
|
||||
}
|
||||
case *user_repo.UserRemovedEvent:
|
||||
case *user.UserRemovedEvent:
|
||||
for i := range users {
|
||||
if users[i].id == eventTyped.Aggregate().ID {
|
||||
users[i] = users[len(users)-1]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type permissionCheck func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error)
|
||||
|
||||
const (
|
||||
permissionUserWrite = "user.write"
|
||||
)
|
||||
@@ -2,7 +2,6 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
@@ -14,6 +13,6 @@ type Phone struct {
|
||||
Verified bool
|
||||
}
|
||||
|
||||
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
||||
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
|
||||
}
|
||||
|
||||
@@ -59,12 +59,16 @@ func (wm *SystemConfigWriteModel) Reduce() error {
|
||||
}
|
||||
wm.Instances[e.Aggregate().InstanceID].Domains = append(wm.Instances[e.Aggregate().InstanceID].Domains, e.Domain)
|
||||
case *instance.DomainRemovedEvent:
|
||||
domains := wm.Instances[e.Aggregate().InstanceID].Domains
|
||||
for i, domain := range domains {
|
||||
instance, ok := wm.Instances[e.Aggregate().InstanceID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for i, domain := range instance.Domains {
|
||||
if domain == e.Domain {
|
||||
domains[i] = domains[len(domains)-1]
|
||||
domains[len(domains)-1] = ""
|
||||
wm.Instances[e.Aggregate().InstanceID].Domains = domains[:len(domains)-1]
|
||||
instance.Domains[i] = instance.Domains[len(instance.Domains)-1]
|
||||
instance.Domains[len(instance.Domains)-1] = ""
|
||||
wm.Instances[e.Aggregate().InstanceID].Domains = instance.Domains[:len(instance.Domains)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
||||
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
|
||||
}
|
||||
|
||||
|
||||
+191
-98
@@ -27,6 +27,8 @@ func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) (
|
||||
}
|
||||
|
||||
type AddHuman struct {
|
||||
// ID is optional, if empty it will be generated
|
||||
ID string
|
||||
// Username is required
|
||||
Username string
|
||||
// FirstName is required
|
||||
@@ -43,63 +45,98 @@ type AddHuman struct {
|
||||
PreferredLanguage language.Tag
|
||||
// Gender is required
|
||||
Gender domain.Gender
|
||||
//Phone represents an international phone number
|
||||
// Phone represents an international phone number
|
||||
Phone Phone
|
||||
//Password is optional
|
||||
// Password is optional
|
||||
Password string
|
||||
//BcryptedPassword is optional
|
||||
// BcryptedPassword is optional
|
||||
BcryptedPassword string
|
||||
//PasswordChangeRequired is used if the `Password`-field is set
|
||||
// PasswordChangeRequired is used if the `Password`-field is set
|
||||
PasswordChangeRequired bool
|
||||
Passwordless bool
|
||||
ExternalIDP bool
|
||||
Register bool
|
||||
Metadata []*AddMetadataEntry
|
||||
|
||||
// Details are set after a successful execution of the command
|
||||
Details *domain.ObjectDetails
|
||||
|
||||
// EmailCode is set by the command
|
||||
EmailCode *string
|
||||
}
|
||||
|
||||
func (c *Commands) AddHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) {
|
||||
existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (h *AddHuman) Validate() (err error) {
|
||||
if err := h.Email.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if isUserStateExists(existingHuman.UserState) {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
|
||||
if h.Username = strings.TrimSpace(h.Username); h.Username == "" {
|
||||
return errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
|
||||
}
|
||||
|
||||
return c.addHumanWithID(ctx, resourceOwner, userID, human)
|
||||
if h.FirstName = strings.TrimSpace(h.FirstName); h.FirstName == "" {
|
||||
return errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
|
||||
}
|
||||
if h.LastName = strings.TrimSpace(h.LastName); h.LastName == "" {
|
||||
return errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
|
||||
}
|
||||
h.ensureDisplayName()
|
||||
|
||||
if h.Phone.Number != "" {
|
||||
if h.Phone.Number, err = h.Phone.Number.Normalize(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, metadataEntry := range h.Metadata {
|
||||
if err := metadataEntry.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commands) addHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) {
|
||||
agg := user.NewAggregate(userID, resourceOwner)
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddHumanCommand(agg, human, c.userPasswordAlg, c.userEncryption))
|
||||
type AddMetadataEntry struct {
|
||||
Key string
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (m *AddMetadataEntry) Valid() error {
|
||||
if m.Key = strings.TrimSpace(m.Key); m.Key == "" {
|
||||
return errors.ThrowInvalidArgument(nil, "USER-Drght", "Errors.User.Metadata.KeyEmpty")
|
||||
}
|
||||
if len(m.Value) == 0 {
|
||||
return errors.ThrowInvalidArgument(nil, "USER-Dbgth", "Errors.User.Metadata.ValueEmpty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool) (err error) {
|
||||
if resourceOwner == "" {
|
||||
return errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
|
||||
}
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
|
||||
c.AddHumanCommand(
|
||||
human,
|
||||
resourceOwner,
|
||||
c.userPasswordAlg,
|
||||
c.userEncryption,
|
||||
allowInitMail,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
human.Details = &domain.ObjectDetails{
|
||||
Sequence: events[len(events)-1].Sequence(),
|
||||
EventDate: events[len(events)-1].CreationDate(),
|
||||
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
|
||||
}
|
||||
|
||||
return &domain.HumanDetails{
|
||||
ID: userID,
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
Sequence: events[len(events)-1].Sequence(),
|
||||
EventDate: events[len(events)-1].CreationDate(),
|
||||
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) {
|
||||
if resourceOwner == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
|
||||
}
|
||||
userID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.addHumanWithID(ctx, resourceOwner, userID, human)
|
||||
return nil
|
||||
}
|
||||
|
||||
type humanCreationCommand interface {
|
||||
@@ -108,30 +145,18 @@ type humanCreationCommand interface {
|
||||
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
|
||||
}
|
||||
|
||||
func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm) preparation.Validation {
|
||||
func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
|
||||
return func() (_ preparation.CreateCommands, err error) {
|
||||
if err := human.Email.Validate(); err != nil {
|
||||
if err := human.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
|
||||
}
|
||||
|
||||
if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
|
||||
}
|
||||
if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
|
||||
}
|
||||
human.ensureDisplayName()
|
||||
|
||||
if human.Phone.Number != "" {
|
||||
if human.Phone.Number, err = human.Phone.Number.Normalize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
if err := c.addHumanCommandCheckID(ctx, filter, human, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := user.NewAggregate(human.ID, orgID)
|
||||
|
||||
domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -176,55 +201,30 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
|
||||
createCmd.AddPhoneData(human.Phone.Number)
|
||||
}
|
||||
|
||||
if human.Password != "" {
|
||||
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
|
||||
}
|
||||
|
||||
if human.BcryptedPassword != "" {
|
||||
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
|
||||
if err := addHumanCommandPassword(ctx, filter, createCmd, human, passwordAlg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmds := make([]eventstore.Command, 0, 3)
|
||||
cmds = append(cmds, createCmd)
|
||||
|
||||
if human.Email.Verified {
|
||||
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
|
||||
}
|
||||
//add init code if
|
||||
// email not verified or
|
||||
// user not registered and password set
|
||||
if human.shouldAddInitCode() {
|
||||
value, expiry, err := newUserInitCode(ctx, filter, codeAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
||||
} else {
|
||||
if !human.Email.Verified {
|
||||
value, expiry, err := newEmailCode(ctx, filter, codeAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
||||
}
|
||||
cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, a, human, codeAlg, allowInitMail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if human.Phone.Verified {
|
||||
cmds = append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate))
|
||||
} else if human.Phone.Number != "" {
|
||||
value, expiry, err := newPhoneCode(ctx, filter, codeAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
||||
cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, a, human, codeAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, metadataEntry := range human.Metadata {
|
||||
cmds = append(cmds, user.NewMetadataSetEvent(
|
||||
ctx,
|
||||
&a.Aggregate,
|
||||
metadataEntry.Key,
|
||||
metadataEntry.Value,
|
||||
))
|
||||
}
|
||||
|
||||
return cmds, nil
|
||||
@@ -232,6 +232,85 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) ([]eventstore.Command, error) {
|
||||
if human.Email.Verified {
|
||||
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
|
||||
}
|
||||
|
||||
// if allowInitMail, used for v1 api (system, admin, mgmt, auth):
|
||||
// add init code if
|
||||
// email not verified or
|
||||
// user not registered and password set
|
||||
if allowInitMail && human.shouldAddInitCode() {
|
||||
initCode, err := newUserInitCode(ctx, filter, codeAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry)), nil
|
||||
}
|
||||
if !human.Email.Verified {
|
||||
emailCode, err := c.newEmailCode(ctx, filter, codeAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if human.Email.ReturnCode {
|
||||
human.EmailCode = &emailCode.Plain
|
||||
}
|
||||
return append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &a.Aggregate, emailCode.Crypted, emailCode.Expiry, human.Email.URLTemplate, human.Email.ReturnCode)), nil
|
||||
}
|
||||
return cmds, nil
|
||||
}
|
||||
func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
|
||||
if human.Phone.Number == "" {
|
||||
return cmds, nil
|
||||
}
|
||||
if human.Phone.Verified {
|
||||
return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)), nil
|
||||
}
|
||||
phoneCode, err := newPhoneCode(ctx, filter, codeAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry)), nil
|
||||
}
|
||||
|
||||
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, human *AddHuman, orgID string) (err error) {
|
||||
if human.ID == "" {
|
||||
human.ID, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
existingHuman, err := humanWriteModelByID(ctx, filter, human.ID, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isUserStateExists(existingHuman.UserState) {
|
||||
return errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, passwordAlg crypto.HashAlgorithm) (err error) {
|
||||
if human.Password != "" {
|
||||
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
|
||||
return nil
|
||||
}
|
||||
|
||||
if human.BcryptedPassword != "" {
|
||||
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error {
|
||||
if mustBeDomain {
|
||||
return nil
|
||||
@@ -507,7 +586,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
|
||||
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
|
||||
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
||||
} else {
|
||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -651,3 +730,17 @@ func (c *Commands) getHumanWriteModelByID(ctx context.Context, userID, resourceo
|
||||
}
|
||||
return humanWriteModel, nil
|
||||
}
|
||||
|
||||
func humanWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceowner string) (*HumanWriteModel, error) {
|
||||
humanWriteModel := NewHumanWriteModel(userID, resourceowner)
|
||||
events, err := filter(ctx, humanWriteModel.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(events) == 0 {
|
||||
return humanWriteModel, nil
|
||||
}
|
||||
humanWriteModel.AppendEvents(events...)
|
||||
err = humanWriteModel.Reduce()
|
||||
return humanWriteModel, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
@@ -41,7 +42,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, em
|
||||
if email.IsEmailVerified {
|
||||
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
||||
} else {
|
||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -113,7 +114,7 @@ func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID,
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9ds", "Errors.User.Email.AlreadyVerified")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
|
||||
emailCode, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
@@ -71,11 +72,14 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
||||
if accountName == "" {
|
||||
accountName = string(human.EmailAddress)
|
||||
}
|
||||
key, secret, err := domain.NewOTPKey(c.multifactors.OTP.Issuer, accountName, c.multifactors.OTP.CryptoMFA)
|
||||
issuer := c.multifactors.OTP.Issuer
|
||||
if issuer == "" {
|
||||
issuer = authz.GetInstance(ctx).RequestedDomain()
|
||||
}
|
||||
key, secret, err := domain.NewOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = c.eventstore.Push(ctx, user.NewHumanOTPAddedEvent(ctx, userAgg, secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+471
-122
@@ -2,17 +2,19 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
@@ -29,16 +31,20 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
idGenerator id.Generator
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
codeAlg crypto.EncryptionAlgorithm
|
||||
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
orgID string
|
||||
human *AddHuman
|
||||
secretGenerator crypto.Generator
|
||||
allowInitMail bool
|
||||
}
|
||||
type res struct {
|
||||
want *domain.HumanDetails
|
||||
err func(error) bool
|
||||
want *domain.ObjectDetails
|
||||
wantID string
|
||||
wantEmailCode string
|
||||
err func(error) bool
|
||||
}
|
||||
|
||||
userAgg := user.NewAggregate("user1", "org1")
|
||||
@@ -68,9 +74,67 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
Address: "[email protected]",
|
||||
},
|
||||
},
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user invalid, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &AddHuman{
|
||||
Username: "username",
|
||||
FirstName: "firstname",
|
||||
},
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with id, already exists, precondition error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", true, ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &AddHuman{
|
||||
ID: "user1",
|
||||
Username: "username",
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
Email: Email{
|
||||
Address: "[email protected]",
|
||||
},
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -81,6 +145,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
@@ -95,9 +160,12 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
},
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsInternal,
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -106,6 +174,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -134,36 +203,20 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
},
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsInternal,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user invalid, invalid argument error",
|
||||
fields: fields{
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &AddHuman{
|
||||
Username: "username",
|
||||
FirstName: "firstname",
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal"))
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human (with initial code), ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -237,16 +290,15 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.HumanDetails{
|
||||
ID: "user1",
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
Sequence: 0,
|
||||
EventDate: time.Time{},
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
Sequence: 0,
|
||||
EventDate: time.Time{},
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -254,6 +306,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -330,14 +383,174 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.HumanDetails{
|
||||
ID: "user1",
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human (with password and email code custom template), ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailCodeAddedEventV2(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("emailCode"),
|
||||
},
|
||||
1*time.Hour,
|
||||
"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}",
|
||||
false,
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newEmailCode: mockEmailCode("emailCode", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &AddHuman{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
Email: Email{
|
||||
Address: "[email protected]",
|
||||
URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}",
|
||||
},
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: false,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human (with password and return email code), ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailCodeAddedEventV2(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("emailCode"),
|
||||
},
|
||||
1*time.Hour,
|
||||
"",
|
||||
true,
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newEmailCode: mockEmailCode("emailCode", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &AddHuman{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
Email: Email{
|
||||
Address: "[email protected]",
|
||||
ReturnCode: true,
|
||||
},
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: false,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
wantEmailCode: "emailCode",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -345,6 +558,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -400,14 +614,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PasswordChangeRequired: true,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.HumanDetails{
|
||||
ID: "user1",
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -415,6 +628,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -470,14 +684,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PasswordChangeRequired: true,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.HumanDetails{
|
||||
ID: "user1",
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -485,6 +698,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -540,14 +754,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PasswordChangeRequired: true,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.HumanDetails{
|
||||
ID: "user1",
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -555,6 +768,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -594,9 +808,12 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PasswordChangeRequired: true,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -604,6 +821,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -687,15 +905,14 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PasswordChangeRequired: true,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
|
||||
res: res{
|
||||
want: &domain.HumanDetails{
|
||||
ID: "user1",
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -703,6 +920,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -787,14 +1005,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.HumanDetails{
|
||||
ID: "user1",
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -802,6 +1019,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
@@ -875,14 +1093,105 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
PreferredLanguage: language.English,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.HumanDetails{
|
||||
ID: "user1",
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human with metadata, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
instance.NewSecretGeneratorAddedEvent(
|
||||
context.Background(),
|
||||
&instanceAgg.Aggregate,
|
||||
domain.SecretGeneratorTypeInitCode,
|
||||
0,
|
||||
1*time.Hour,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte(""),
|
||||
},
|
||||
1*time.Hour,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewMetadataSetEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"testKey",
|
||||
[]byte("testValue"),
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &AddHuman{
|
||||
Username: "username",
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
Email: Email{
|
||||
Address: "[email protected]",
|
||||
},
|
||||
PreferredLanguage: language.English,
|
||||
Metadata: []*AddMetadataEntry{
|
||||
{
|
||||
Key: "testKey",
|
||||
Value: []byte("testValue"),
|
||||
},
|
||||
},
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: true,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -893,8 +1202,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
userEncryption: tt.fields.codeAlg,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
newEmailCode: tt.fields.newEmailCode,
|
||||
}
|
||||
got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human)
|
||||
err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail)
|
||||
if tt.res.err == nil {
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
@@ -904,7 +1214,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
if tt.res.err == nil {
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
assert.Equal(t, tt.res.want, tt.args.human.Details)
|
||||
assert.Equal(t, tt.res.wantID, tt.args.human.ID)
|
||||
assert.Equal(t, tt.res.wantEmailCode, gu.Value(tt.args.human.EmailCode))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -958,7 +1270,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -985,7 +1297,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1022,7 +1334,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1065,7 +1377,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1869,7 +2181,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1899,7 +2211,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1939,7 +2251,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1987,7 +2299,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2056,7 +2368,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2125,7 +2437,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2211,7 +2523,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -3147,7 +3459,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) {
|
||||
userID: "",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -3164,7 +3476,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) {
|
||||
userID: "user1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsNotFound,
|
||||
err: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -3261,7 +3573,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
userIDs: []string{"user1"},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -3277,7 +3589,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
userIDs: []string{},
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -3479,37 +3791,41 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone
|
||||
}
|
||||
|
||||
func TestAddHumanCommand(t *testing.T) {
|
||||
type fields struct {
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
a *user.Aggregate
|
||||
human *AddHuman
|
||||
passwordAlg crypto.HashAlgorithm
|
||||
filter preparation.FilterToQueryReducer
|
||||
codeAlg crypto.EncryptionAlgorithm
|
||||
human *AddHuman
|
||||
orgID string
|
||||
passwordAlg crypto.HashAlgorithm
|
||||
filter preparation.FilterToQueryReducer
|
||||
codeAlg crypto.EncryptionAlgorithm
|
||||
allowInitMail bool
|
||||
}
|
||||
agg := user.NewAggregate("id", "ro")
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Want
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want Want
|
||||
}{
|
||||
{
|
||||
name: "invalid email",
|
||||
args: args{
|
||||
a: agg,
|
||||
human: &AddHuman{
|
||||
Email: Email{
|
||||
Address: "invalid",
|
||||
},
|
||||
},
|
||||
orgID: "ro",
|
||||
},
|
||||
want: Want{
|
||||
ValidationErr: errors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
|
||||
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid first name",
|
||||
args: args{
|
||||
a: agg,
|
||||
human: &AddHuman{
|
||||
Username: "username",
|
||||
PreferredLanguage: language.English,
|
||||
@@ -3517,30 +3833,33 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
Address: "[email protected]",
|
||||
},
|
||||
},
|
||||
orgID: "ro",
|
||||
},
|
||||
want: Want{
|
||||
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
|
||||
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid last name",
|
||||
args: args{
|
||||
a: agg,
|
||||
human: &AddHuman{
|
||||
Username: "username",
|
||||
PreferredLanguage: language.English,
|
||||
FirstName: "hurst",
|
||||
Email: Email{Address: "[email protected]"},
|
||||
},
|
||||
orgID: "ro",
|
||||
},
|
||||
want: Want{
|
||||
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
|
||||
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid password",
|
||||
fields: fields{
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||
},
|
||||
args: args{
|
||||
a: agg,
|
||||
human: &AddHuman{
|
||||
Email: Email{Address: "[email protected]"},
|
||||
PreferredLanguage: language.English,
|
||||
@@ -3549,23 +3868,28 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
Password: "short",
|
||||
Username: "username",
|
||||
},
|
||||
orgID: "ro",
|
||||
filter: NewMultiFilter().Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{
|
||||
org.NewDomainPolicyAddedEvent(
|
||||
context.Background(),
|
||||
&org.NewAggregate("id").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
}, nil
|
||||
return []eventstore.Event{}, nil
|
||||
}).
|
||||
Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{
|
||||
org.NewDomainPolicyAddedEvent(
|
||||
ctx,
|
||||
&org.NewAggregate("id").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
}, nil
|
||||
}).
|
||||
Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{
|
||||
org.NewPasswordComplexityPolicyAddedEvent(
|
||||
context.Background(),
|
||||
ctx,
|
||||
&org.NewAggregate("id").Aggregate,
|
||||
8,
|
||||
true,
|
||||
@@ -3578,13 +3902,15 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
Filter(),
|
||||
},
|
||||
want: Want{
|
||||
CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
|
||||
CreateErr: caos_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "correct",
|
||||
fields: fields{
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||
},
|
||||
args: args{
|
||||
a: agg,
|
||||
human: &AddHuman{
|
||||
Email: Email{Address: "[email protected]", Verified: true},
|
||||
PreferredLanguage: language.English,
|
||||
@@ -3593,25 +3919,30 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
Password: "password",
|
||||
Username: "username",
|
||||
},
|
||||
orgID: "ro",
|
||||
passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
filter: NewMultiFilter().Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{
|
||||
org.NewDomainPolicyAddedEvent(
|
||||
context.Background(),
|
||||
&org.NewAggregate("id").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
}, nil
|
||||
return []eventstore.Event{}, nil
|
||||
}).
|
||||
Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{
|
||||
org.NewDomainPolicyAddedEvent(
|
||||
ctx,
|
||||
&org.NewAggregate("id").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
}, nil
|
||||
}).
|
||||
Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{
|
||||
org.NewPasswordComplexityPolicyAddedEvent(
|
||||
context.Background(),
|
||||
ctx,
|
||||
&org.NewAggregate("id").Aggregate,
|
||||
2,
|
||||
false,
|
||||
@@ -3654,7 +3985,25 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
AssertValidation(t, context.Background(), AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg, tt.args.codeAlg), tt.args.filter, tt.want)
|
||||
c := &Commands{
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.human, tt.args.orgID, tt.args.passwordAlg, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockEmailCode(code string, exp time.Duration) func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return &CryptoCodeWithExpiry{
|
||||
Crypted: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte(code),
|
||||
},
|
||||
Plain: code,
|
||||
Expiry: exp,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
// ChangeUserEmail sets a user's email address, generates a code
|
||||
// and triggers a notification e-mail with the default confirmation URL format.
|
||||
func (c *Commands) ChangeUserEmail(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) {
|
||||
return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, false, "")
|
||||
}
|
||||
|
||||
// ChangeUserEmailURLTemplate sets a user's email address, generates a code
|
||||
// and triggers a notification e-mail with the confirmation URL rendered from the passed urlTmpl.
|
||||
// urlTmpl must be a valid [tmpl.Template].
|
||||
func (c *Commands) ChangeUserEmailURLTemplate(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, urlTmpl string) (*domain.Email, error) {
|
||||
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTmpl, userID, "code", "orgID"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, false, urlTmpl)
|
||||
}
|
||||
|
||||
// ChangeUserEmailReturnCode sets a user's email address, generates a code and does not send a notification email.
|
||||
// The generated plain text code will be set in the returned Email object.
|
||||
func (c *Commands) ChangeUserEmailReturnCode(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) {
|
||||
return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, true, "")
|
||||
}
|
||||
|
||||
// ChangeUserEmailVerified sets a user's email address and marks it is verified.
|
||||
// No code is generated and no confirmation e-mail is send.
|
||||
func (c *Commands) ChangeUserEmailVerified(ctx context.Context, userID, resourceOwner, email string) (*domain.Email, error) {
|
||||
cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd.SetVerified(ctx)
|
||||
return cmd.Push(ctx)
|
||||
}
|
||||
|
||||
func (c *Commands) changeUserEmailWithCode(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, returnCode bool, urlTmpl string) (*domain.Email, error) {
|
||||
config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gen := crypto.NewEncryptionGenerator(*config, alg)
|
||||
return c.changeUserEmailWithGenerator(ctx, userID, resourceOwner, email, gen, returnCode, urlTmpl)
|
||||
}
|
||||
|
||||
// changeUserEmailWithGenerator set a user's email address.
|
||||
// returnCode controls if the plain text version of the code will be set in the return object.
|
||||
// When the plain text code is returned, no notification e-mail will be send to the user.
|
||||
// urlTmpl allows changing the target URL that is used by the e-mail and should be a validated Go template, if used.
|
||||
func (c *Commands) changeUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*domain.Email, error) {
|
||||
cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd.Push(ctx)
|
||||
}
|
||||
|
||||
func (c *Commands) VerifyUserEmail(ctx context.Context, userID, resourceOwner, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) {
|
||||
config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gen := crypto.NewEncryptionGenerator(*config, alg)
|
||||
return c.verifyUserEmailWithGenerator(ctx, userID, resourceOwner, code, gen)
|
||||
}
|
||||
|
||||
func (c *Commands) verifyUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, code string, gen crypto.Generator) (*domain.ObjectDetails, error) {
|
||||
cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = cmd.VerifyCode(ctx, code, gen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = cmd.Push(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&cmd.model.WriteModel), nil
|
||||
}
|
||||
|
||||
// UserEmailEvents allows step-by-step additions of events,
|
||||
// operating on the Human Email Model.
|
||||
type UserEmailEvents struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
aggregate *eventstore.Aggregate
|
||||
events []eventstore.Command
|
||||
model *HumanEmailWriteModel
|
||||
|
||||
plainCode *string
|
||||
}
|
||||
|
||||
// NewUserEmailEvents constructs a UserEmailEvents with a Human Email Write Model,
|
||||
// filtered by userID and resourceOwner.
|
||||
// If a model cannot be found, or it's state is invalid and error is returned.
|
||||
func (c *Commands) NewUserEmailEvents(ctx context.Context, userID, resourceOwner string) (*UserEmailEvents, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing")
|
||||
}
|
||||
|
||||
model, err := c.emailWriteModel(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if model.UserState == domain.UserStateUnspecified || model.UserState == domain.UserStateDeleted {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-ieJ2e", "Errors.User.Email.NotFound")
|
||||
}
|
||||
if model.UserState == domain.UserStateInitial {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-uz0Uu", "Errors.User.NotInitialised")
|
||||
}
|
||||
return &UserEmailEvents{
|
||||
eventstore: c.eventstore,
|
||||
aggregate: UserAggregateFromWriteModel(&model.WriteModel),
|
||||
model: model,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Change sets a new email address.
|
||||
// The generated event unsets any previously generated code and verified flag.
|
||||
func (c *UserEmailEvents) Change(ctx context.Context, email domain.EmailAddress) error {
|
||||
if err := email.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
event, hasChanged := c.model.NewChangedEvent(ctx, c.aggregate, email)
|
||||
if !hasChanged {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged")
|
||||
}
|
||||
c.events = append(c.events, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetVerified sets the email address to verified.
|
||||
func (c *UserEmailEvents) SetVerified(ctx context.Context) {
|
||||
c.events = append(c.events, user.NewHumanEmailVerifiedEvent(ctx, c.aggregate))
|
||||
}
|
||||
|
||||
// AddGeneratedCode generates a new encrypted code and sets it to the email address.
|
||||
// When returnCode a plain text of the code will be returned from Push.
|
||||
func (c *UserEmailEvents) AddGeneratedCode(ctx context.Context, gen crypto.Generator, urlTmpl string, returnCode bool) error {
|
||||
value, plain, err := crypto.NewCode(gen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.events = append(c.events, user.NewHumanEmailCodeAddedEventV2(ctx, c.aggregate, value, gen.Expiry(), urlTmpl, returnCode))
|
||||
if returnCode {
|
||||
c.plainCode = &plain
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UserEmailEvents) VerifyCode(ctx context.Context, code string, gen crypto.Generator) error {
|
||||
if code == "" {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty")
|
||||
}
|
||||
|
||||
err := crypto.VerifyCode(c.model.CodeCreationDate, c.model.CodeExpiry, c.model.Code, code, gen)
|
||||
if err == nil {
|
||||
c.events = append(c.events, user.NewHumanEmailVerifiedEvent(ctx, c.aggregate))
|
||||
return nil
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, user.NewHumanEmailVerificationFailedEvent(ctx, c.aggregate))
|
||||
logging.WithFields("id", "COMMAND-Zoo6b", "userID", c.aggregate.ID).OnError(err).Error("NewHumanEmailVerificationFailedEvent push failed")
|
||||
return caos_errs.ThrowInvalidArgument(err, "COMMAND-eis9R", "Errors.User.Code.Invalid")
|
||||
}
|
||||
|
||||
// Push all events to the eventstore and Reduce them into the Model.
|
||||
func (c *UserEmailEvents) Push(ctx context.Context) (*domain.Email, error) {
|
||||
pushedEvents, err := c.eventstore.Push(ctx, c.events...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(c.model, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
email := writeModelToEmail(c.model)
|
||||
email.PlainCode = c.plainCode
|
||||
|
||||
return email, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,6 +90,7 @@ const (
|
||||
OIDCGrantTypeAuthorizationCode OIDCGrantType = iota
|
||||
OIDCGrantTypeImplicit
|
||||
OIDCGrantTypeRefreshToken
|
||||
OIDCGrantTypeDeviceCode
|
||||
)
|
||||
|
||||
type OIDCApplicationType int32
|
||||
|
||||
@@ -122,6 +122,8 @@ func NewAuthRequestFromType(requestType AuthRequestType) (*AuthRequest, error) {
|
||||
return &AuthRequest{Request: &AuthRequestOIDC{}}, nil
|
||||
case AuthRequestTypeSAML:
|
||||
return &AuthRequest{Request: &AuthRequestSAML{}}, nil
|
||||
case AuthRequestTypeDevice:
|
||||
return &AuthRequest{Request: &AuthRequestDevice{}}, nil
|
||||
}
|
||||
return nil, errors.ThrowInvalidArgument(nil, "DOMAIN-ds2kl", "invalid request type")
|
||||
}
|
||||
@@ -184,3 +186,12 @@ func (a *AuthRequest) GetScopeOrgID() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *AuthRequest) Done() bool {
|
||||
for _, step := range a.PossibleSteps {
|
||||
if step.Type() == NextStepRedirectToCallback {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
// DeviceAuth describes a Device Authorization request.
|
||||
// It is used as input and output model in the command and query packages.
|
||||
type DeviceAuth struct {
|
||||
models.ObjectRoot
|
||||
|
||||
ClientID string
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Expires time.Time
|
||||
Scopes []string
|
||||
Subject string
|
||||
State DeviceAuthState
|
||||
}
|
||||
|
||||
// DeviceAuthState describes the step the
|
||||
// the device authorization process is in.
|
||||
// We generate the Stringer implemntation for pretier
|
||||
// log output.
|
||||
//
|
||||
//go:generate stringer -type=DeviceAuthState -linecomment
|
||||
type DeviceAuthState uint
|
||||
|
||||
const (
|
||||
DeviceAuthStateUndefined DeviceAuthState = iota // undefined
|
||||
DeviceAuthStateInitiated // initiated
|
||||
DeviceAuthStateApproved // approved
|
||||
DeviceAuthStateDenied // denied
|
||||
DeviceAuthStateExpired // expired
|
||||
DeviceAuthStateRemoved // removed
|
||||
)
|
||||
|
||||
// Exists returns true when not Undefined and
|
||||
// any status lower than Removed.
|
||||
func (s DeviceAuthState) Exists() bool {
|
||||
return s > DeviceAuthStateUndefined && s < DeviceAuthStateRemoved
|
||||
}
|
||||
|
||||
// Done returns true when DeviceAuthState is Approved.
|
||||
// This implements the OIDC interface requirement of "Done"
|
||||
func (s DeviceAuthState) Done() bool {
|
||||
return s == DeviceAuthStateApproved
|
||||
}
|
||||
|
||||
// Denied returns true when DeviceAuthState is Denied, Expired or Removed.
|
||||
// This implements the OIDC interface requirement of "Denied".
|
||||
func (s DeviceAuthState) Denied() bool {
|
||||
return s >= DeviceAuthStateDenied
|
||||
}
|
||||
|
||||
func (s DeviceAuthState) GoString() string {
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
|
||||
// DeviceAuthCanceled is a subset of DeviceAuthState, allowed to
|
||||
// be used in the deviceauth.CanceledEvent.
|
||||
// The string type is used to make the eventstore more readable
|
||||
// on the reason of cancelation.
|
||||
type DeviceAuthCanceled string
|
||||
|
||||
const (
|
||||
DeviceAuthCanceledDenied = "denied"
|
||||
DeviceAuthCanceledExpired = "expired"
|
||||
)
|
||||
|
||||
func (c DeviceAuthCanceled) State() DeviceAuthState {
|
||||
switch c {
|
||||
case DeviceAuthCanceledDenied:
|
||||
return DeviceAuthStateDenied
|
||||
case DeviceAuthCanceledExpired:
|
||||
return DeviceAuthStateExpired
|
||||
default:
|
||||
return DeviceAuthStateUndefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeviceAuthState_Exists(t *testing.T) {
|
||||
tests := []struct {
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s.String(), func(t *testing.T) {
|
||||
if got := tt.s.Exists(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Exists() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthState_Done(t *testing.T) {
|
||||
tests := []struct {
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s.String(), func(t *testing.T) {
|
||||
if got := tt.s.Done(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Done() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthState_Denied(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.s.Denied(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Denied() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthCanceled_State(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c DeviceAuthCanceled
|
||||
want DeviceAuthState
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
want: DeviceAuthStateUndefined,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
c: "foo",
|
||||
want: DeviceAuthStateUndefined,
|
||||
},
|
||||
{
|
||||
name: "denied",
|
||||
c: DeviceAuthCanceledDenied,
|
||||
want: DeviceAuthStateDenied,
|
||||
},
|
||||
{
|
||||
name: "expired",
|
||||
c: DeviceAuthCanceledExpired,
|
||||
want: DeviceAuthStateExpired,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.c.State(); got != tt.want {
|
||||
t.Errorf("DeviceAuthCanceled.State() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Code generated by "stringer -type=DeviceAuthState -linecomment"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[DeviceAuthStateUndefined-0]
|
||||
_ = x[DeviceAuthStateInitiated-1]
|
||||
_ = x[DeviceAuthStateApproved-2]
|
||||
_ = x[DeviceAuthStateDenied-3]
|
||||
_ = x[DeviceAuthStateExpired-4]
|
||||
_ = x[DeviceAuthStateRemoved-5]
|
||||
}
|
||||
|
||||
const _DeviceAuthState_name = "undefinedinitiatedapproveddeniedexpiredremoved"
|
||||
|
||||
var _DeviceAuthState_index = [...]uint8{0, 9, 18, 26, 32, 39, 46}
|
||||
|
||||
func (i DeviceAuthState) String() string {
|
||||
if i >= DeviceAuthState(len(_DeviceAuthState_index)-1) {
|
||||
return "DeviceAuthState(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _DeviceAuthState_name[_DeviceAuthState_index[i]:_DeviceAuthState_index[i+1]]
|
||||
}
|
||||
@@ -10,11 +10,6 @@ import (
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type HumanDetails struct {
|
||||
ID string
|
||||
ObjectDetails
|
||||
}
|
||||
|
||||
type Human struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
@@ -91,7 +86,13 @@ func (u *Human) CheckDomainPolicy(policy *DomainPolicy) error {
|
||||
}
|
||||
|
||||
func (u *Human) EnsureDisplayName() {
|
||||
if u.Profile != nil && u.DisplayName == "" && u.FirstName != "" && u.LastName != "" {
|
||||
if u.Profile == nil {
|
||||
u.Profile = new(Profile)
|
||||
}
|
||||
if u.DisplayName != "" {
|
||||
return
|
||||
}
|
||||
if u.FirstName != "" && u.LastName != "" {
|
||||
u.DisplayName = u.FirstName + " " + u.LastName
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
@@ -35,6 +38,8 @@ type Email struct {
|
||||
|
||||
EmailAddress EmailAddress
|
||||
IsEmailVerified bool
|
||||
// PlainCode is set by the command and can be used to return it to the caller (API)
|
||||
PlainCode *string
|
||||
}
|
||||
|
||||
type EmailCode struct {
|
||||
@@ -51,13 +56,36 @@ func (e *Email) Validate() error {
|
||||
return e.EmailAddress.Validate()
|
||||
}
|
||||
|
||||
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) {
|
||||
emailCodeCrypto, _, err := crypto.NewCode(emailGenerator)
|
||||
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, string, error) {
|
||||
emailCodeCrypto, code, err := crypto.NewCode(emailGenerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
return &EmailCode{
|
||||
Code: emailCodeCrypto,
|
||||
Expiry: emailGenerator.Expiry(),
|
||||
}, nil
|
||||
}, code, nil
|
||||
}
|
||||
|
||||
type ConfirmURLData struct {
|
||||
UserID string
|
||||
Code string
|
||||
OrgID string
|
||||
}
|
||||
|
||||
// RenderConfirmURLTemplate parses and renders tmplStr.
|
||||
// userID, code and orgID are passed into the [ConfirmURLData].
|
||||
// "%s%s?userID=%s&code=%s&orgID=%s"
|
||||
func RenderConfirmURLTemplate(w io.Writer, tmplStr, userID, code, orgID string) error {
|
||||
tmpl, err := template.New("").Parse(tmplStr)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInvalidArgument(err, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate")
|
||||
}
|
||||
|
||||
data := &ConfirmURLData{userID, code, orgID}
|
||||
if err = tmpl.Execute(w, data); err != nil {
|
||||
return caos_errs.ThrowInvalidArgument(err, "USERv2-ohSi5", "Errors.User.Email.InvalidURLTemplate")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user