Merge branch 'main' into next-rc

# Conflicts:
#	cmd/defaults.yaml
#	cmd/setup/config.go
#	cmd/setup/setup.go
#	cmd/start/start.go
#	docs/yarn.lock
#	go.mod
#	go.sum
#	internal/api/grpc/action/v2beta/execution.go
#	internal/api/grpc/action/v2beta/query.go
#	internal/api/grpc/action/v2beta/server.go
#	internal/api/grpc/action/v2beta/target.go
#	internal/api/grpc/feature/v2/converter.go
#	internal/api/grpc/feature/v2/converter_test.go
#	internal/api/grpc/feature/v2/integration_test/feature_test.go
#	internal/api/grpc/feature/v2beta/converter.go
#	internal/api/grpc/feature/v2beta/converter_test.go
#	internal/api/grpc/feature/v2beta/integration_test/feature_test.go
#	internal/api/oidc/key.go
#	internal/api/oidc/op.go
#	internal/command/idp_intent_test.go
#	internal/command/instance_features.go
#	internal/command/instance_features_test.go
#	internal/command/system_features.go
#	internal/command/system_features_test.go
#	internal/feature/feature.go
#	internal/feature/key_enumer.go
#	internal/integration/client.go
#	internal/query/instance_features.go
#	internal/query/system_features.go
#	internal/repository/feature/feature_v2/feature.go
#	proto/zitadel/feature/v2/instance.proto
#	proto/zitadel/feature/v2/system.proto
#	proto/zitadel/feature/v2beta/instance.proto
#	proto/zitadel/feature/v2beta/system.proto
This commit is contained in:
Livio Spring
2025-07-04 17:51:34 +02:00
1009 changed files with 94589 additions and 10431 deletions

View File

@@ -3,6 +3,7 @@ package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
@@ -11,18 +12,18 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
func (s *Server) SetEmail(ctx context.Context, req *connect.Request[user.SetEmailRequest]) (resp *connect.Response[user.SetEmailResponse], err error) {
var email *domain.Email
switch v := req.GetVerification().(type) {
switch v := req.Msg.GetVerification().(type) {
case *user.SetEmailRequest_SendCode:
email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
case *user.SetEmailRequest_ReturnCode:
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg)
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg)
case *user.SetEmailRequest_IsVerified:
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), req.GetEmail())
email, err = s.command.ChangeUserEmailVerified(ctx, req.Msg.GetUserId(), req.Msg.GetEmail())
case nil:
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg)
email, err = s.command.ChangeUserEmail(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v)
}
@@ -30,26 +31,26 @@ func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp
return nil, err
}
return &user.SetEmailResponse{
return connect.NewResponse(&user.SetEmailResponse{
Details: &object.Details{
Sequence: email.Sequence,
ChangeDate: timestamppb.New(email.ChangeDate),
ResourceOwner: email.ResourceOwner,
},
VerificationCode: email.PlainCode,
}, nil
}), nil
}
func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.ResendEmailCodeResponse, err error) {
func (s *Server) ResendEmailCode(ctx context.Context, req *connect.Request[user.ResendEmailCodeRequest]) (resp *connect.Response[user.ResendEmailCodeResponse], err error) {
var email *domain.Email
switch v := req.GetVerification().(type) {
switch v := req.Msg.GetVerification().(type) {
case *user.ResendEmailCodeRequest_SendCode:
email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.Msg.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
case *user.ResendEmailCodeRequest_ReturnCode:
email, err = s.command.ResendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg)
email, err = s.command.ResendUserEmailReturnCode(ctx, req.Msg.GetUserId(), s.userCodeAlg)
case nil:
email, err = s.command.ResendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg)
email, err = s.command.ResendUserEmailCode(ctx, req.Msg.GetUserId(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method ResendEmailCode not implemented", v)
}
@@ -57,26 +58,26 @@ func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeR
return nil, err
}
return &user.ResendEmailCodeResponse{
return connect.NewResponse(&user.ResendEmailCodeResponse{
Details: &object.Details{
Sequence: email.Sequence,
ChangeDate: timestamppb.New(email.ChangeDate),
ResourceOwner: email.ResourceOwner,
},
VerificationCode: email.PlainCode,
}, nil
}), nil
}
func (s *Server) SendEmailCode(ctx context.Context, req *user.SendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
func (s *Server) SendEmailCode(ctx context.Context, req *connect.Request[user.SendEmailCodeRequest]) (resp *connect.Response[user.SendEmailCodeResponse], err error) {
var email *domain.Email
switch v := req.GetVerification().(type) {
switch v := req.Msg.GetVerification().(type) {
case *user.SendEmailCodeRequest_SendCode:
email, err = s.command.SendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
email, err = s.command.SendUserEmailCodeURLTemplate(ctx, req.Msg.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
case *user.SendEmailCodeRequest_ReturnCode:
email, err = s.command.SendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg)
email, err = s.command.SendUserEmailReturnCode(ctx, req.Msg.GetUserId(), s.userCodeAlg)
case nil:
email, err = s.command.SendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg)
email, err = s.command.SendUserEmailCode(ctx, req.Msg.GetUserId(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method SendEmailCode not implemented", v)
}
@@ -84,30 +85,30 @@ func (s *Server) SendEmailCode(ctx context.Context, req *user.SendEmailCodeReque
return nil, err
}
return &user.SendEmailCodeResponse{
return connect.NewResponse(&user.SendEmailCodeResponse{
Details: &object.Details{
Sequence: email.Sequence,
ChangeDate: timestamppb.New(email.ChangeDate),
ResourceOwner: email.ResourceOwner,
},
VerificationCode: email.PlainCode,
}, nil
}), nil
}
func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) {
func (s *Server) VerifyEmail(ctx context.Context, req *connect.Request[user.VerifyEmailRequest]) (*connect.Response[user.VerifyEmailResponse], error) {
details, err := s.command.VerifyUserEmail(ctx,
req.GetUserId(),
req.GetVerificationCode(),
req.Msg.GetUserId(),
req.Msg.GetVerificationCode(),
s.userCodeAlg,
)
if err != nil {
return nil, err
}
return &user.VerifyEmailResponse{
return connect.NewResponse(&user.VerifyEmailResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}), nil
}

View File

@@ -0,0 +1,188 @@
package user
import (
"context"
"io"
"connectrpc.com/connect"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
legacyobject "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*connect.Response[user.CreateUserResponse], error) {
addHumanPb := &user.AddHumanUserRequest{
Username: userName,
UserId: userId,
Organization: &legacyobject.Organization{
Org: &legacyobject.Organization_OrgId{OrgId: orgId},
},
Profile: humanPb.Profile,
Email: humanPb.Email,
Phone: humanPb.Phone,
IdpLinks: humanPb.IdpLinks,
TotpSecret: humanPb.TotpSecret,
}
switch pwType := humanPb.GetPasswordType().(type) {
case *user.CreateUserRequest_Human_HashedPassword:
addHumanPb.PasswordType = &user.AddHumanUserRequest_HashedPassword{
HashedPassword: pwType.HashedPassword,
}
case *user.CreateUserRequest_Human_Password:
addHumanPb.PasswordType = &user.AddHumanUserRequest_Password{
Password: pwType.Password,
}
default:
// optional password is not set
}
newHuman, err := AddUserRequestToAddHuman(addHumanPb)
if err != nil {
return nil, err
}
if err = s.command.AddUserHuman(
ctx,
orgId,
newHuman,
false,
s.userCodeAlg,
); err != nil {
return nil, err
}
return connect.NewResponse(&user.CreateUserResponse{
Id: newHuman.ID,
CreationDate: timestamppb.New(newHuman.Details.EventDate),
EmailCode: newHuman.EmailCode,
PhoneCode: newHuman.PhoneCode,
}), nil
}
func (s *Server) updateUserTypeHuman(ctx context.Context, humanPb *user.UpdateUserRequest_Human, userId string, userName *string) (*connect.Response[user.UpdateUserResponse], error) {
cmd, err := updateHumanUserToCommand(userId, userName, humanPb)
if err != nil {
return nil, err
}
if err = s.command.ChangeUserHuman(ctx, cmd, s.userCodeAlg); err != nil {
return nil, err
}
return connect.NewResponse(&user.UpdateUserResponse{
ChangeDate: timestamppb.New(cmd.Details.EventDate),
EmailCode: cmd.EmailCode,
PhoneCode: cmd.PhoneCode,
}), nil
}
func updateHumanUserToCommand(userId string, userName *string, human *user.UpdateUserRequest_Human) (*command.ChangeHuman, error) {
phone := human.GetPhone()
if phone != nil && phone.Phone == "" && phone.GetVerification() != nil {
return nil, zerrors.ThrowInvalidArgument(nil, "USERv2-4f3d6", "Errors.User.Phone.VerifyingRemovalIsNotSupported")
}
email, err := setHumanEmailToEmail(human.Email, userId)
if err != nil {
return nil, err
}
return &command.ChangeHuman{
ID: userId,
Username: userName,
Profile: SetHumanProfileToProfile(human.Profile),
Email: email,
Phone: setHumanPhoneToPhone(human.Phone, true),
Password: setHumanPasswordToPassword(human.Password),
}, nil
}
func updateHumanUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) {
email, err := setHumanEmailToEmail(req.Email, req.GetUserId())
if err != nil {
return nil, err
}
changeHuman := &command.ChangeHuman{
ID: req.GetUserId(),
Username: req.Username,
Email: email,
Phone: setHumanPhoneToPhone(req.Phone, false),
Password: setHumanPasswordToPassword(req.Password),
}
if profile := req.GetProfile(); profile != nil {
var firstName *string
if profile.GivenName != "" {
firstName = &profile.GivenName
}
var lastName *string
if profile.FamilyName != "" {
lastName = &profile.FamilyName
}
changeHuman.Profile = SetHumanProfileToProfile(&user.UpdateUserRequest_Human_Profile{
GivenName: firstName,
FamilyName: lastName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: profile.PreferredLanguage,
Gender: profile.Gender,
})
}
return changeHuman, nil
}
func SetHumanProfileToProfile(profile *user.UpdateUserRequest_Human_Profile) *command.Profile {
if profile == nil {
return nil
}
return &command.Profile{
FirstName: profile.GivenName,
LastName: profile.FamilyName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make),
Gender: ifNotNilPtr(profile.Gender, genderToDomain),
}
}
func setHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) {
if email == nil {
return nil, nil
}
var urlTemplate string
if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil {
urlTemplate = *email.GetSendCode().UrlTemplate
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil {
return nil, err
}
}
return &command.Email{
Address: domain.EmailAddress(email.Email),
Verified: email.GetIsVerified(),
ReturnCode: email.GetReturnCode() != nil,
URLTemplate: urlTemplate,
}, nil
}
func setHumanPhoneToPhone(phone *user.SetHumanPhone, withRemove bool) *command.Phone {
if phone == nil {
return nil
}
number := phone.GetPhone()
return &command.Phone{
Number: domain.PhoneNumber(number),
Verified: phone.GetIsVerified(),
ReturnCode: phone.GetReturnCode() != nil,
Remove: withRemove && number == "",
}
}
func setHumanPasswordToPassword(password *user.SetPassword) *command.Password {
if password == nil {
return nil
}
return &command.Password{
PasswordCode: password.GetVerificationCode(),
OldPassword: password.GetCurrentPassword(),
Password: password.GetPassword().GetPassword(),
EncodedPasswordHash: password.GetHashedPassword().GetHash(),
ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(),
}
}

View File

@@ -0,0 +1,254 @@
package user
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_patchHumanUserToCommand(t *testing.T) {
type args struct {
userId string
userName *string
human *user.UpdateUserRequest_Human
}
tests := []struct {
name string
args args
want *command.ChangeHuman
wantErr assert.ErrorAssertionFunc
}{{
name: "single property",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
GivenName: gu.Ptr("givenName"),
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Profile: &command.Profile{
FirstName: gu.Ptr("givenName"),
},
},
wantErr: assert.NoError,
}, {
name: "all properties",
args: args{
userId: "userId",
userName: gu.Ptr("userName"),
human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
GivenName: gu.Ptr("givenName"),
FamilyName: gu.Ptr("familyName"),
NickName: gu.Ptr("nickName"),
DisplayName: gu.Ptr("displayName"),
PreferredLanguage: gu.Ptr("en-US"),
Gender: gu.Ptr(user.Gender_GENDER_FEMALE),
},
Email: &user.SetHumanEmail{
Email: "email@example.com",
Verification: &user.SetHumanEmail_IsVerified{
IsVerified: true,
},
},
Phone: &user.SetHumanPhone{
Phone: "+123456789",
Verification: &user.SetHumanPhone_IsVerified{
IsVerified: true,
},
},
Password: &user.SetPassword{
Verification: &user.SetPassword_CurrentPassword{
CurrentPassword: "currentPassword",
},
PasswordType: &user.SetPassword_Password{
Password: &user.Password{
Password: "newPassword",
ChangeRequired: true,
},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Username: gu.Ptr("userName"),
Profile: &command.Profile{
FirstName: gu.Ptr("givenName"),
LastName: gu.Ptr("familyName"),
NickName: gu.Ptr("nickName"),
DisplayName: gu.Ptr("displayName"),
PreferredLanguage: &language.AmericanEnglish,
Gender: gu.Ptr(domain.GenderFemale),
},
Email: &command.Email{
Address: "email@example.com",
Verified: true,
},
Phone: &command.Phone{
Number: "+123456789",
Verified: true,
},
Password: &command.Password{
OldPassword: "currentPassword",
Password: "newPassword",
ChangeRequired: true,
},
},
wantErr: assert.NoError,
}, {
name: "set email and request code",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Email: &user.SetHumanEmail{
Email: "email@example.com",
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Email: &command.Email{
Address: "email@example.com",
ReturnCode: true,
},
},
wantErr: assert.NoError,
}, {
name: "set email and send code",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Email: &user.SetHumanEmail{
Email: "email@example.com",
Verification: &user.SetHumanEmail_SendCode{
SendCode: &user.SendEmailVerificationCode{},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Email: &command.Email{
Address: "email@example.com",
},
},
wantErr: assert.NoError,
}, {
name: "set email and send code with template",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Email: &user.SetHumanEmail{
Email: "email@example.com",
Verification: &user.SetHumanEmail_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("Code: {{.Code}}"),
},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Email: &command.Email{
Address: "email@example.com",
URLTemplate: "Code: {{.Code}}",
},
},
wantErr: assert.NoError,
}, {
name: "set phone and request code",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Phone: &user.SetHumanPhone{
Phone: "+123456789",
Verification: &user.SetHumanPhone_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Phone: &command.Phone{
Number: "+123456789",
ReturnCode: true,
},
},
wantErr: assert.NoError,
}, {
name: "set phone and send code",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Phone: &user.SetHumanPhone{
Phone: "+123456789",
Verification: &user.SetHumanPhone_SendCode{
SendCode: &user.SendPhoneVerificationCode{},
},
},
},
},
want: &command.ChangeHuman{
ID: "userId",
Phone: &command.Phone{
Number: "+123456789",
},
},
wantErr: assert.NoError,
}, {
name: "remove phone, ok",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Phone: &user.SetHumanPhone{},
},
},
want: &command.ChangeHuman{
ID: "userId",
Phone: &command.Phone{
Remove: true,
},
},
wantErr: assert.NoError,
}, {
name: "remove phone with verification, error",
args: args{
userId: "userId",
human: &user.UpdateUserRequest_Human{
Phone: &user.SetHumanPhone{
Verification: &user.SetHumanPhone_ReturnCode{},
},
},
},
wantErr: assert.Error,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := updateHumanUserToCommand(tt.args.userId, tt.args.userName, tt.args.human)
if !tt.wantErr(t, err, fmt.Sprintf("patchHumanUserToCommand(%v, %v, %v)", tt.args.userId, tt.args.userName, tt.args.human)) {
return
}
if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" {
t.Errorf("patchHumanUserToCommand() mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -3,6 +3,8 @@ package user
import (
"context"
"connectrpc.com/connect"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
@@ -11,22 +13,22 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{
IDPID: req.GetIdpLink().GetIdpId(),
DisplayName: req.GetIdpLink().GetUserName(),
IDPExternalID: req.GetIdpLink().GetUserId(),
func (s *Server) AddIDPLink(ctx context.Context, req *connect.Request[user.AddIDPLinkRequest]) (_ *connect.Response[user.AddIDPLinkResponse], err error) {
details, err := s.command.AddUserIDPLink(ctx, req.Msg.GetUserId(), "", &command.AddLink{
IDPID: req.Msg.GetIdpLink().GetIdpId(),
DisplayName: req.Msg.GetIdpLink().GetUserName(),
IDPExternalID: req.Msg.GetIdpLink().GetUserId(),
})
if err != nil {
return nil, err
}
return &user.AddIDPLinkResponse{
return connect.NewResponse(&user.AddIDPLinkResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func (s *Server) ListIDPLinks(ctx context.Context, req *user.ListIDPLinksRequest) (_ *user.ListIDPLinksResponse, err error) {
queries, err := ListLinkedIDPsRequestToQuery(req)
func (s *Server) ListIDPLinks(ctx context.Context, req *connect.Request[user.ListIDPLinksRequest]) (_ *connect.Response[user.ListIDPLinksResponse], err error) {
queries, err := ListLinkedIDPsRequestToQuery(req.Msg)
if err != nil {
return nil, err
}
@@ -34,10 +36,10 @@ func (s *Server) ListIDPLinks(ctx context.Context, req *user.ListIDPLinksRequest
if err != nil {
return nil, err
}
return &user.ListIDPLinksResponse{
return connect.NewResponse(&user.ListIDPLinksResponse{
Result: IDPLinksToPb(res.Links),
Details: object.ToListDetails(res.SearchResponse),
}, nil
}), nil
}
func ListLinkedIDPsRequestToQuery(req *user.ListIDPLinksRequest) (*query.IDPUserLinksSearchQuery, error) {
@@ -72,14 +74,14 @@ func IDPLinkToPb(link *query.IDPUserLink) *user.IDPLink {
}
}
func (s *Server) RemoveIDPLink(ctx context.Context, req *user.RemoveIDPLinkRequest) (*user.RemoveIDPLinkResponse, error) {
objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveIDPLinkRequestToDomain(ctx, req))
func (s *Server) RemoveIDPLink(ctx context.Context, req *connect.Request[user.RemoveIDPLinkRequest]) (*connect.Response[user.RemoveIDPLinkResponse], error) {
objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveIDPLinkRequestToDomain(ctx, req.Msg))
if err != nil {
return nil, err
}
return &user.RemoveIDPLinkResponse{
return connect.NewResponse(&user.RemoveIDPLinkResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}), nil
}
func RemoveIDPLinkRequestToDomain(ctx context.Context, req *user.RemoveIDPLinkRequest) *domain.UserIDPLink {

View File

@@ -10,13 +10,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_SetEmail(t *testing.T) {
func TestServer_Deprecated_SetEmail(t *testing.T) {
userID := Instance.CreateHumanUser(CTX).GetUserId()
tests := []struct {

View File

@@ -0,0 +1,659 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"slices"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_AddKey(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
req *user.AddKeyRequest
prepare func(request *user.AddKeyRequest) error
}
tests := []struct {
name string
args args
wantErr bool
wantEmtpyKey bool
}{
{
name: "add key, user not existing",
args: args{
&user.AddKeyRequest{
UserId: "notexisting",
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error { return nil },
},
wantErr: true,
},
{
name: "generate key pair, ok",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error {
request.UserId = userId
return nil
},
},
},
{
name: "add valid public key, ok",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
// This is the public key of the tester system user. This must be valid.
PublicKey: []byte(`
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzi+FFSJL7f5yw4KTwzgM
P34ePGycm/M+kT0M7V4Cgx5V3EaDIvTQKTLfBaEB45zb9LtjIXzDw0rXRoS2hO6t
h+CYQCz3KCvh09C0IzxZiB2IS3H/aT+5Bx9EFY+vnAkZjccbyG5YNRvmtOlnvIeI
H7qZ0tEwkPfF5GEZNPJPtmy3UGV7iofdVQS1xRj73+aMw5rvH4D8IdyiAC3VekIb
pt0Vj0SUX3DwKtog337BzTiPk3aXRF0sbFhQoqdJRI8NqgZjCwjq9yfI5tyxYswn
+JGzHGdHvW3idODlmwEt5K2pasiRIWK2OGfq+w0EcltQHabuqEPgZlmhCkRdNfix
BwIDAQAB
-----END PUBLIC KEY-----
`),
},
func(request *user.AddKeyRequest) error {
request.UserId = userId
return nil
},
},
wantEmtpyKey: true,
},
{
name: "add invalid public key, error",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
PublicKey: []byte(`
-----BEGIN PUBLIC KEY-----
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
-----END PUBLIC KEY-----
`),
},
func(request *user.AddKeyRequest) error {
request.UserId = userId
return nil
},
},
wantErr: true,
},
{
name: "add key human, error",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error {
resp := Instance.CreateUserTypeHuman(IamCTX)
request.UserId = resp.Id
return nil
},
},
wantErr: true,
},
{
name: "add another key, ok",
args: args{
&user.AddKeyRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddKeyRequest) error {
request.UserId = userId
_, err := Client.AddKey(IamCTX, &user.AddKeyRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.AddKey(CTX, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.KeyId, "key id is empty")
if tt.wantEmtpyKey {
assert.Empty(t, got.KeyContent, "key content is not empty")
} else {
assert.NotEmpty(t, got.KeyContent, "key content is empty")
}
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_AddKey_Permission(t *testing.T) {
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddKey-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
request := &user.AddKeyRequest{
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)),
UserId: otherOrgUser.GetId(),
}
type args struct {
ctx context.Context
req *user.AddKeyRequest
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{SystemCTX, request},
},
{
name: "instance, ok",
args: args{IamCTX, request},
},
{
name: "org, error",
args: args{OrgCTX, request},
wantErr: true,
},
{
name: "user, error",
args: args{UserCTX, request},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, err)
got, err := Client.AddKey(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.KeyId, "key id is empty")
assert.NotEmpty(t, got.KeyContent, "key content is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemoveKey(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
req *user.RemoveKeyRequest
prepare func(request *user.RemoveKeyRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "remove key, user not existing",
args: args{
&user.RemoveKeyRequest{
UserId: "notexisting",
},
func(request *user.RemoveKeyRequest) error {
key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
request.KeyId = key.GetKeyId()
return err
},
},
wantErr: true,
},
{
name: "remove key, not existing",
args: args{
&user.RemoveKeyRequest{
KeyId: "notexisting",
},
func(request *user.RemoveKeyRequest) error {
request.UserId = userId
return nil
},
},
wantErr: true,
},
{
name: "remove key, ok",
args: args{
&user.RemoveKeyRequest{},
func(request *user.RemoveKeyRequest) error {
key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
request.KeyId = key.GetKeyId()
request.UserId = userId
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.RemoveKey(CTX, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
deletionDate := got.DeletionDate.AsTime()
assert.Greater(t, deletionDate, now, "creation date is before the test started")
assert.Less(t, deletionDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemoveKey_Permission(t *testing.T) {
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveKey-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
request := &user.RemoveKeyRequest{
UserId: otherOrgUser.GetId(),
}
prepare := func(request *user.RemoveKeyRequest) error {
key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)),
UserId: otherOrgUser.GetId(),
})
request.KeyId = key.GetKeyId()
return err
}
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemoveKeyRequest
prepare func(request *user.RemoveKeyRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{SystemCTX, request, prepare},
},
{
name: "instance, ok",
args: args{IamCTX, request, prepare},
},
{
name: "org, error",
args: args{OrgCTX, request, prepare},
wantErr: true,
},
{
name: "user, error",
args: args{UserCTX, request, prepare},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, tt.args.prepare(tt.args.req))
got, err := Client.RemoveKey(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.DeletionDate, "client key is empty")
creationDate := got.DeletionDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_ListKeys(t *testing.T) {
type args struct {
ctx context.Context
req *user.ListKeysRequest
}
type testCase struct {
name string
args args
want *user.ListKeysResponse
}
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListKeys-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
otherOrgUserId := otherOrgUser.GetId()
otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId()
onlySinceTestStartFilter := &user.KeysSearchFilter{Filter: &user.KeysSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{
Timestamp: timestamppb.Now(),
Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS,
}}}
myOrgId := Instance.DefaultOrg.GetId()
myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID
expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24)
myDataPoint := setupKeyDataPoint(t, myUserId, myOrgId, expiresInADay)
otherUserDataPoint := setupKeyDataPoint(t, otherUserId, myOrgId, expiresInADay)
otherOrgDataPointExpiringSoon := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour))
otherOrgDataPointExpiringLate := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30))
sortingColumnExpirationDate := user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE
awaitKeys(t, onlySinceTestStartFilter,
otherOrgDataPointExpiringSoon.GetId(),
otherOrgDataPointExpiringLate.GetId(),
otherUserDataPoint.GetId(),
myDataPoint.GetId(),
)
tests := []testCase{
{
name: "list all, instance",
args: args{
IamCTX,
&user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringLate,
otherOrgDataPointExpiringSoon,
otherUserDataPoint,
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 4,
AppliedLimit: 100,
},
},
},
{
name: "list all, org",
args: args{
OrgCTX,
&user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherUserDataPoint,
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "list all, user",
args: args{
UserCTX,
&user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
},
},
{
name: "list by id",
args: args{
IamCTX,
&user.ListKeysRequest{
Filters: []*user.KeysSearchFilter{
onlySinceTestStartFilter,
{
Filter: &user.KeysSearchFilter_KeyIdFilter{
KeyIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id},
},
},
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringSoon,
},
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
},
},
{
name: "list all from other org",
args: args{
IamCTX,
&user.ListKeysRequest{
Filters: []*user.KeysSearchFilter{
onlySinceTestStartFilter,
{
Filter: &user.KeysSearchFilter_OrganizationIdFilter{
OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId},
},
},
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringLate,
otherOrgDataPointExpiringSoon,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "sort by next expiration dates",
args: args{
IamCTX,
&user.ListKeysRequest{
Pagination: &filter.PaginationRequest{
Asc: true,
},
SortingColumn: &sortingColumnExpirationDate,
Filters: []*user.KeysSearchFilter{
onlySinceTestStartFilter,
{Filter: &user.KeysSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}},
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringSoon,
otherOrgDataPointExpiringLate,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "get page",
args: args{
IamCTX,
&user.ListKeysRequest{
Pagination: &filter.PaginationRequest{
Offset: 2,
Limit: 2,
Asc: true,
},
Filters: []*user.KeysSearchFilter{
onlySinceTestStartFilter,
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{
otherOrgDataPointExpiringSoon,
otherOrgDataPointExpiringLate,
},
Pagination: &filter.PaginationResponse{
TotalResult: 4,
AppliedLimit: 2,
},
},
},
{
name: "empty list",
args: args{
UserCTX,
&user.ListKeysRequest{
Filters: []*user.KeysSearchFilter{
{
Filter: &user.KeysSearchFilter_KeyIdFilter{
KeyIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id},
},
},
},
},
},
want: &user.ListKeysResponse{
Result: []*user.Key{},
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
},
},
}
t.Run("with permission flag v2", func(t *testing.T) {
setPermissionCheckV2Flag(t, true)
defer setPermissionCheckV2Flag(t, false)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListKeys(tt.args.ctx, tt.args.req)
require.NoError(t, err)
assert.Len(t, got.Result, len(tt.want.Result))
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff)
}
})
}
})
t.Run("without permission flag v2", func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListKeys(tt.args.ctx, tt.args.req)
require.NoError(t, err)
assert.Len(t, got.Result, len(tt.want.Result))
// ignore the total result, as this is a known bug with the in-memory permission checks.
// The command can't know how many keys exist in the system if the SQL statement has a limit.
// This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188
tt.want.Pagination.TotalResult = got.Pagination.TotalResult
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
func setupKeyDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.Key {
expirationDatePb := timestamppb.New(expirationDate)
newKey, err := Client.AddKey(SystemCTX, &user.AddKeyRequest{
UserId: userId,
ExpirationDate: expirationDatePb,
PublicKey: nil,
})
require.NoError(t, err)
return &user.Key{
CreationDate: newKey.CreationDate,
ChangeDate: newKey.CreationDate,
Id: newKey.GetKeyId(),
UserId: userId,
OrganizationId: orgId,
ExpirationDate: expirationDatePb,
}
}
func awaitKeys(t *testing.T, sinceTestStartFilter *user.KeysSearchFilter, keyIds ...string) {
sortingColumn := user.KeyFieldName_KEY_FIELD_NAME_ID
slices.Sort(keyIds)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
result, err := Client.ListKeys(SystemCTX, &user.ListKeysRequest{
Filters: []*user.KeysSearchFilter{sinceTestStartFilter},
SortingColumn: &sortingColumn,
Pagination: &filter.PaginationRequest{
Asc: true,
},
})
require.NoError(t, err)
if !assert.Len(collect, result.Result, len(keyIds)) {
return
}
for i := range keyIds {
keyId := keyIds[i]
require.Equal(collect, keyId, result.Result[i].GetId())
}
}, 5*time.Second, time.Second, "key not created in time")
}

View File

@@ -104,7 +104,7 @@ func TestServer_RequestPasswordReset(t *testing.T) {
}
}
func TestServer_SetPassword(t *testing.T) {
func TestServer_Deprecated_SetPassword(t *testing.T) {
type args struct {
ctx context.Context
req *user.SetPasswordRequest

View File

@@ -0,0 +1,615 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"slices"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_AddPersonalAccessToken(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
req *user.AddPersonalAccessTokenRequest
prepare func(request *user.AddPersonalAccessTokenRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "add pat, user not existing",
args: args{
&user.AddPersonalAccessTokenRequest{
UserId: "notexisting",
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error { return nil },
},
wantErr: true,
},
{
name: "add pat, ok",
args: args{
&user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error {
request.UserId = userId
return nil
},
},
},
{
name: "add pat human, not ok",
args: args{
&user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error {
resp := Instance.CreateUserTypeHuman(IamCTX)
request.UserId = resp.Id
return nil
},
},
wantErr: true,
},
{
name: "add another pat, ok",
args: args{
&user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
},
func(request *user.AddPersonalAccessTokenRequest) error {
request.UserId = userId
_, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.AddPersonalAccessToken(CTX, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.TokenId, "id is empty")
assert.NotEmpty(t, got.Token, "token is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_AddPersonalAccessToken_Permission(t *testing.T) {
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddPersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
request := &user.AddPersonalAccessTokenRequest{
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)),
UserId: otherOrgUser.GetId(),
}
type args struct {
ctx context.Context
req *user.AddPersonalAccessTokenRequest
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{SystemCTX, request},
},
{
name: "instance, ok",
args: args{IamCTX, request},
},
{
name: "org, error",
args: args{OrgCTX, request},
wantErr: true,
},
{
name: "user, error",
args: args{UserCTX, request},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, err)
got, err := Client.AddPersonalAccessToken(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.TokenId, "id is empty")
assert.NotEmpty(t, got.Token, "token is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemovePersonalAccessToken(t *testing.T) {
resp := Instance.CreateUserTypeMachine(IamCTX)
userId := resp.GetId()
expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24))
type args struct {
req *user.RemovePersonalAccessTokenRequest
prepare func(request *user.RemovePersonalAccessTokenRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "remove pat, user not existing",
args: args{
&user.RemovePersonalAccessTokenRequest{
UserId: "notexisting",
},
func(request *user.RemovePersonalAccessTokenRequest) error {
pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
request.TokenId = pat.GetTokenId()
return err
},
},
wantErr: true,
},
{
name: "remove pat, not existing",
args: args{
&user.RemovePersonalAccessTokenRequest{
TokenId: "notexisting",
},
func(request *user.RemovePersonalAccessTokenRequest) error {
request.UserId = userId
return nil
},
},
wantErr: true,
},
{
name: "remove pat, ok",
args: args{
&user.RemovePersonalAccessTokenRequest{},
func(request *user.RemovePersonalAccessTokenRequest) error {
pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{
ExpirationDate: expirationDate,
UserId: userId,
})
request.TokenId = pat.GetTokenId()
request.UserId = userId
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.RemovePersonalAccessToken(CTX, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
deletionDate := got.DeletionDate.AsTime()
assert.Greater(t, deletionDate, now, "creation date is before the test started")
assert.Less(t, deletionDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemovePersonalAccessToken_Permission(t *testing.T) {
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemovePersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
request := &user.RemovePersonalAccessTokenRequest{
UserId: otherOrgUser.GetId(),
}
prepare := func(request *user.RemovePersonalAccessTokenRequest) error {
pat, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)),
UserId: otherOrgUser.GetId(),
})
request.TokenId = pat.GetTokenId()
return err
}
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemovePersonalAccessTokenRequest
prepare func(request *user.RemovePersonalAccessTokenRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{SystemCTX, request, prepare},
},
{
name: "instance, ok",
args: args{IamCTX, request, prepare},
},
{
name: "org, error",
args: args{CTX, request, prepare},
wantErr: true,
},
{
name: "user, error",
args: args{UserCTX, request, prepare},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, tt.args.prepare(tt.args.req))
got, err := Client.RemovePersonalAccessToken(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.DeletionDate, "client pat is empty")
creationDate := got.DeletionDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_ListPersonalAccessTokens(t *testing.T) {
type args struct {
ctx context.Context
req *user.ListPersonalAccessTokensRequest
}
type testCase struct {
name string
args args
want *user.ListPersonalAccessTokensResponse
}
OrgCTX := CTX
otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListPersonalAccessTokens-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
otherOrgUserId := otherOrgUser.GetId()
otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId()
onlySinceTestStartFilter := &user.PersonalAccessTokensSearchFilter{Filter: &user.PersonalAccessTokensSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{
Timestamp: timestamppb.Now(),
Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS,
}}}
myOrgId := Instance.DefaultOrg.GetId()
myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID
expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24)
myDataPoint := setupPATDataPoint(t, myUserId, myOrgId, expiresInADay)
otherUserDataPoint := setupPATDataPoint(t, otherUserId, myOrgId, expiresInADay)
otherOrgDataPointExpiringSoon := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour))
otherOrgDataPointExpiringLate := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30))
sortingColumnExpirationDate := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE
awaitPersonalAccessTokens(t,
onlySinceTestStartFilter,
otherOrgDataPointExpiringSoon.GetId(),
otherOrgDataPointExpiringLate.GetId(),
otherUserDataPoint.GetId(),
myDataPoint.GetId(),
)
tests := []testCase{
{
name: "list all, instance",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringLate,
otherOrgDataPointExpiringSoon,
otherUserDataPoint,
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 4,
AppliedLimit: 100,
},
},
},
{
name: "list all, org",
args: args{
OrgCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherUserDataPoint,
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "list all, user",
args: args{
UserCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
myDataPoint,
},
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
},
},
{
name: "list by id",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{
onlySinceTestStartFilter,
{
Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{
TokenIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id},
},
},
},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringSoon,
},
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
},
},
{
name: "list all from other org",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{
onlySinceTestStartFilter,
{
Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{
OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId},
},
}},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringLate,
otherOrgDataPointExpiringSoon,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "sort by next expiration dates",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Pagination: &filter.PaginationRequest{
Asc: true,
},
SortingColumn: &sortingColumnExpirationDate,
Filters: []*user.PersonalAccessTokensSearchFilter{
onlySinceTestStartFilter,
{Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}},
},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringSoon,
otherOrgDataPointExpiringLate,
},
Pagination: &filter.PaginationResponse{
TotalResult: 2,
AppliedLimit: 100,
},
},
},
{
name: "get page",
args: args{
IamCTX,
&user.ListPersonalAccessTokensRequest{
Pagination: &filter.PaginationRequest{
Offset: 2,
Limit: 2,
Asc: true,
},
Filters: []*user.PersonalAccessTokensSearchFilter{
onlySinceTestStartFilter,
},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{
otherOrgDataPointExpiringSoon,
otherOrgDataPointExpiringLate,
},
Pagination: &filter.PaginationResponse{
TotalResult: 4,
AppliedLimit: 2,
},
},
},
{
name: "empty list",
args: args{
UserCTX,
&user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{
{
Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{
TokenIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id},
},
},
},
},
},
want: &user.ListPersonalAccessTokensResponse{
Result: []*user.PersonalAccessToken{},
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
},
},
}
t.Run("with permission flag v2", func(t *testing.T) {
setPermissionCheckV2Flag(t, true)
defer setPermissionCheckV2Flag(t, false)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req)
require.NoError(t, err)
assert.Len(t, got.Result, len(tt.want.Result))
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff)
}
})
}
})
t.Run("without permission flag v2", func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req)
require.NoError(t, err)
assert.Len(t, got.Result, len(tt.want.Result))
// ignore the total result, as this is a known bug with the in-memory permission checks.
// The command can't know how many keys exist in the system if the SQL statement has a limit.
// This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188
tt.want.Pagination.TotalResult = got.Pagination.TotalResult
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
func setupPATDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.PersonalAccessToken {
expirationDatePb := timestamppb.New(expirationDate)
newPersonalAccessToken, err := Client.AddPersonalAccessToken(SystemCTX, &user.AddPersonalAccessTokenRequest{
UserId: userId,
ExpirationDate: expirationDatePb,
})
require.NoError(t, err)
return &user.PersonalAccessToken{
CreationDate: newPersonalAccessToken.CreationDate,
ChangeDate: newPersonalAccessToken.CreationDate,
Id: newPersonalAccessToken.GetTokenId(),
UserId: userId,
OrganizationId: orgId,
ExpirationDate: expirationDatePb,
}
}
func awaitPersonalAccessTokens(t *testing.T, sinceTestStartFilter *user.PersonalAccessTokensSearchFilter, patIds ...string) {
sortingColumn := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID
slices.Sort(patIds)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
result, err := Client.ListPersonalAccessTokens(SystemCTX, &user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{sinceTestStartFilter},
SortingColumn: &sortingColumn,
Pagination: &filter.PaginationRequest{
Asc: true,
},
})
require.NoError(t, err)
if !assert.Len(collect, result.Result, len(patIds)) {
return
}
for i := range patIds {
patId := patIds[i]
require.Equal(collect, patId, result.Result[i].GetId())
}
}, 5*time.Second, time.Second, "pat not created in time")
}

View File

@@ -17,7 +17,7 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_SetPhone(t *testing.T) {
func TestServer_Deprecated_SetPhone(t *testing.T) {
userID := Instance.CreateHumanUser(CTX).GetUserId()
tests := []struct {
@@ -249,7 +249,7 @@ func TestServer_VerifyPhone(t *testing.T) {
}
}
func TestServer_RemovePhone(t *testing.T) {
func TestServer_Deprecated_RemovePhone(t *testing.T) {
userResp := Instance.CreateHumanUser(CTX)
failResp := Instance.CreateHumanUserNoPhone(CTX)
otherUser := Instance.CreateHumanUser(CTX).GetUserId()

View File

@@ -0,0 +1,347 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_AddSecret(t *testing.T) {
type args struct {
ctx context.Context
req *user.AddSecretRequest
prepare func(request *user.AddSecretRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "add secret, user not existing",
args: args{
CTX,
&user.AddSecretRequest{
UserId: "notexisting",
},
func(request *user.AddSecretRequest) error { return nil },
},
wantErr: true,
},
{
name: "add secret, ok",
args: args{
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
return nil
},
},
},
{
name: "add secret human, not ok",
args: args{
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
return nil
},
},
},
{
name: "overwrite secret, ok",
args: args{
CTX,
&user.AddSecretRequest{},
func(request *user.AddSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
_, err := Client.AddSecret(CTX, &user.AddSecretRequest{
UserId: resp.GetId(),
})
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.AddSecret(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.ClientSecret, "client secret is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_AddSecret_Permission(t *testing.T) {
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddSecret-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.AddSecretRequest
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{
SystemCTX,
&user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
},
},
},
{
name: "instance, ok",
args: args{
IamCTX,
&user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
},
},
},
{
name: "org, error",
args: args{
CTX,
&user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
},
},
wantErr: true,
},
{
name: "user, error",
args: args{
UserCTX,
&user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, err)
got, err := Client.AddSecret(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.ClientSecret, "client secret is empty")
creationDate := got.CreationDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemoveSecret(t *testing.T) {
type args struct {
ctx context.Context
req *user.RemoveSecretRequest
prepare func(request *user.RemoveSecretRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "remove secret, user not existing",
args: args{
CTX,
&user.RemoveSecretRequest{
UserId: "notexisting",
},
func(request *user.RemoveSecretRequest) error { return nil },
},
wantErr: true,
},
{
name: "remove secret, not existing",
args: args{
CTX,
&user.RemoveSecretRequest{},
func(request *user.RemoveSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
return nil
},
},
wantErr: true,
},
{
name: "remove secret, ok",
args: args{
CTX,
&user.RemoveSecretRequest{},
func(request *user.RemoveSecretRequest) error {
resp := Instance.CreateUserTypeMachine(CTX)
request.UserId = resp.GetId()
_, err := Instance.Client.UserV2.AddSecret(CTX, &user.AddSecretRequest{
UserId: resp.GetId(),
})
return err
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
deletionDate := got.DeletionDate.AsTime()
assert.Greater(t, deletionDate, now, "creation date is before the test started")
assert.Less(t, deletionDate, time.Now(), "creation date is in the future")
})
}
}
func TestServer_RemoveSecret_Permission(t *testing.T) {
otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveSecret-%s", gofakeit.AppName()), gofakeit.Email())
otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{
OrganizationId: otherOrg.OrganizationId,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: gofakeit.Name(),
},
},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemoveSecretRequest
prepare func(request *user.RemoveSecretRequest) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "system, ok",
args: args{
SystemCTX,
&user.RemoveSecretRequest{
UserId: otherOrgUser.GetId(),
},
func(request *user.RemoveSecretRequest) error {
_, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
})
return err
},
},
},
{
name: "instance, ok",
args: args{
IamCTX,
&user.RemoveSecretRequest{
UserId: otherOrgUser.GetId(),
},
func(request *user.RemoveSecretRequest) error {
_, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
})
return err
},
},
},
{
name: "org, error",
args: args{
CTX,
&user.RemoveSecretRequest{
UserId: otherOrgUser.GetId(),
},
func(request *user.RemoveSecretRequest) error {
_, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
})
return err
},
},
wantErr: true,
},
{
name: "user, error",
args: args{
UserCTX,
&user.RemoveSecretRequest{
UserId: otherOrgUser.GetId(),
},
func(request *user.RemoveSecretRequest) error {
_, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{
UserId: otherOrgUser.GetId(),
})
return err
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := time.Now()
require.NoError(t, tt.args.prepare(tt.args.req))
got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, got.DeletionDate, "client secret is empty")
creationDate := got.DeletionDate.AsTime()
assert.Greater(t, creationDate, now, "creation date is before the test started")
assert.Less(t, creationDate, time.Now(), "creation date is in the future")
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import (
"errors"
"time"
"connectrpc.com/connect"
oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -32,18 +33,18 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) {
switch t := req.GetContent().(type) {
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *connect.Request[user.StartIdentityProviderIntentRequest]) (_ *connect.Response[user.StartIdentityProviderIntentResponse], err error) {
switch t := req.Msg.GetContent().(type) {
case *user.StartIdentityProviderIntentRequest_Urls:
return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls)
return s.startIDPIntent(ctx, req.Msg.GetIdpId(), t.Urls)
case *user.StartIdentityProviderIntentRequest_Ldap:
return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap)
return s.startLDAPIntent(ctx, req.Msg.GetIdpId(), t.Ldap)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t)
}
}
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) {
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*connect.Response[user.StartIdentityProviderIntentResponse], error) {
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
if err != nil {
return nil, err
@@ -52,22 +53,31 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re
if err != nil {
return nil, err
}
content, redirect := session.GetAuth(ctx)
if redirect {
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
}, nil
auth, err := session.GetAuth(ctx)
if err != nil {
return nil, err
}
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
PostForm: []byte(content),
},
}, nil
switch a := auth.(type) {
case *idp.RedirectAuth:
return connect.NewResponse(&user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: a.RedirectURL},
}), nil
case *idp.FormAuth:
return connect.NewResponse(&user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_FormData{
FormData: &user.FormData{
Url: a.URL,
Fields: a.Fields,
},
},
}), nil
}
return nil, zerrors.ThrowInvalidArgumentf(nil, "USERv2-3g2j3", "type oneOf %T in method StartIdentityProviderIntent not implemented", auth)
}
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*connect.Response[user.StartIdentityProviderIntentResponse], error) {
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil)
if err != nil {
return nil, err
@@ -83,7 +93,7 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti
if err != nil {
return nil, err
}
return &user.StartIdentityProviderIntentResponse{
return connect.NewResponse(&user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{
IdpIntent: &user.IDPIntent{
@@ -92,7 +102,7 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti
UserId: userID,
},
},
}, nil
}), nil
}
func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) {
@@ -141,12 +151,12 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string
return externalUser, userID, session, nil
}
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "")
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *connect.Request[user.RetrieveIdentityProviderIntentRequest]) (_ *connect.Response[user.RetrieveIdentityProviderIntentResponse], err error) {
intent, err := s.command.GetIntentWriteModel(ctx, req.Msg.GetIdpIntentId(), "")
if err != nil {
return nil, err
}
if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil {
if err := s.checkIntentToken(req.Msg.GetIdpIntentToken(), intent.AggregateID); err != nil {
return nil, err
}
if intent.State != domain.IDPIntentStateSucceeded {
@@ -194,7 +204,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R
}
idpIntent.AddHumanUser = idpUserToAddHumanUser(idpUser, idpIntent.IdpInformation.IdpId)
}
return idpIntent, nil
return connect.NewResponse(idpIntent), nil
}
type rawUserMapper struct {

View File

@@ -0,0 +1,63 @@
package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddKey(ctx context.Context, req *connect.Request[user.AddKeyRequest]) (*connect.Response[user.AddKeyResponse], error) {
newMachineKey := &command.MachineKey{
ObjectRoot: models.ObjectRoot{
AggregateID: req.Msg.GetUserId(),
},
ExpirationDate: req.Msg.GetExpirationDate().AsTime(),
Type: domain.AuthNKeyTypeJSON,
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
}
newMachineKey.PublicKey = req.Msg.GetPublicKey()
pubkeySupplied := len(newMachineKey.PublicKey) > 0
details, err := s.command.AddUserMachineKey(ctx, newMachineKey)
if err != nil {
return nil, err
}
// Return key details only if the pubkey wasn't supplied, otherwise the user already has
// private key locally
var keyDetails []byte
if !pubkeySupplied {
var err error
keyDetails, err = newMachineKey.Detail()
if err != nil {
return nil, err
}
}
return connect.NewResponse(&user.AddKeyResponse{
KeyId: newMachineKey.KeyID,
KeyContent: keyDetails,
CreationDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) RemoveKey(ctx context.Context, req *connect.Request[user.RemoveKeyRequest]) (*connect.Response[user.RemoveKeyResponse], error) {
machineKey := &command.MachineKey{
ObjectRoot: models.ObjectRoot{
AggregateID: req.Msg.GetUserId(),
},
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
KeyID: req.Msg.GetKeyId(),
}
objectDetails, err := s.command.RemoveUserMachineKey(ctx, machineKey)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.RemoveKeyResponse{
DeletionDate: timestamppb.New(objectDetails.EventDate),
}), nil
}

View File

@@ -0,0 +1,125 @@
package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) ListKeys(ctx context.Context, req *connect.Request[user.ListKeysRequest]) (*connect.Response[user.ListKeysResponse], error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Msg.GetPagination())
if err != nil {
return nil, err
}
filters, err := keyFiltersToQueries(req.Msg.GetFilters())
if err != nil {
return nil, err
}
search := &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: authnKeyFieldNameToSortingColumn(req.Msg.SortingColumn),
},
Queries: filters,
}
result, err := s.query.SearchAuthNKeys(ctx, search, query.JoinFilterUserMachine, s.checkPermission)
if err != nil {
return nil, err
}
resp := &user.ListKeysResponse{
Result: make([]*user.Key, len(result.AuthNKeys)),
Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse),
}
for i, key := range result.AuthNKeys {
resp.Result[i] = &user.Key{
CreationDate: timestamppb.New(key.CreationDate),
ChangeDate: timestamppb.New(key.ChangeDate),
Id: key.ID,
UserId: key.AggregateID,
OrganizationId: key.ResourceOwner,
ExpirationDate: timestamppb.New(key.Expiration),
}
}
return connect.NewResponse(resp), nil
}
func keyFiltersToQueries(filters []*user.KeysSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(filters))
for i, filter := range filters {
q[i], err = keyFilterToQuery(filter)
if err != nil {
return nil, err
}
}
return q, nil
}
func keyFilterToQuery(filter *user.KeysSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *user.KeysSearchFilter_CreatedDateFilter:
return authnKeyCreatedFilterToQuery(q.CreatedDateFilter)
case *user.KeysSearchFilter_ExpirationDateFilter:
return authnKeyExpirationFilterToQuery(q.ExpirationDateFilter)
case *user.KeysSearchFilter_KeyIdFilter:
return authnKeyIdFilterToQuery(q.KeyIdFilter)
case *user.KeysSearchFilter_UserIdFilter:
return authnKeyUserIdFilterToQuery(q.UserIdFilter)
case *user.KeysSearchFilter_OrganizationIdFilter:
return authnKeyOrgIdFilterToQuery(q.OrganizationIdFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func authnKeyIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyIDQuery(f.Id)
}
func authnKeyUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyIdentifyerQuery(f.Id)
}
func authnKeyOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyResourceOwnerQuery(f.Id)
}
func authnKeyCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
}
func authnKeyExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewAuthNKeyExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
}
// authnKeyFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func authnKeyFieldNameToSortingColumn(field *user.KeyFieldName) query.Column {
if field == nil {
return query.AuthNKeyColumnCreationDate
}
switch *field {
case user.KeyFieldName_KEY_FIELD_NAME_UNSPECIFIED:
return query.AuthNKeyColumnCreationDate
case user.KeyFieldName_KEY_FIELD_NAME_ID:
return query.AuthNKeyColumnID
case user.KeyFieldName_KEY_FIELD_NAME_USER_ID:
return query.AuthNKeyColumnIdentifier
case user.KeyFieldName_KEY_FIELD_NAME_ORGANIZATION_ID:
return query.AuthNKeyColumnResourceOwner
case user.KeyFieldName_KEY_FIELD_NAME_CREATED_DATE:
return query.AuthNKeyColumnCreationDate
case user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE:
return query.AuthNKeyColumnExpiration
default:
return query.AuthNKeyColumnCreationDate
}
}

View File

@@ -0,0 +1,60 @@
package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.CreateUserRequest_Machine, orgId, userName, userId string) (*connect.Response[user.CreateUserResponse], error) {
cmd := &command.Machine{
Username: userName,
Name: machinePb.Name,
Description: machinePb.GetDescription(),
AccessTokenType: domain.OIDCTokenTypeBearer,
ObjectRoot: models.ObjectRoot{
ResourceOwner: orgId,
AggregateID: userId,
},
}
details, err := s.command.AddMachine(
ctx,
cmd,
nil,
s.command.NewPermissionCheckUserWrite(ctx),
command.AddMachineWithUsernameToIDFallback(),
)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.CreateUserResponse{
Id: cmd.AggregateID,
CreationDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) updateUserTypeMachine(ctx context.Context, machinePb *user.UpdateUserRequest_Machine, userId string, userName *string) (*connect.Response[user.UpdateUserResponse], error) {
cmd := updateMachineUserToCommand(userId, userName, machinePb)
err := s.command.ChangeUserMachine(ctx, cmd)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.UpdateUserResponse{
ChangeDate: timestamppb.New(cmd.Details.EventDate),
}), nil
}
func updateMachineUserToCommand(userId string, userName *string, machine *user.UpdateUserRequest_Machine) *command.ChangeMachine {
return &command.ChangeMachine{
ID: userId,
Username: userName,
Name: machine.Name,
Description: machine.Description,
}
}

View File

@@ -0,0 +1,62 @@
package user
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/muhlemmer/gu"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_patchMachineUserToCommand(t *testing.T) {
type args struct {
userId string
userName *string
machine *user.UpdateUserRequest_Machine
}
tests := []struct {
name string
args args
want *command.ChangeMachine
}{{
name: "single property",
args: args{
userId: "userId",
machine: &user.UpdateUserRequest_Machine{
Name: gu.Ptr("name"),
},
},
want: &command.ChangeMachine{
ID: "userId",
Name: gu.Ptr("name"),
},
}, {
name: "all properties",
args: args{
userId: "userId",
userName: gu.Ptr("userName"),
machine: &user.UpdateUserRequest_Machine{
Name: gu.Ptr("name"),
Description: gu.Ptr("description"),
},
},
want: &command.ChangeMachine{
ID: "userId",
Username: gu.Ptr("userName"),
Name: gu.Ptr("name"),
Description: gu.Ptr("description"),
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := updateMachineUserToCommand(tt.args.userId, tt.args.userName, tt.args.machine)
if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" {
t.Errorf("patchMachineUserToCommand() mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -3,39 +3,41 @@ package user
import (
"context"
"connectrpc.com/connect"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {
details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), "")
func (s *Server) AddOTPSMS(ctx context.Context, req *connect.Request[user.AddOTPSMSRequest]) (*connect.Response[user.AddOTPSMSResponse], error) {
details, err := s.command.AddHumanOTPSMS(ctx, req.Msg.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}, nil
return connect.NewResponse(&user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}), nil
}
func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) {
objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), "")
func (s *Server) RemoveOTPSMS(ctx context.Context, req *connect.Request[user.RemoveOTPSMSRequest]) (*connect.Response[user.RemoveOTPSMSResponse], error) {
objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.Msg.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
return connect.NewResponse(&user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil
}
func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) {
details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), "")
func (s *Server) AddOTPEmail(ctx context.Context, req *connect.Request[user.AddOTPEmailRequest]) (*connect.Response[user.AddOTPEmailResponse], error) {
details, err := s.command.AddHumanOTPEmail(ctx, req.Msg.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}, nil
return connect.NewResponse(&user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}), nil
}
func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) {
objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), "")
func (s *Server) RemoveOTPEmail(ctx context.Context, req *connect.Request[user.RemoveOTPEmailRequest]) (*connect.Response[user.RemoveOTPEmailResponse], error) {
objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.Msg.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
return connect.NewResponse(&user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil
}

View File

@@ -3,6 +3,7 @@ package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
@@ -13,17 +14,17 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
func (s *Server) RegisterPasskey(ctx context.Context, req *connect.Request[user.RegisterPasskeyRequest]) (resp *connect.Response[user.RegisterPasskeyResponse], err error) {
var (
authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator())
authenticator = passkeyAuthenticatorToDomain(req.Msg.GetAuthenticator())
)
if code := req.GetCode(); code != nil {
if code := req.Msg.GetCode(); code != nil {
return passkeyRegistrationDetailsToPb(
s.command.RegisterUserPasskeyWithCode(ctx, req.GetUserId(), "", authenticator, code.Id, code.Code, req.GetDomain(), s.userCodeAlg),
s.command.RegisterUserPasskeyWithCode(ctx, req.Msg.GetUserId(), "", authenticator, code.Id, code.Code, req.Msg.GetDomain(), s.userCodeAlg),
)
}
return passkeyRegistrationDetailsToPb(
s.command.RegisterUserPasskey(ctx, req.GetUserId(), "", req.GetDomain(), authenticator),
s.command.RegisterUserPasskey(ctx, req.Msg.GetUserId(), "", req.Msg.GetDomain(), authenticator),
)
}
@@ -51,86 +52,86 @@ func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails
return object.DomainToDetailsPb(details.ObjectDetails), options, nil
}
func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) {
func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*connect.Response[user.RegisterPasskeyResponse], error) {
objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err)
if err != nil {
return nil, err
}
return &user.RegisterPasskeyResponse{
return connect.NewResponse(&user.RegisterPasskeyResponse{
Details: objectDetails,
PasskeyId: details.ID,
PublicKeyCredentialCreationOptions: options,
}, nil
}), nil
}
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *connect.Request[user.VerifyPasskeyRegistrationRequest]) (*connect.Response[user.VerifyPasskeyRegistrationResponse], error) {
pkc, err := req.Msg.GetPublicKeyCredential().MarshalJSON()
if err != nil {
return nil, zerrors.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
}
objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), "", req.GetPasskeyName(), "", pkc)
objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.Msg.GetUserId(), "", req.Msg.GetPasskeyName(), "", pkc)
if err != nil {
return nil, err
}
return &user.VerifyPasskeyRegistrationResponse{
return connect.NewResponse(&user.VerifyPasskeyRegistrationResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}), nil
}
func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) {
switch medium := req.Medium.(type) {
func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *connect.Request[user.CreatePasskeyRegistrationLinkRequest]) (resp *connect.Response[user.CreatePasskeyRegistrationLinkResponse], err error) {
switch medium := req.Msg.Medium.(type) {
case nil:
return passkeyDetailsToPb(
s.command.AddUserPasskeyCode(ctx, req.GetUserId(), "", s.userCodeAlg),
s.command.AddUserPasskeyCode(ctx, req.Msg.GetUserId(), "", s.userCodeAlg),
)
case *user.CreatePasskeyRegistrationLinkRequest_SendLink:
return passkeyDetailsToPb(
s.command.AddUserPasskeyCodeURLTemplate(ctx, req.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()),
s.command.AddUserPasskeyCodeURLTemplate(ctx, req.Msg.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()),
)
case *user.CreatePasskeyRegistrationLinkRequest_ReturnCode:
return passkeyCodeDetailsToPb(
s.command.AddUserPasskeyCodeReturn(ctx, req.GetUserId(), "", s.userCodeAlg),
s.command.AddUserPasskeyCodeReturn(ctx, req.Msg.GetUserId(), "", s.userCodeAlg),
)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-gaD8y", "verification oneOf %T in method CreatePasskeyRegistrationLink not implemented", medium)
}
}
func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*connect.Response[user.CreatePasskeyRegistrationLinkResponse], error) {
if err != nil {
return nil, err
}
return &user.CreatePasskeyRegistrationLinkResponse{
return connect.NewResponse(&user.CreatePasskeyRegistrationLinkResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*connect.Response[user.CreatePasskeyRegistrationLinkResponse], error) {
if err != nil {
return nil, err
}
return &user.CreatePasskeyRegistrationLinkResponse{
return connect.NewResponse(&user.CreatePasskeyRegistrationLinkResponse{
Details: object.DomainToDetailsPb(details.ObjectDetails),
Code: &user.PasskeyRegistrationCode{
Id: details.CodeID,
Code: details.Code,
},
}, nil
}), nil
}
func (s *Server) RemovePasskey(ctx context.Context, req *user.RemovePasskeyRequest) (*user.RemovePasskeyResponse, error) {
objectDetails, err := s.command.HumanRemovePasswordless(ctx, req.GetUserId(), req.GetPasskeyId(), "")
func (s *Server) RemovePasskey(ctx context.Context, req *connect.Request[user.RemovePasskeyRequest]) (*connect.Response[user.RemovePasskeyResponse], error) {
objectDetails, err := s.command.HumanRemovePasswordless(ctx, req.Msg.GetUserId(), req.Msg.GetPasskeyId(), "")
if err != nil {
return nil, err
}
return &user.RemovePasskeyResponse{
return connect.NewResponse(&user.RemovePasskeyResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}), nil
}
func (s *Server) ListPasskeys(ctx context.Context, req *user.ListPasskeysRequest) (*user.ListPasskeysResponse, error) {
func (s *Server) ListPasskeys(ctx context.Context, req *connect.Request[user.ListPasskeysRequest]) (*connect.Response[user.ListPasskeysResponse], error) {
query := new(query.UserAuthMethodSearchQueries)
err := query.AppendUserIDQuery(req.UserId)
err := query.AppendUserIDQuery(req.Msg.UserId)
if err != nil {
return nil, err
}
@@ -146,10 +147,10 @@ func (s *Server) ListPasskeys(ctx context.Context, req *user.ListPasskeysRequest
if err != nil {
return nil, err
}
return &user.ListPasskeysResponse{
return connect.NewResponse(&user.ListPasskeysResponse{
Details: object.ToListDetails(authMethods.SearchResponse),
Result: authMethodsToPasskeyPb(authMethods),
}, nil
}), nil
}
func authMethodsToPasskeyPb(methods *query.AuthMethods) []*user.Passkey {

View File

@@ -123,11 +123,11 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyRegistrationDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.wantErr)
if !proto.Equal(tt.want, got) {
if tt.want != nil && !proto.Equal(tt.want, got.Msg) {
t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got)
}
if tt.want != nil {
grpc.AllFieldsSet(t, got.ProtoReflect())
grpc.AllFieldsSet(t, got.Msg.ProtoReflect())
}
})
}
@@ -181,7 +181,9 @@ func Test_passkeyDetailsToPb(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.args.err)
assert.Equal(t, tt.want, got)
if tt.want != nil {
assert.Equal(t, tt.want, got.Msg)
}
})
}
}
@@ -242,9 +244,9 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyCodeDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.args.err)
assert.Equal(t, tt.want, got)
if tt.want != nil {
grpc.AllFieldsSet(t, got.ProtoReflect())
assert.Equal(t, tt.want, got.Msg)
grpc.AllFieldsSet(t, got.Msg.ProtoReflect())
}
})
}

View File

@@ -3,23 +3,25 @@ package user
import (
"context"
"connectrpc.com/connect"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) {
func (s *Server) PasswordReset(ctx context.Context, req *connect.Request[user.PasswordResetRequest]) (_ *connect.Response[user.PasswordResetResponse], err error) {
var details *domain.ObjectDetails
var code *string
switch m := req.GetMedium().(type) {
switch m := req.Msg.GetMedium().(type) {
case *user.PasswordResetRequest_SendLink:
details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType()))
details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.Msg.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType()))
case *user.PasswordResetRequest_ReturnCode:
details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.GetUserId())
details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.Msg.GetUserId())
case nil:
details, code, err = s.command.RequestPasswordReset(ctx, req.GetUserId())
details, code, err = s.command.RequestPasswordReset(ctx, req.Msg.GetUserId())
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-SDeeg", "verification oneOf %T in method RequestPasswordReset not implemented", m)
}
@@ -27,10 +29,10 @@ func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetReque
return nil, err
}
return &user.PasswordResetResponse{
return connect.NewResponse(&user.PasswordResetResponse{
Details: object.DomainToDetailsPb(details),
VerificationCode: code,
}, nil
}), nil
}
func notificationTypeToDomain(notificationType user.NotificationType) domain.NotificationType {
@@ -46,16 +48,16 @@ func notificationTypeToDomain(notificationType user.NotificationType) domain.Not
}
}
func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) {
func (s *Server) SetPassword(ctx context.Context, req *connect.Request[user.SetPasswordRequest]) (_ *connect.Response[user.SetPasswordResponse], err error) {
var details *domain.ObjectDetails
switch v := req.GetVerification().(type) {
switch v := req.Msg.GetVerification().(type) {
case *user.SetPasswordRequest_CurrentPassword:
details, err = s.command.ChangePassword(ctx, "", req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired())
details, err = s.command.ChangePassword(ctx, "", req.Msg.GetUserId(), v.CurrentPassword, req.Msg.GetNewPassword().GetPassword(), "", req.Msg.GetNewPassword().GetChangeRequired())
case *user.SetPasswordRequest_VerificationCode:
details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired())
details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.Msg.GetUserId(), v.VerificationCode, req.Msg.GetNewPassword().GetPassword(), "", req.Msg.GetNewPassword().GetChangeRequired())
case nil:
details, err = s.command.SetPassword(ctx, "", req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired())
details, err = s.command.SetPassword(ctx, "", req.Msg.GetUserId(), req.Msg.GetNewPassword().GetPassword(), req.Msg.GetNewPassword().GetChangeRequired())
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-SFdf2", "verification oneOf %T in method SetPasswordRequest not implemented", v)
}
@@ -63,7 +65,7 @@ func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest)
return nil, err
}
return &user.SetPasswordResponse{
return connect.NewResponse(&user.SetPasswordResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}

View File

@@ -0,0 +1,57 @@
package user
import (
"context"
"connectrpc.com/connect"
"github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/protobuf/types/known/timestamppb"
z_oidc "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddPersonalAccessToken(ctx context.Context, req *connect.Request[user.AddPersonalAccessTokenRequest]) (*connect.Response[user.AddPersonalAccessTokenResponse], error) {
newPat := &command.PersonalAccessToken{
ObjectRoot: models.ObjectRoot{
AggregateID: req.Msg.GetUserId(),
},
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
ExpirationDate: req.Msg.GetExpirationDate().AsTime(),
Scopes: []string{
oidc.ScopeOpenID,
oidc.ScopeProfile,
z_oidc.ScopeUserMetaData,
z_oidc.ScopeResourceOwner,
},
AllowedUserType: domain.UserTypeMachine,
}
details, err := s.command.AddPersonalAccessToken(ctx, newPat)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.AddPersonalAccessTokenResponse{
CreationDate: timestamppb.New(details.EventDate),
TokenId: newPat.TokenID,
Token: newPat.Token,
}), nil
}
func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *connect.Request[user.RemovePersonalAccessTokenRequest]) (*connect.Response[user.RemovePersonalAccessTokenResponse], error) {
objectDetails, err := s.command.RemovePersonalAccessToken(ctx, &command.PersonalAccessToken{
TokenID: req.Msg.GetTokenId(),
ObjectRoot: models.ObjectRoot{
AggregateID: req.Msg.GetUserId(),
},
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
})
if err != nil {
return nil, err
}
return connect.NewResponse(&user.RemovePersonalAccessTokenResponse{
DeletionDate: timestamppb.New(objectDetails.EventDate),
}), nil
}

View File

@@ -0,0 +1,124 @@
package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[user.ListPersonalAccessTokensRequest]) (*connect.Response[user.ListPersonalAccessTokensResponse], error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Msg.GetPagination())
if err != nil {
return nil, err
}
filters, err := patFiltersToQueries(req.Msg.GetFilters())
if err != nil {
return nil, err
}
search := &query.PersonalAccessTokenSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: authnPersonalAccessTokenFieldNameToSortingColumn(req.Msg.SortingColumn),
},
Queries: filters,
}
result, err := s.query.SearchPersonalAccessTokens(ctx, search, s.checkPermission)
if err != nil {
return nil, err
}
resp := &user.ListPersonalAccessTokensResponse{
Result: make([]*user.PersonalAccessToken, len(result.PersonalAccessTokens)),
Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse),
}
for i, pat := range result.PersonalAccessTokens {
resp.Result[i] = &user.PersonalAccessToken{
CreationDate: timestamppb.New(pat.CreationDate),
ChangeDate: timestamppb.New(pat.ChangeDate),
Id: pat.ID,
UserId: pat.UserID,
OrganizationId: pat.ResourceOwner,
ExpirationDate: timestamppb.New(pat.Expiration),
}
}
return connect.NewResponse(resp), nil
}
func patFiltersToQueries(filters []*user.PersonalAccessTokensSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(filters))
for i, filter := range filters {
q[i], err = patFilterToQuery(filter)
if err != nil {
return nil, err
}
}
return q, nil
}
func patFilterToQuery(filter *user.PersonalAccessTokensSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *user.PersonalAccessTokensSearchFilter_CreatedDateFilter:
return authnPersonalAccessTokenCreatedFilterToQuery(q.CreatedDateFilter)
case *user.PersonalAccessTokensSearchFilter_ExpirationDateFilter:
return authnPersonalAccessTokenExpirationFilterToQuery(q.ExpirationDateFilter)
case *user.PersonalAccessTokensSearchFilter_TokenIdFilter:
return authnPersonalAccessTokenIdFilterToQuery(q.TokenIdFilter)
case *user.PersonalAccessTokensSearchFilter_UserIdFilter:
return authnPersonalAccessTokenUserIdFilterToQuery(q.UserIdFilter)
case *user.PersonalAccessTokensSearchFilter_OrganizationIdFilter:
return authnPersonalAccessTokenOrgIdFilterToQuery(q.OrganizationIdFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func authnPersonalAccessTokenIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenIDQuery(f.Id)
}
func authnPersonalAccessTokenUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenUserIDSearchQuery(f.Id)
}
func authnPersonalAccessTokenOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenResourceOwnerSearchQuery(f.Id)
}
func authnPersonalAccessTokenCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
}
func authnPersonalAccessTokenExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
return query.NewPersonalAccessTokenExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
}
// authnPersonalAccessTokenFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func authnPersonalAccessTokenFieldNameToSortingColumn(field *user.PersonalAccessTokenFieldName) query.Column {
if field == nil {
return query.PersonalAccessTokenColumnCreationDate
}
switch *field {
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED:
return query.PersonalAccessTokenColumnCreationDate
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID:
return query.PersonalAccessTokenColumnID
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID:
return query.PersonalAccessTokenColumnUserID
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID:
return query.PersonalAccessTokenColumnResourceOwner
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE:
return query.PersonalAccessTokenColumnCreationDate
case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE:
return query.PersonalAccessTokenColumnExpiration
default:
return query.PersonalAccessTokenColumnCreationDate
}
}

View File

@@ -3,6 +3,7 @@ package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
@@ -11,18 +12,18 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) {
func (s *Server) SetPhone(ctx context.Context, req *connect.Request[user.SetPhoneRequest]) (resp *connect.Response[user.SetPhoneResponse], err error) {
var phone *domain.Phone
switch v := req.GetVerification().(type) {
switch v := req.Msg.GetVerification().(type) {
case *user.SetPhoneRequest_SendCode:
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
phone, err = s.command.ChangeUserPhone(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg)
case *user.SetPhoneRequest_ReturnCode:
phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg)
case *user.SetPhoneRequest_IsVerified:
phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), req.GetPhone())
phone, err = s.command.ChangeUserPhoneVerified(ctx, req.Msg.GetUserId(), req.Msg.GetPhone())
case nil:
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
phone, err = s.command.ChangeUserPhone(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v)
}
@@ -30,42 +31,42 @@ func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp
return nil, err
}
return &user.SetPhoneResponse{
return connect.NewResponse(&user.SetPhoneResponse{
Details: &object.Details{
Sequence: phone.Sequence,
ChangeDate: timestamppb.New(phone.ChangeDate),
ResourceOwner: phone.ResourceOwner,
},
VerificationCode: phone.PlainCode,
}, nil
}), nil
}
func (s *Server) RemovePhone(ctx context.Context, req *user.RemovePhoneRequest) (resp *user.RemovePhoneResponse, err error) {
func (s *Server) RemovePhone(ctx context.Context, req *connect.Request[user.RemovePhoneRequest]) (resp *connect.Response[user.RemovePhoneResponse], err error) {
details, err := s.command.RemoveUserPhone(ctx,
req.GetUserId(),
req.Msg.GetUserId(),
)
if err != nil {
return nil, err
}
return &user.RemovePhoneResponse{
return connect.NewResponse(&user.RemovePhoneResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}), nil
}
func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) {
func (s *Server) ResendPhoneCode(ctx context.Context, req *connect.Request[user.ResendPhoneCodeRequest]) (resp *connect.Response[user.ResendPhoneCodeResponse], err error) {
var phone *domain.Phone
switch v := req.GetVerification().(type) {
switch v := req.Msg.GetVerification().(type) {
case *user.ResendPhoneCodeRequest_SendCode:
phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg)
phone, err = s.command.ResendUserPhoneCode(ctx, req.Msg.GetUserId(), s.userCodeAlg)
case *user.ResendPhoneCodeRequest_ReturnCode:
phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.GetUserId(), s.userCodeAlg)
phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.Msg.GetUserId(), s.userCodeAlg)
case nil:
phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg)
phone, err = s.command.ResendUserPhoneCode(ctx, req.Msg.GetUserId(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-ResendUserPhoneCode", "verification oneOf %T in method SetPhone not implemented", v)
}
@@ -73,30 +74,30 @@ func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeR
return nil, err
}
return &user.ResendPhoneCodeResponse{
return connect.NewResponse(&user.ResendPhoneCodeResponse{
Details: &object.Details{
Sequence: phone.Sequence,
ChangeDate: timestamppb.New(phone.ChangeDate),
ResourceOwner: phone.ResourceOwner,
},
VerificationCode: phone.PlainCode,
}, nil
}), nil
}
func (s *Server) VerifyPhone(ctx context.Context, req *user.VerifyPhoneRequest) (*user.VerifyPhoneResponse, error) {
func (s *Server) VerifyPhone(ctx context.Context, req *connect.Request[user.VerifyPhoneRequest]) (*connect.Response[user.VerifyPhoneResponse], error) {
details, err := s.command.VerifyUserPhone(ctx,
req.GetUserId(),
req.GetVerificationCode(),
req.Msg.GetUserId(),
req.Msg.GetVerificationCode(),
s.userCodeAlg,
)
if err != nil {
return nil, err
}
return &user.VerifyPhoneResponse{
return connect.NewResponse(&user.VerifyPhoneResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}), nil
}

View File

@@ -0,0 +1,40 @@
package user
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddSecret(ctx context.Context, req *connect.Request[user.AddSecretRequest]) (*connect.Response[user.AddSecretResponse], error) {
newSecret := &command.GenerateMachineSecret{
PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx),
}
details, err := s.command.GenerateMachineSecret(ctx, req.Msg.GetUserId(), "", newSecret)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.AddSecretResponse{
CreationDate: timestamppb.New(details.EventDate),
ClientSecret: newSecret.ClientSecret,
}), nil
}
func (s *Server) RemoveSecret(ctx context.Context, req *connect.Request[user.RemoveSecretRequest]) (*connect.Response[user.RemoveSecretResponse], error) {
details, err := s.command.RemoveMachineSecret(
ctx,
req.Msg.GetUserId(),
"",
s.command.NewPermissionCheckUserWrite(ctx),
)
if err != nil {
return nil, err
}
return connect.NewResponse(&user.RemoveSecretResponse{
DeletionDate: timestamppb.New(details.EventDate),
}), nil
}

View File

@@ -2,28 +2,32 @@ package user
import (
"context"
"net/http"
"google.golang.org/grpc"
"connectrpc.com/connect"
"google.golang.org/protobuf/reflect/protoreflect"
"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/config/systemdefaults"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2/userconnect"
)
var _ user.UserServiceServer = (*Server)(nil)
var _ userconnect.UserServiceHandler = (*Server)(nil)
type Server struct {
user.UnimplementedUserServiceServer
command *command.Commands
query *query.Queries
userCodeAlg crypto.EncryptionAlgorithm
idpAlg crypto.EncryptionAlgorithm
idpCallback func(ctx context.Context) string
samlRootURL func(ctx context.Context, idpID string) string
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
userCodeAlg crypto.EncryptionAlgorithm
idpAlg crypto.EncryptionAlgorithm
idpCallback func(ctx context.Context) string
samlRootURL func(ctx context.Context, idpID string) string
assetAPIPrefix func(context.Context) string
@@ -33,6 +37,7 @@ type Server struct {
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
userCodeAlg crypto.EncryptionAlgorithm,
@@ -43,6 +48,7 @@ func CreateServer(
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
userCodeAlg: userCodeAlg,
@@ -54,8 +60,12 @@ func CreateServer(
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
user.RegisterUserServiceServer(grpcServer, s)
func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) {
return userconnect.NewUserServiceHandler(s, connect.WithInterceptors(interceptors...))
}
func (s *Server) FileDescriptor() protoreflect.FileDescriptor {
return user.File_zitadel_user_v2_user_service_proto
}
func (s *Server) AppName() string {

View File

@@ -3,42 +3,44 @@ package user
import (
"context"
"connectrpc.com/connect"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) {
func (s *Server) RegisterTOTP(ctx context.Context, req *connect.Request[user.RegisterTOTPRequest]) (*connect.Response[user.RegisterTOTPResponse], error) {
return totpDetailsToPb(
s.command.AddUserTOTP(ctx, req.GetUserId(), ""),
s.command.AddUserTOTP(ctx, req.Msg.GetUserId(), ""),
)
}
func totpDetailsToPb(totp *domain.TOTP, err error) (*user.RegisterTOTPResponse, error) {
func totpDetailsToPb(totp *domain.TOTP, err error) (*connect.Response[user.RegisterTOTPResponse], error) {
if err != nil {
return nil, err
}
return &user.RegisterTOTPResponse{
return connect.NewResponse(&user.RegisterTOTPResponse{
Details: object.DomainToDetailsPb(totp.ObjectDetails),
Uri: totp.URI,
Secret: totp.Secret,
}, nil
}), nil
}
func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *user.VerifyTOTPRegistrationRequest) (*user.VerifyTOTPRegistrationResponse, error) {
objectDetails, err := s.command.CheckUserTOTP(ctx, req.GetUserId(), req.GetCode(), "")
func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *connect.Request[user.VerifyTOTPRegistrationRequest]) (*connect.Response[user.VerifyTOTPRegistrationResponse], error) {
objectDetails, err := s.command.CheckUserTOTP(ctx, req.Msg.GetUserId(), req.Msg.GetCode(), "")
if err != nil {
return nil, err
}
return &user.VerifyTOTPRegistrationResponse{
return connect.NewResponse(&user.VerifyTOTPRegistrationResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}), nil
}
func (s *Server) RemoveTOTP(ctx context.Context, req *user.RemoveTOTPRequest) (*user.RemoveTOTPResponse, error) {
objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.GetUserId(), "")
func (s *Server) RemoveTOTP(ctx context.Context, req *connect.Request[user.RemoveTOTPRequest]) (*connect.Response[user.RemoveTOTPResponse], error) {
objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.Msg.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.RemoveTOTPResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
return connect.NewResponse(&user.RemoveTOTPResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil
}

View File

@@ -63,7 +63,7 @@ func Test_totpDetailsToPb(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got, err := totpDetailsToPb(tt.args.otp, tt.args.err)
require.ErrorIs(t, err, tt.wantErr)
if !proto.Equal(tt.want, got) {
if tt.want != nil && !proto.Equal(tt.want, got.Msg) {
t.Errorf("RegisterTOTPResponse =\n%v\nwant\n%v", got, tt.want)
}
})

View File

@@ -3,50 +3,52 @@ package user
import (
"context"
"connectrpc.com/connect"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) {
func (s *Server) RegisterU2F(ctx context.Context, req *connect.Request[user.RegisterU2FRequest]) (*connect.Response[user.RegisterU2FResponse], error) {
return u2fRegistrationDetailsToPb(
s.command.RegisterUserU2F(ctx, req.GetUserId(), "", req.GetDomain()),
s.command.RegisterUserU2F(ctx, req.Msg.GetUserId(), "", req.Msg.GetDomain()),
)
}
func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterU2FResponse, error) {
func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*connect.Response[user.RegisterU2FResponse], error) {
objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err)
if err != nil {
return nil, err
}
return &user.RegisterU2FResponse{
return connect.NewResponse(&user.RegisterU2FResponse{
Details: objectDetails,
U2FId: details.ID,
PublicKeyCredentialCreationOptions: options,
}, nil
}), nil
}
func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) {
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
func (s *Server) VerifyU2FRegistration(ctx context.Context, req *connect.Request[user.VerifyU2FRegistrationRequest]) (*connect.Response[user.VerifyU2FRegistrationResponse], error) {
pkc, err := req.Msg.GetPublicKeyCredential().MarshalJSON()
if err != nil {
return nil, zerrors.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal")
}
objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.GetUserId(), "", req.GetTokenName(), "", pkc)
objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.Msg.GetUserId(), "", req.Msg.GetTokenName(), "", pkc)
if err != nil {
return nil, err
}
return &user.VerifyU2FRegistrationResponse{
return connect.NewResponse(&user.VerifyU2FRegistrationResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}), nil
}
func (s *Server) RemoveU2F(ctx context.Context, req *user.RemoveU2FRequest) (*user.RemoveU2FResponse, error) {
objectDetails, err := s.command.HumanRemoveU2F(ctx, req.GetUserId(), req.GetU2FId(), "")
func (s *Server) RemoveU2F(ctx context.Context, req *connect.Request[user.RemoveU2FRequest]) (*connect.Response[user.RemoveU2FResponse], error) {
objectDetails, err := s.command.HumanRemoveU2F(ctx, req.Msg.GetUserId(), req.Msg.GetU2FId(), "")
if err != nil {
return nil, err
}
return &user.RemoveU2FResponse{
return connect.NewResponse(&user.RemoveU2FResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}), nil
}

View File

@@ -92,11 +92,11 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got, err := u2fRegistrationDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.wantErr)
if !proto.Equal(tt.want, got) {
if tt.want != nil && !proto.Equal(tt.want, got.Msg) {
t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got)
}
if tt.want != nil {
grpc.AllFieldsSet(t, got.ProtoReflect())
grpc.AllFieldsSet(t, got.Msg.ProtoReflect())
}
})
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"io"
"connectrpc.com/connect"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
@@ -11,11 +12,12 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
human, err := AddUserRequestToAddHuman(req)
func (s *Server) AddHumanUser(ctx context.Context, req *connect.Request[user.AddHumanUserRequest]) (_ *connect.Response[user.AddHumanUserResponse], err error) {
human, err := AddUserRequestToAddHuman(req.Msg)
if err != nil {
return nil, err
}
@@ -23,12 +25,12 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
if err = s.command.AddUserHuman(ctx, orgID, human, false, s.userCodeAlg); err != nil {
return nil, err
}
return &user.AddHumanUserResponse{
return connect.NewResponse(&user.AddHumanUserResponse{
UserId: human.ID,
Details: object.DomainToDetailsPb(human.Details),
EmailCode: human.EmailCode,
PhoneCode: human.PhoneCode,
}, nil
}), nil
}
func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
@@ -116,8 +118,8 @@ func genderToDomain(gender user.Gender) domain.Gender {
}
}
func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) {
human, err := UpdateUserRequestToChangeHuman(req)
func (s *Server) UpdateHumanUser(ctx context.Context, req *connect.Request[user.UpdateHumanUserRequest]) (_ *connect.Response[user.UpdateHumanUserResponse], err error) {
human, err := updateHumanUserRequestToChangeHuman(req.Msg)
if err != nil {
return nil, err
}
@@ -125,51 +127,51 @@ func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserR
if err != nil {
return nil, err
}
return &user.UpdateHumanUserResponse{
return connect.NewResponse(&user.UpdateHumanUserResponse{
Details: object.DomainToDetailsPb(human.Details),
EmailCode: human.EmailCode,
PhoneCode: human.PhoneCode,
}, nil
}), nil
}
func (s *Server) LockUser(ctx context.Context, req *user.LockUserRequest) (_ *user.LockUserResponse, err error) {
details, err := s.command.LockUserV2(ctx, req.UserId)
func (s *Server) LockUser(ctx context.Context, req *connect.Request[user.LockUserRequest]) (_ *connect.Response[user.LockUserResponse], err error) {
details, err := s.command.LockUserV2(ctx, req.Msg.GetUserId())
if err != nil {
return nil, err
}
return &user.LockUserResponse{
return connect.NewResponse(&user.LockUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func (s *Server) UnlockUser(ctx context.Context, req *user.UnlockUserRequest) (_ *user.UnlockUserResponse, err error) {
details, err := s.command.UnlockUserV2(ctx, req.UserId)
func (s *Server) UnlockUser(ctx context.Context, req *connect.Request[user.UnlockUserRequest]) (_ *connect.Response[user.UnlockUserResponse], err error) {
details, err := s.command.UnlockUserV2(ctx, req.Msg.GetUserId())
if err != nil {
return nil, err
}
return &user.UnlockUserResponse{
return connect.NewResponse(&user.UnlockUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func (s *Server) DeactivateUser(ctx context.Context, req *user.DeactivateUserRequest) (_ *user.DeactivateUserResponse, err error) {
details, err := s.command.DeactivateUserV2(ctx, req.UserId)
func (s *Server) DeactivateUser(ctx context.Context, req *connect.Request[user.DeactivateUserRequest]) (_ *connect.Response[user.DeactivateUserResponse], err error) {
details, err := s.command.DeactivateUserV2(ctx, req.Msg.GetUserId())
if err != nil {
return nil, err
}
return &user.DeactivateUserResponse{
return connect.NewResponse(&user.DeactivateUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func (s *Server) ReactivateUser(ctx context.Context, req *user.ReactivateUserRequest) (_ *user.ReactivateUserResponse, err error) {
details, err := s.command.ReactivateUserV2(ctx, req.UserId)
func (s *Server) ReactivateUser(ctx context.Context, req *connect.Request[user.ReactivateUserRequest]) (_ *connect.Response[user.ReactivateUserResponse], err error) {
details, err := s.command.ReactivateUserV2(ctx, req.Msg.GetUserId())
if err != nil {
return nil, err
}
return &user.ReactivateUserResponse{
return connect.NewResponse(&user.ReactivateUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p {
@@ -181,98 +183,18 @@ func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p {
return &pVal
}
func UpdateUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) {
email, err := SetHumanEmailToEmail(req.Email, req.GetUserId())
func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.DeleteUserRequest]) (_ *connect.Response[user.DeleteUserResponse], err error) {
memberships, grants, err := s.removeUserDependencies(ctx, req.Msg.GetUserId())
if err != nil {
return nil, err
}
return &command.ChangeHuman{
ID: req.GetUserId(),
Username: req.Username,
Profile: SetHumanProfileToProfile(req.Profile),
Email: email,
Phone: SetHumanPhoneToPhone(req.Phone),
Password: SetHumanPasswordToPassword(req.Password),
}, nil
}
func SetHumanProfileToProfile(profile *user.SetHumanProfile) *command.Profile {
if profile == nil {
return nil
}
var firstName *string
if profile.GivenName != "" {
firstName = &profile.GivenName
}
var lastName *string
if profile.FamilyName != "" {
lastName = &profile.FamilyName
}
return &command.Profile{
FirstName: firstName,
LastName: lastName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make),
Gender: ifNotNilPtr(profile.Gender, genderToDomain),
}
}
func SetHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) {
if email == nil {
return nil, nil
}
var urlTemplate string
if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil {
urlTemplate = *email.GetSendCode().UrlTemplate
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil {
return nil, err
}
}
return &command.Email{
Address: domain.EmailAddress(email.Email),
Verified: email.GetIsVerified(),
ReturnCode: email.GetReturnCode() != nil,
URLTemplate: urlTemplate,
}, nil
}
func SetHumanPhoneToPhone(phone *user.SetHumanPhone) *command.Phone {
if phone == nil {
return nil
}
return &command.Phone{
Number: domain.PhoneNumber(phone.GetPhone()),
Verified: phone.GetIsVerified(),
ReturnCode: phone.GetReturnCode() != nil,
}
}
func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password {
if password == nil {
return nil
}
return &command.Password{
PasswordCode: password.GetVerificationCode(),
OldPassword: password.GetCurrentPassword(),
Password: password.GetPassword().GetPassword(),
EncodedPasswordHash: password.GetHashedPassword().GetHash(),
ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(),
}
}
func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) {
memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId())
details, err := s.command.RemoveUserV2(ctx, req.Msg.GetUserId(), "", memberships, grants...)
if err != nil {
return nil, err
}
details, err := s.command.RemoveUserV2(ctx, req.UserId, "", memberships, grants...)
if err != nil {
return nil, err
}
return &user.DeleteUserResponse{
return connect.NewResponse(&user.DeleteUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
@@ -347,35 +269,35 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string {
return converted
}
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) {
authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true, req.GetDomainQuery().GetIncludeWithoutDomain(), req.GetDomainQuery().GetDomain())
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *connect.Request[user.ListAuthenticationMethodTypesRequest]) (*connect.Response[user.ListAuthenticationMethodTypesResponse], error) {
authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.Msg.GetUserId(), true, req.Msg.GetDomainQuery().GetIncludeWithoutDomain(), req.Msg.GetDomainQuery().GetDomain())
if err != nil {
return nil, err
}
return &user.ListAuthenticationMethodTypesResponse{
return connect.NewResponse(&user.ListAuthenticationMethodTypesResponse{
Details: object.ToListDetails(authMethods.SearchResponse),
AuthMethodTypes: authMethodTypesToPb(authMethods.AuthMethodTypes),
}, nil
}), nil
}
func (s *Server) ListAuthenticationFactors(ctx context.Context, req *user.ListAuthenticationFactorsRequest) (*user.ListAuthenticationFactorsResponse, error) {
func (s *Server) ListAuthenticationFactors(ctx context.Context, req *connect.Request[user.ListAuthenticationFactorsRequest]) (*connect.Response[user.ListAuthenticationFactorsResponse], error) {
query := new(query.UserAuthMethodSearchQueries)
if err := query.AppendUserIDQuery(req.UserId); err != nil {
if err := query.AppendUserIDQuery(req.Msg.GetUserId()); err != nil {
return nil, err
}
authMethodsType := []domain.UserAuthMethodType{domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPSMS, domain.UserAuthMethodTypeOTPEmail}
if len(req.GetAuthFactors()) > 0 {
authMethodsType = object.AuthFactorsToPb(req.GetAuthFactors())
if len(req.Msg.GetAuthFactors()) > 0 {
authMethodsType = object.AuthFactorsToPb(req.Msg.GetAuthFactors())
}
if err := query.AppendAuthMethodsQuery(authMethodsType...); err != nil {
return nil, err
}
states := []domain.MFAState{domain.MFAStateReady}
if len(req.GetStates()) > 0 {
states = object.AuthFactorStatesToPb(req.GetStates())
if len(req.Msg.GetStates()) > 0 {
states = object.AuthFactorStatesToPb(req.Msg.GetStates())
}
if err := query.AppendStatesQuery(states...); err != nil {
return nil, err
@@ -386,9 +308,9 @@ func (s *Server) ListAuthenticationFactors(ctx context.Context, req *user.ListAu
return nil, err
}
return &user.ListAuthenticationFactorsResponse{
return connect.NewResponse(&user.ListAuthenticationFactorsResponse{
Result: object.AuthMethodsToPb(authMethods),
}, nil
}), nil
}
func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType {
@@ -422,8 +344,8 @@ func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.Authenticatio
}
}
func (s *Server) CreateInviteCode(ctx context.Context, req *user.CreateInviteCodeRequest) (*user.CreateInviteCodeResponse, error) {
invite, err := createInviteCodeRequestToCommand(req)
func (s *Server) CreateInviteCode(ctx context.Context, req *connect.Request[user.CreateInviteCodeRequest]) (*connect.Response[user.CreateInviteCodeResponse], error) {
invite, err := createInviteCodeRequestToCommand(req.Msg)
if err != nil {
return nil, err
}
@@ -431,30 +353,30 @@ func (s *Server) CreateInviteCode(ctx context.Context, req *user.CreateInviteCod
if err != nil {
return nil, err
}
return &user.CreateInviteCodeResponse{
return connect.NewResponse(&user.CreateInviteCodeResponse{
Details: object.DomainToDetailsPb(details),
InviteCode: code,
}, nil
}), nil
}
func (s *Server) ResendInviteCode(ctx context.Context, req *user.ResendInviteCodeRequest) (*user.ResendInviteCodeResponse, error) {
details, err := s.command.ResendInviteCode(ctx, req.GetUserId(), "", "")
func (s *Server) ResendInviteCode(ctx context.Context, req *connect.Request[user.ResendInviteCodeRequest]) (*connect.Response[user.ResendInviteCodeResponse], error) {
details, err := s.command.ResendInviteCode(ctx, req.Msg.GetUserId(), "", "")
if err != nil {
return nil, err
}
return &user.ResendInviteCodeResponse{
return connect.NewResponse(&user.ResendInviteCodeResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func (s *Server) VerifyInviteCode(ctx context.Context, req *user.VerifyInviteCodeRequest) (*user.VerifyInviteCodeResponse, error) {
details, err := s.command.VerifyInviteCode(ctx, req.GetUserId(), req.GetVerificationCode())
func (s *Server) VerifyInviteCode(ctx context.Context, req *connect.Request[user.VerifyInviteCodeRequest]) (*connect.Response[user.VerifyInviteCodeResponse], error) {
details, err := s.command.VerifyInviteCode(ctx, req.Msg.GetUserId(), req.Msg.GetVerificationCode())
if err != nil {
return nil, err
}
return &user.VerifyInviteCodeResponse{
return connect.NewResponse(&user.VerifyInviteCodeResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func createInviteCodeRequestToCommand(req *user.CreateInviteCodeRequest) (*command.CreateUserInvite, error) {
@@ -473,12 +395,34 @@ func createInviteCodeRequestToCommand(req *user.CreateInviteCodeRequest) (*comma
}
}
func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *user.HumanMFAInitSkippedRequest) (_ *user.HumanMFAInitSkippedResponse, err error) {
details, err := s.command.HumanMFAInitSkippedV2(ctx, req.UserId)
func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *connect.Request[user.HumanMFAInitSkippedRequest]) (_ *connect.Response[user.HumanMFAInitSkippedResponse], err error) {
details, err := s.command.HumanMFAInitSkippedV2(ctx, req.Msg.GetUserId())
if err != nil {
return nil, err
}
return &user.HumanMFAInitSkippedResponse{
return connect.NewResponse(&user.HumanMFAInitSkippedResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}), nil
}
func (s *Server) CreateUser(ctx context.Context, req *connect.Request[user.CreateUserRequest]) (*connect.Response[user.CreateUserResponse], error) {
switch userType := req.Msg.GetUserType().(type) {
case *user.CreateUserRequest_Human_:
return s.createUserTypeHuman(ctx, userType.Human, req.Msg.GetOrganizationId(), req.Msg.Username, req.Msg.UserId)
case *user.CreateUserRequest_Machine_:
return s.createUserTypeMachine(ctx, userType.Machine, req.Msg.GetOrganizationId(), req.Msg.GetUsername(), req.Msg.GetUserId())
default:
return nil, zerrors.ThrowInternal(nil, "", "user type is not implemented")
}
}
func (s *Server) UpdateUser(ctx context.Context, req *connect.Request[user.UpdateUserRequest]) (*connect.Response[user.UpdateUserResponse], error) {
switch userType := req.Msg.GetUserType().(type) {
case *user.UpdateUserRequest_Human_:
return s.updateUserTypeHuman(ctx, userType.Human, req.Msg.GetUserId(), req.Msg.Username)
case *user.UpdateUserRequest_Machine_:
return s.updateUserTypeMachine(ctx, userType.Machine, req.Msg.GetUserId(), req.Msg.Username)
default:
return nil, zerrors.ThrowUnimplemented(nil, "", "user type is not implemented")
}
}

View File

@@ -3,6 +3,7 @@ package user
import (
"context"
"connectrpc.com/connect"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -13,12 +14,12 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) {
resp, err := s.query.GetUserByIDWithPermission(ctx, true, req.GetUserId(), s.checkPermission)
func (s *Server) GetUserByID(ctx context.Context, req *connect.Request[user.GetUserByIDRequest]) (_ *connect.Response[user.GetUserByIDResponse], err error) {
resp, err := s.query.GetUserByIDWithPermission(ctx, true, req.Msg.GetUserId(), s.checkPermission)
if err != nil {
return nil, err
}
return &user.GetUserByIDResponse{
return connect.NewResponse(&user.GetUserByIDResponse{
Details: object.DomainToDetailsPb(&domain.ObjectDetails{
Sequence: resp.Sequence,
CreationDate: resp.CreationDate,
@@ -26,22 +27,22 @@ func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest)
ResourceOwner: resp.ResourceOwner,
}),
User: userToPb(resp, s.assetAPIPrefix(ctx)),
}, nil
}), nil
}
func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) {
queries, filterOrgId, err := listUsersRequestToModel(req)
func (s *Server) ListUsers(ctx context.Context, req *connect.Request[user.ListUsersRequest]) (*connect.Response[user.ListUsersResponse], error) {
queries, err := listUsersRequestToModel(req.Msg)
if err != nil {
return nil, err
}
res, err := s.query.SearchUsers(ctx, queries, filterOrgId, s.checkPermission)
res, err := s.query.SearchUsers(ctx, queries, s.checkPermission)
if err != nil {
return nil, err
}
return &user.ListUsersResponse{
return connect.NewResponse(&user.ListUsersResponse{
Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)),
Details: object.ToListDetails(res.SearchResponse),
}, nil
}), nil
}
func UsersToPb(users []*query.User, assetPrefix string) []*user.User {
@@ -171,11 +172,11 @@ func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenT
}
}
func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, string, error) {
func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, filterOrgId, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/)
queries, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/)
if err != nil {
return nil, "", err
return nil, err
}
return &query.UserSearchQueries{
SearchRequest: query.SearchRequest{
@@ -185,7 +186,7 @@ func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueri
SortingColumn: userFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, filterOrgId, nil
}, nil
}
func userFieldNameToSortingColumn(field user.UserFieldName) query.Column {
@@ -215,18 +216,15 @@ func userFieldNameToSortingColumn(field user.UserFieldName) query.Column {
}
}
func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, filterOrgId string, err error) {
func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
if orgFilter := query.GetOrganizationIdQuery(); orgFilter != nil {
filterOrgId = orgFilter.OrganizationId
}
q[i], err = userQueryToQuery(query, level)
if err != nil {
return nil, filterOrgId, err
return nil, err
}
}
return q, filterOrgId, nil
return q, nil
}
func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) {
@@ -320,14 +318,14 @@ func inUserIdsQueryToQuery(q *user.InUserIDQuery) (query.SearchQuery, error) {
return query.NewUserInUserIdsSearchQuery(q.UserIds)
}
func orQueryToQuery(q *user.OrQuery, level uint8) (query.SearchQuery, error) {
mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1)
mappedQueries, err := userQueriesToQuery(q.Queries, level+1)
if err != nil {
return nil, err
}
return query.NewUserOrSearchQuery(mappedQueries)
}
func andQueryToQuery(q *user.AndQuery, level uint8) (query.SearchQuery, error) {
mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1)
mappedQueries, err := userQueriesToQuery(q.Queries, level+1)
if err != nil {
return nil, err
}