2023-04-26 05:47:57 +00:00
|
|
|
package user
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-12-08 14:30:55 +00:00
|
|
|
"errors"
|
2023-04-26 05:47:57 +00:00
|
|
|
"io"
|
|
|
|
|
|
|
|
"golang.org/x/text/language"
|
2023-06-20 12:39:50 +00:00
|
|
|
"google.golang.org/protobuf/types/known/structpb"
|
2023-05-24 18:29:58 +00:00
|
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
2023-04-26 05:47:57 +00:00
|
|
|
|
2023-04-26 14:19:32 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
2023-04-26 05:47:57 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
|
|
|
"github.com/zitadel/zitadel/internal/command"
|
2023-05-24 18:29:58 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
2023-04-26 05:47:57 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
2023-08-16 11:29:57 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/idp"
|
|
|
|
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
|
|
|
"github.com/zitadel/zitadel/internal/query"
|
2023-12-08 14:30:55 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
2023-09-13 12:43:01 +00:00
|
|
|
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
|
|
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
2023-04-26 05:47:57 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
2023-08-11 14:19:14 +00:00
|
|
|
human, err := AddUserRequestToAddHuman(req)
|
2023-04-26 05:47:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-05-05 15:34:53 +00:00
|
|
|
orgID := authz.GetCtxData(ctx).OrgID
|
2023-12-21 09:03:37 +00:00
|
|
|
if err = s.command.AddUserHuman(ctx, orgID, human, false, s.userCodeAlg); err != nil {
|
2023-04-26 05:47:57 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.AddHumanUserResponse{
|
|
|
|
UserId: human.ID,
|
|
|
|
Details: object.DomainToDetailsPb(human.Details),
|
|
|
|
EmailCode: human.EmailCode,
|
2023-08-03 04:42:59 +00:00
|
|
|
PhoneCode: human.PhoneCode,
|
2023-04-26 05:47:57 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-08-11 14:19:14 +00:00
|
|
|
func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
|
2023-04-26 05:47:57 +00:00
|
|
|
username := req.GetUsername()
|
|
|
|
if username == "" {
|
|
|
|
username = req.GetEmail().GetEmail()
|
|
|
|
}
|
|
|
|
var urlTemplate string
|
|
|
|
if req.GetEmail().GetSendCode() != nil {
|
|
|
|
urlTemplate = req.GetEmail().GetSendCode().GetUrlTemplate()
|
|
|
|
// test the template execution so the async notification will not fail because of it and the user won't realize
|
|
|
|
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired()
|
|
|
|
metadata := make([]*command.AddMetadataEntry, len(req.Metadata))
|
|
|
|
for i, metadataEntry := range req.Metadata {
|
|
|
|
metadata[i] = &command.AddMetadataEntry{
|
|
|
|
Key: metadataEntry.GetKey(),
|
|
|
|
Value: metadataEntry.GetValue(),
|
|
|
|
}
|
|
|
|
}
|
2023-05-24 18:29:58 +00:00
|
|
|
links := make([]*command.AddLink, len(req.GetIdpLinks()))
|
|
|
|
for i, link := range req.GetIdpLinks() {
|
|
|
|
links[i] = &command.AddLink{
|
|
|
|
IDPID: link.GetIdpId(),
|
2023-06-20 12:39:50 +00:00
|
|
|
IDPExternalID: link.GetUserId(),
|
|
|
|
DisplayName: link.GetUserName(),
|
2023-05-24 18:29:58 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-26 05:47:57 +00:00
|
|
|
return &command.AddHuman{
|
|
|
|
ID: req.GetUserId(),
|
|
|
|
Username: username,
|
2023-08-22 10:05:45 +00:00
|
|
|
FirstName: req.GetProfile().GetGivenName(),
|
|
|
|
LastName: req.GetProfile().GetFamilyName(),
|
2023-04-26 05:47:57 +00:00
|
|
|
NickName: req.GetProfile().GetNickName(),
|
|
|
|
DisplayName: req.GetProfile().GetDisplayName(),
|
|
|
|
Email: command.Email{
|
|
|
|
Address: domain.EmailAddress(req.GetEmail().GetEmail()),
|
|
|
|
Verified: req.GetEmail().GetIsVerified(),
|
|
|
|
ReturnCode: req.GetEmail().GetReturnCode() != nil,
|
|
|
|
URLTemplate: urlTemplate,
|
|
|
|
},
|
2023-08-03 04:42:59 +00:00
|
|
|
Phone: command.Phone{
|
|
|
|
Number: domain.PhoneNumber(req.GetPhone().GetPhone()),
|
|
|
|
Verified: req.GetPhone().GetIsVerified(),
|
|
|
|
ReturnCode: req.GetPhone().GetReturnCode() != nil,
|
|
|
|
},
|
2023-04-26 05:47:57 +00:00
|
|
|
PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()),
|
|
|
|
Gender: genderToDomain(req.GetProfile().GetGender()),
|
|
|
|
Password: req.GetPassword().GetPassword(),
|
2023-07-14 06:49:57 +00:00
|
|
|
EncodedPasswordHash: req.GetHashedPassword().GetHash(),
|
2023-04-26 05:47:57 +00:00
|
|
|
PasswordChangeRequired: passwordChangeRequired,
|
|
|
|
Passwordless: false,
|
|
|
|
Register: false,
|
|
|
|
Metadata: metadata,
|
2023-05-24 18:29:58 +00:00
|
|
|
Links: links,
|
2023-04-26 05:47:57 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func genderToDomain(gender user.Gender) domain.Gender {
|
|
|
|
switch gender {
|
|
|
|
case user.Gender_GENDER_UNSPECIFIED:
|
|
|
|
return domain.GenderUnspecified
|
|
|
|
case user.Gender_GENDER_FEMALE:
|
|
|
|
return domain.GenderFemale
|
|
|
|
case user.Gender_GENDER_MALE:
|
|
|
|
return domain.GenderMale
|
|
|
|
case user.Gender_GENDER_DIVERSE:
|
|
|
|
return domain.GenderDiverse
|
|
|
|
default:
|
|
|
|
return domain.GenderUnspecified
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-21 09:03:37 +00:00
|
|
|
func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) {
|
|
|
|
human, err := UpdateUserRequestToChangeHuman(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = s.command.ChangeUserHuman(ctx, human, s.userCodeAlg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.UpdateHumanUserResponse{
|
|
|
|
Details: object.DomainToDetailsPb(human.Details),
|
|
|
|
EmailCode: human.EmailCode,
|
|
|
|
PhoneCode: human.PhoneCode,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) LockUser(ctx context.Context, req *user.LockUserRequest) (_ *user.LockUserResponse, err error) {
|
|
|
|
details, err := s.command.LockUserV2(ctx, req.UserId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.LockUserResponse{
|
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) UnlockUser(ctx context.Context, req *user.UnlockUserRequest) (_ *user.UnlockUserResponse, err error) {
|
|
|
|
details, err := s.command.UnlockUserV2(ctx, req.UserId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.UnlockUserResponse{
|
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) DeactivateUser(ctx context.Context, req *user.DeactivateUserRequest) (_ *user.DeactivateUserResponse, err error) {
|
|
|
|
details, err := s.command.DeactivateUserV2(ctx, req.UserId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.DeactivateUserResponse{
|
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) ReactivateUser(ctx context.Context, req *user.ReactivateUserRequest) (_ *user.ReactivateUserResponse, err error) {
|
|
|
|
details, err := s.command.ReactivateUserV2(ctx, req.UserId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.ReactivateUserResponse{
|
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p {
|
|
|
|
var pNil *p
|
|
|
|
if value == nil {
|
|
|
|
return pNil
|
|
|
|
}
|
|
|
|
pVal := conv(*value)
|
|
|
|
return &pVal
|
|
|
|
}
|
|
|
|
|
|
|
|
func UpdateUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) {
|
|
|
|
email, err := SetHumanEmailToEmail(req.Email, req.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
|
|
|
|
}
|
|
|
|
var changeRequired bool
|
|
|
|
var passwordStr *string
|
|
|
|
if password.GetPassword() != nil {
|
|
|
|
passwordStr = &password.GetPassword().Password
|
|
|
|
changeRequired = password.GetPassword().GetChangeRequired()
|
|
|
|
}
|
|
|
|
var hash *string
|
|
|
|
if password.GetHashedPassword() != nil {
|
|
|
|
hash = &password.GetHashedPassword().Hash
|
|
|
|
changeRequired = password.GetHashedPassword().GetChangeRequired()
|
|
|
|
}
|
|
|
|
var code *string
|
|
|
|
if password.GetVerificationCode() != "" {
|
|
|
|
codeT := password.GetVerificationCode()
|
|
|
|
code = &codeT
|
|
|
|
}
|
|
|
|
var oldPassword *string
|
|
|
|
if password.GetCurrentPassword() != "" {
|
|
|
|
oldPasswordT := password.GetCurrentPassword()
|
|
|
|
oldPassword = &oldPasswordT
|
|
|
|
}
|
|
|
|
return &command.Password{
|
|
|
|
PasswordCode: code,
|
|
|
|
OldPassword: oldPassword,
|
|
|
|
Password: passwordStr,
|
|
|
|
EncodedPasswordHash: hash,
|
|
|
|
ChangeRequired: changeRequired,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-24 18:29:58 +00:00
|
|
|
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
|
|
|
|
orgID := authz.GetCtxData(ctx).OrgID
|
2023-08-23 11:55:52 +00:00
|
|
|
details, err := s.command.AddUserIDPLink(ctx, req.UserId, orgID, &command.AddLink{
|
|
|
|
IDPID: req.GetIdpLink().GetIdpId(),
|
|
|
|
DisplayName: req.GetIdpLink().GetUserName(),
|
|
|
|
IDPExternalID: req.GetIdpLink().GetUserId(),
|
2023-05-24 18:29:58 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.AddIDPLinkResponse{
|
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-12-21 09:03:37 +00:00
|
|
|
func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) {
|
|
|
|
memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId())
|
|
|
|
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{
|
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
|
|
|
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
|
|
|
Queries: []query.SearchQuery{userGrantUserQuery},
|
2024-01-08 15:26:30 +00:00
|
|
|
}, true)
|
2023-12-21 09:03:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{
|
|
|
|
Queries: []query.SearchQuery{membershipsUserQuery},
|
|
|
|
}, false)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership {
|
|
|
|
cascades := make([]*command.CascadingMembership, len(memberships))
|
|
|
|
for i, membership := range memberships {
|
|
|
|
cascades[i] = &command.CascadingMembership{
|
|
|
|
UserID: membership.UserID,
|
|
|
|
ResourceOwner: membership.ResourceOwner,
|
|
|
|
IAM: cascadingIAMMembership(membership.IAM),
|
|
|
|
Org: cascadingOrgMembership(membership.Org),
|
|
|
|
Project: cascadingProjectMembership(membership.Project),
|
|
|
|
ProjectGrant: cascadingProjectGrantMembership(membership.ProjectGrant),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return cascades
|
|
|
|
}
|
|
|
|
|
|
|
|
func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingIAMMembership {
|
|
|
|
if membership == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return &command.CascadingIAMMembership{IAMID: membership.IAMID}
|
|
|
|
}
|
|
|
|
func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership {
|
|
|
|
if membership == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return &command.CascadingOrgMembership{OrgID: membership.OrgID}
|
|
|
|
}
|
|
|
|
func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership {
|
|
|
|
if membership == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return &command.CascadingProjectMembership{ProjectID: membership.ProjectID}
|
|
|
|
}
|
|
|
|
func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership {
|
|
|
|
if membership == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return &command.CascadingProjectGrantMembership{ProjectID: membership.ProjectID, GrantID: membership.GrantID}
|
|
|
|
}
|
|
|
|
|
|
|
|
func userGrantsToIDs(userGrants []*query.UserGrant) []string {
|
|
|
|
converted := make([]string, len(userGrants))
|
|
|
|
for i, grant := range userGrants {
|
|
|
|
converted[i] = grant.ID
|
|
|
|
}
|
|
|
|
return converted
|
|
|
|
}
|
|
|
|
|
2023-08-22 10:05:45 +00:00
|
|
|
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) {
|
2023-08-16 11:29:57 +00:00
|
|
|
switch t := req.GetContent().(type) {
|
2023-08-22 10:05:45 +00:00
|
|
|
case *user.StartIdentityProviderIntentRequest_Urls:
|
2023-08-16 11:29:57 +00:00
|
|
|
return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls)
|
2023-08-22 10:05:45 +00:00
|
|
|
case *user.StartIdentityProviderIntentRequest_Ldap:
|
2023-08-16 11:29:57 +00:00
|
|
|
return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap)
|
|
|
|
default:
|
2023-12-08 14:30:55 +00:00
|
|
|
return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t)
|
2023-08-16 11:29:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-22 10:05:45 +00:00
|
|
|
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) {
|
2023-08-16 11:29:57 +00:00
|
|
|
intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetCtxData(ctx).OrgID)
|
2023-05-24 18:29:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-09-29 09:26:14 +00:00
|
|
|
content, redirect, err := s.command.AuthFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
|
2023-05-24 18:29:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-09-29 09:26:14 +00:00
|
|
|
if redirect {
|
|
|
|
return &user.StartIdentityProviderIntentResponse{
|
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
|
|
|
|
}, nil
|
|
|
|
} else {
|
|
|
|
return &user.StartIdentityProviderIntentResponse{
|
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
|
|
|
|
PostForm: []byte(content),
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
2023-05-24 18:29:58 +00:00
|
|
|
}
|
|
|
|
|
2023-08-22 10:05:45 +00:00
|
|
|
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {
|
2023-08-16 11:29:57 +00:00
|
|
|
intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetCtxData(ctx).OrgID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
|
|
|
|
if err != nil {
|
|
|
|
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-22 10:05:45 +00:00
|
|
|
return &user.StartIdentityProviderIntentResponse{
|
2023-09-25 05:21:50 +00:00
|
|
|
Details: object.DomainToDetailsPb(details),
|
|
|
|
NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{
|
|
|
|
IdpIntent: &user.IDPIntent{
|
|
|
|
IdpIntentId: intentWriteModel.AggregateID,
|
|
|
|
IdpIntentToken: token,
|
|
|
|
UserId: userID,
|
|
|
|
},
|
|
|
|
},
|
2023-08-16 11:29:57 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) {
|
|
|
|
idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
queries := []query.SearchQuery{
|
|
|
|
idQuery, externalIDQuery,
|
|
|
|
}
|
|
|
|
links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if len(links.Links) == 1 {
|
|
|
|
return links.Links[0].UserID, nil
|
|
|
|
}
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) {
|
2023-09-29 09:26:14 +00:00
|
|
|
provider, err := s.command.GetProvider(ctx, idpID, "", "")
|
2023-08-16 11:29:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, "", nil, err
|
|
|
|
}
|
|
|
|
ldapProvider, ok := provider.(*ldap.Provider)
|
|
|
|
if !ok {
|
2023-12-08 14:30:55 +00:00
|
|
|
return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
2023-08-16 11:29:57 +00:00
|
|
|
}
|
|
|
|
session := ldapProvider.GetSession(username, password)
|
|
|
|
externalUser, err := session.FetchUser(ctx)
|
2023-12-08 14:30:55 +00:00
|
|
|
if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) {
|
|
|
|
return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed")
|
2023-08-16 11:29:57 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", nil, err
|
|
|
|
}
|
|
|
|
userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID())
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
attributes := make(map[string][]string, 0)
|
|
|
|
for _, item := range session.Entry.Attributes {
|
|
|
|
attributes[item.Name] = item.Values
|
|
|
|
}
|
|
|
|
return externalUser, userID, attributes, nil
|
|
|
|
}
|
|
|
|
|
2023-08-22 10:05:45 +00:00
|
|
|
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
|
|
|
|
intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), authz.GetCtxData(ctx).OrgID)
|
2023-05-24 18:29:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-22 10:05:45 +00:00
|
|
|
if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil {
|
2023-05-24 18:29:58 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if intent.State != domain.IDPIntentStateSucceeded {
|
2023-12-08 14:30:55 +00:00
|
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-Hk38e", "Errors.Intent.NotSucceeded")
|
2023-05-24 18:29:58 +00:00
|
|
|
}
|
2023-08-22 10:05:45 +00:00
|
|
|
return idpIntentToIDPIntentPb(intent, s.idpAlg)
|
2023-05-24 18:29:58 +00:00
|
|
|
}
|
|
|
|
|
2023-08-22 10:05:45 +00:00
|
|
|
func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
|
2023-06-20 12:39:50 +00:00
|
|
|
rawInformation := new(structpb.Struct)
|
|
|
|
err = rawInformation.UnmarshalJSON(intent.IDPUser)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-22 10:05:45 +00:00
|
|
|
information := &user.RetrieveIdentityProviderIntentResponse{
|
2023-08-16 11:29:57 +00:00
|
|
|
Details: intentToDetailsPb(intent),
|
2023-05-24 18:29:58 +00:00
|
|
|
IdpInformation: &user.IDPInformation{
|
2023-06-20 12:39:50 +00:00
|
|
|
IdpId: intent.IDPID,
|
|
|
|
UserId: intent.IDPUserID,
|
|
|
|
UserName: intent.IDPUserName,
|
|
|
|
RawInformation: rawInformation,
|
2023-05-24 18:29:58 +00:00
|
|
|
},
|
2023-09-25 05:21:50 +00:00
|
|
|
UserId: intent.UserID,
|
2023-08-16 11:29:57 +00:00
|
|
|
}
|
|
|
|
if intent.IDPIDToken != "" || intent.IDPAccessToken != nil {
|
|
|
|
information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if intent.IDPEntryAttributes != nil {
|
|
|
|
access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
information.IdpInformation.Access = access
|
|
|
|
}
|
|
|
|
|
2023-09-29 09:26:14 +00:00
|
|
|
if intent.Assertion != nil {
|
|
|
|
assertion, err := crypto.Decrypt(intent.Assertion, alg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
information.IdpInformation.Access = IDPSAMLResponseToPb(assertion)
|
|
|
|
}
|
|
|
|
|
2023-08-16 11:29:57 +00:00
|
|
|
return information, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) {
|
|
|
|
var idToken *string
|
|
|
|
if idpIDToken != "" {
|
|
|
|
idToken = &idpIDToken
|
|
|
|
}
|
|
|
|
var accessToken string
|
|
|
|
if idpAccessToken != nil {
|
|
|
|
accessToken, err = crypto.DecryptString(idpAccessToken, alg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &user.IDPInformation_Oauth{
|
|
|
|
Oauth: &user.IDPOAuthAccessInformation{
|
|
|
|
AccessToken: accessToken,
|
|
|
|
IdToken: idToken,
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details {
|
|
|
|
return &object_pb.Details{
|
|
|
|
Sequence: intent.ProcessedSequence,
|
|
|
|
ChangeDate: timestamppb.New(intent.ChangeDate),
|
|
|
|
ResourceOwner: intent.ResourceOwner,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) {
|
|
|
|
values := make(map[string]interface{}, 0)
|
|
|
|
for k, v := range entryAttributes {
|
|
|
|
intValues := make([]interface{}, len(v))
|
|
|
|
for i, value := range v {
|
|
|
|
intValues[i] = value
|
|
|
|
}
|
|
|
|
values[k] = intValues
|
|
|
|
}
|
|
|
|
attributes, err := structpb.NewStruct(values)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.IDPInformation_Ldap{
|
|
|
|
Ldap: &user.IDPLDAPAccessInformation{
|
|
|
|
Attributes: attributes,
|
|
|
|
},
|
2023-05-24 18:29:58 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-09-29 09:26:14 +00:00
|
|
|
func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml {
|
|
|
|
return &user.IDPInformation_Saml{
|
|
|
|
Saml: &user.IDPSAMLAccessInformation{
|
|
|
|
Assertion: assertion,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-24 18:29:58 +00:00
|
|
|
func (s *Server) checkIntentToken(token string, intentID string) error {
|
2023-06-21 14:06:18 +00:00
|
|
|
return crypto.CheckToken(s.idpAlg, token, intentID)
|
2023-05-24 18:29:58 +00:00
|
|
|
}
|
2023-06-20 16:23:28 +00:00
|
|
|
|
|
|
|
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) {
|
2023-11-21 12:11:38 +00:00
|
|
|
authMethods, err := s.query.ListActiveUserAuthMethodTypes(ctx, req.GetUserId())
|
2023-06-20 16:23:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &user.ListAuthenticationMethodTypesResponse{
|
|
|
|
Details: object.ToListDetails(authMethods.SearchResponse),
|
|
|
|
AuthMethodTypes: authMethodTypesToPb(authMethods.AuthMethodTypes),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType {
|
|
|
|
methods := make([]user.AuthenticationMethodType, len(methodTypes))
|
|
|
|
for i, method := range methodTypes {
|
|
|
|
methods[i] = authMethodTypeToPb(method)
|
|
|
|
}
|
|
|
|
return methods
|
|
|
|
}
|
|
|
|
|
|
|
|
func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.AuthenticationMethodType {
|
|
|
|
switch methodType {
|
2023-08-02 16:57:53 +00:00
|
|
|
case domain.UserAuthMethodTypeTOTP:
|
2023-06-20 16:23:28 +00:00
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP
|
|
|
|
case domain.UserAuthMethodTypeU2F:
|
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F
|
|
|
|
case domain.UserAuthMethodTypePasswordless:
|
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY
|
|
|
|
case domain.UserAuthMethodTypePassword:
|
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD
|
|
|
|
case domain.UserAuthMethodTypeIDP:
|
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP
|
2023-08-02 16:57:53 +00:00
|
|
|
case domain.UserAuthMethodTypeOTPSMS:
|
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_SMS
|
|
|
|
case domain.UserAuthMethodTypeOTPEmail:
|
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_EMAIL
|
2023-06-20 16:23:28 +00:00
|
|
|
case domain.UserAuthMethodTypeUnspecified:
|
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
|
|
|
|
default:
|
|
|
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
|
|
|
|
}
|
|
|
|
}
|