Merge branch 'main' into grcp-server-reflect

This commit is contained in:
Tim Möhlmann
2023-05-07 16:47:52 +02:00
107 changed files with 5654 additions and 173 deletions

View File

@@ -0,0 +1,36 @@
//go:build integration
package admin_test
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
var (
Tester *integration.Tester
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
return m.Run()
}())
}
func TestServer_Healthz(t *testing.T) {
client := admin.NewAdminServiceClient(Tester.GRPCClientConn)
_, err := client.Healthz(context.TODO(), &admin.HealthzRequest{})
require.NoError(t, err)
}

View File

@@ -4,6 +4,7 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
)
@@ -17,3 +18,21 @@ func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
}
return details
}
func ToListDetails(response query.SearchResponse) *object.ListDetails {
details := &object.ListDetails{
TotalResult: response.Count,
ProcessedSequence: response.Sequence,
}
if !response.Timestamp.IsZero() {
details.Timestamp = timestamppb.New(response.Timestamp)
}
return details
}
func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) {
if query == nil {
return 0, 0, false
}
return query.Offset, uint64(query.Limit), query.Asc
}

View File

@@ -12,6 +12,7 @@ import (
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
client_middleware "github.com/zitadel/zitadel/internal/api/grpc/client/middleware"
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
@@ -38,6 +39,7 @@ var (
runtime.WithMarshalerOption(runtime.MIMEWildcard, jsonMarshaler),
runtime.WithIncomingHeaderMatcher(headerMatcher),
runtime.WithOutgoingHeaderMatcher(runtime.DefaultHeaderMatcher),
runtime.WithForwardResponseOption(responseForwarder),
}
headerMatcher = runtime.HeaderMatcherFunc(
@@ -50,6 +52,15 @@ var (
return runtime.DefaultHeaderMatcher(header)
},
)
responseForwarder = func(ctx context.Context, w http.ResponseWriter, resp proto.Message) error {
t, ok := resp.(CustomHTTPResponse)
if ok {
// TODO: find a way to return a location header if needed w.Header().Set("location", t.Location())
w.WriteHeader(t.CustomHTTPCode())
}
return nil
}
)
type Gateway struct {
@@ -62,6 +73,10 @@ func (g *Gateway) Handler() http.Handler {
return addInterceptors(g.mux, g.http1HostName)
}
type CustomHTTPResponse interface {
CustomHTTPCode() int
}
type RegisterGatewayFunc func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error
func CreateGatewayWithPrefix(ctx context.Context, g WithGatewayPrefix, port uint16, http1HostName string) (http.Handler, string, error) {
@@ -134,6 +149,7 @@ func addInterceptors(handler http.Handler, http1HostName string) http.Handler {
handler = http_mw.CallDurationHandler(handler)
handler = http1Host(handler, http1HostName)
handler = http_mw.CORSInterceptor(handler)
handler = http_mw.RobotsTagHandler(handler)
handler = http_mw.DefaultTelemetryHandler(handler)
return http_mw.DefaultMetricsHandler(handler)
}

View File

@@ -11,6 +11,7 @@ import (
grpc_util "github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
)
func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor {
@@ -33,12 +34,14 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
return nil, status.Error(codes.Unauthenticated, "auth header missing")
}
var orgDomain string
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
if o, ok := req.(AuthContext); ok {
orgID = o.AuthContext()
if o, ok := req.(OrganisationFromRequest); ok {
orgID = o.OrganisationFromRequest().GetOrgId()
orgDomain = o.OrganisationFromRequest().GetOrgDomain()
}
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod)
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod)
if err != nil {
return nil, err
}
@@ -46,6 +49,6 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
return handler(ctxSetter(ctx), req)
}
type AuthContext interface {
AuthContext() string
type OrganisationFromRequest interface {
OrganisationFromRequest() *object.Organisation
}

View File

@@ -31,8 +31,8 @@ func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string) ([
func (v *verifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *verifierMock) ExistsOrg(ctx context.Context, orgID string) error {
return nil
func (v *verifierMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
return orgID, nil
}
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "", "", nil

View File

@@ -6,16 +6,18 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
)
var _ session.SessionServiceServer = (*Server)(nil)
type Server struct {
session.UnimplementedSessionServiceServer
command *command.Commands
query *query.Queries
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
}
type Config struct{}
@@ -23,10 +25,12 @@ type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
command: command,
query: query,
checkPermission: checkPermission,
}
}

View File

@@ -3,16 +3,260 @@ package session
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
)
func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) {
res, err := s.query.SessionByID(ctx, req.GetSessionId(), req.GetSessionToken())
if err != nil {
return nil, err
}
return &session.GetSessionResponse{
Session: &session.Session{
Id: req.Id,
User: &user.User{Id: authz.GetCtxData(ctx).UserID},
},
Session: sessionToPb(res),
}, nil
}
func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequest) (*session.ListSessionsResponse, error) {
queries, err := listSessionsRequestToQuery(ctx, req)
if err != nil {
return nil, err
}
sessions, err := s.query.SearchSessions(ctx, queries)
if err != nil {
return nil, err
}
return &session.ListSessionsResponse{
Details: object.ToListDetails(sessions.SearchResponse),
Sessions: sessionsToPb(sessions.Sessions),
}, nil
}
func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) {
checks, metadata, err := s.createSessionRequestToCommand(ctx, req)
if err != nil {
return nil, err
}
set, err := s.command.CreateSession(ctx, checks, metadata)
if err != nil {
return nil, err
}
return &session.CreateSessionResponse{
Details: object.DomainToDetailsPb(set.ObjectDetails),
SessionId: set.ID,
SessionToken: set.NewToken,
}, nil
}
func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest) (*session.SetSessionResponse, error) {
checks, err := s.setSessionRequestToCommand(ctx, req)
if err != nil {
return nil, err
}
set, err := s.command.UpdateSession(ctx, req.GetSessionId(), req.GetSessionToken(), checks, req.GetMetadata())
if err != nil {
return nil, err
}
// if there's no new token, just return the current
if set.NewToken == "" {
set.NewToken = req.GetSessionToken()
}
return &session.SetSessionResponse{
Details: object.DomainToDetailsPb(set.ObjectDetails),
SessionToken: set.NewToken,
}, nil
}
func (s *Server) DeleteSession(ctx context.Context, req *session.DeleteSessionRequest) (*session.DeleteSessionResponse, error) {
details, err := s.command.TerminateSession(ctx, req.GetSessionId(), req.GetSessionToken())
if err != nil {
return nil, err
}
return &session.DeleteSessionResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func sessionsToPb(sessions []*query.Session) []*session.Session {
s := make([]*session.Session, len(sessions))
for i, session := range sessions {
s[i] = sessionToPb(session)
}
return s
}
func sessionToPb(s *query.Session) *session.Session {
return &session.Session{
Id: s.ID,
CreationDate: timestamppb.New(s.CreationDate),
ChangeDate: timestamppb.New(s.ChangeDate),
Sequence: s.Sequence,
Factors: factorsToPb(s),
Metadata: s.Metadata,
}
}
func factorsToPb(s *query.Session) *session.Factors {
user := userFactorToPb(s.UserFactor)
pw := passwordFactorToPb(s.PasswordFactor)
if user == nil && pw == nil {
return nil
}
return &session.Factors{
User: user,
Password: pw,
}
}
func passwordFactorToPb(factor query.SessionPasswordFactor) *session.PasswordFactor {
if factor.PasswordCheckedAt.IsZero() {
return nil
}
return &session.PasswordFactor{
VerifiedAt: timestamppb.New(factor.PasswordCheckedAt),
}
}
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
return nil
}
return &session.UserFactor{
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
Id: factor.UserID,
LoginName: factor.LoginName,
DisplayName: factor.DisplayName,
}
}
func listSessionsRequestToQuery(ctx context.Context, req *session.ListSessionsRequest) (*query.SessionsSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := sessionQueriesToQuery(ctx, req.GetQueries())
if err != nil {
return nil, err
}
return &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func sessionQueriesToQuery(ctx context.Context, queries []*session.SearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries)+1)
for i, query := range queries {
q[i], err = sessionQueryToQuery(query)
if err != nil {
return nil, err
}
}
creatorQuery, err := query.NewSessionCreatorSearchQuery(authz.GetCtxData(ctx).UserID)
if err != nil {
return nil, err
}
q[len(queries)] = creatorQuery
return q, nil
}
func sessionQueryToQuery(query *session.SearchQuery) (query.SearchQuery, error) {
switch q := query.Query.(type) {
case *session.SearchQuery_IdsQuery:
return idsQueryToQuery(q.IdsQuery)
default:
return nil, caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid")
}
}
func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) {
return query.NewSessionIDsSearchQuery(q.Ids)
}
func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCheck, map[string][]byte, error) {
checks, err := s.checksToCommand(ctx, req.Checks)
if err != nil {
return nil, nil, err
}
return checks, req.GetMetadata(), nil
}
func (s *Server) setSessionRequestToCommand(ctx context.Context, req *session.SetSessionRequest) ([]command.SessionCheck, error) {
checks, err := s.checksToCommand(ctx, req.Checks)
if err != nil {
return nil, err
}
return checks, nil
}
func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([]command.SessionCheck, error) {
checkUser, err := userCheck(checks.GetUser())
if err != nil {
return nil, err
}
sessionChecks := make([]command.SessionCheck, 0, 2)
if checkUser != nil {
user, err := checkUser.search(ctx, s.query)
if err != nil {
return nil, err
}
sessionChecks = append(sessionChecks, command.CheckUser(user.ID))
}
if password := checks.GetPassword(); password != nil {
sessionChecks = append(sessionChecks, command.CheckPassword(password.GetPassword()))
}
return sessionChecks, nil
}
func userCheck(user *session.CheckUser) (userSearch, error) {
if user == nil {
return nil, nil
}
switch s := user.GetSearch().(type) {
case *session.CheckUser_UserId:
return userByID(s.UserId), nil
case *session.CheckUser_LoginName:
return userByLoginName(s.LoginName)
default:
return nil, caos_errs.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", s)
}
}
type userSearch interface {
search(ctx context.Context, q *query.Queries) (*query.User, error)
}
func userByID(userID string) userSearch {
return userSearchByID{userID}
}
func userByLoginName(loginName string) (userSearch, error) {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
if err != nil {
return nil, err
}
return userSearchByLoginName{loginNameQuery}, nil
}
type userSearchByID struct {
id string
}
func (u userSearchByID) search(ctx context.Context, q *query.Queries) (*query.User, error) {
return q.GetUserByID(ctx, true, u.id, false)
}
type userSearchByLoginName struct {
loginNameQuery query.SearchQuery
}
func (u userSearchByLoginName) search(ctx context.Context, q *query.Queries) (*query.User, error) {
return q.GetUser(ctx, true, false, u.loginNameQuery)
}

View File

@@ -0,0 +1,379 @@
package session
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
)
func Test_sessionsToPb(t *testing.T) {
now := time.Now()
past := now.Add(-time.Hour)
sessions := []*query.Session{
{ // no factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // user factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // no factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
PasswordFactor: query.SessionPasswordFactor{
PasswordCheckedAt: past,
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
}
want := []*session.Session{
{ // no factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: nil,
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // user factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // password factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
Password: &session.PasswordFactor{
VerifiedAt: timestamppb.New(past),
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
}
out := sessionsToPb(sessions)
require.Len(t, out, len(want))
for i, got := range out {
if !proto.Equal(got, want[i]) {
t.Errorf("session %d got:\n%v\nwant:\n%v", i, got, want)
}
}
}
func mustNewTextQuery(t testing.TB, column query.Column, value string, compare query.TextComparison) query.SearchQuery {
q, err := query.NewTextQuery(column, value, compare)
require.NoError(t, err)
return q
}
func mustNewListQuery(t testing.TB, column query.Column, list []any, compare query.ListComparison) query.SearchQuery {
q, err := query.NewListQuery(query.SessionColumnID, list, compare)
require.NoError(t, err)
return q
}
func Test_listSessionsRequestToQuery(t *testing.T) {
type args struct {
ctx context.Context
req *session.ListSessionsRequest
}
tests := []struct {
name string
args args
want *query.SessionsSearchQueries
wantErr error
}{
{
name: "default request",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{},
},
want: &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 0,
Asc: false,
},
Queries: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
},
{
name: "with list query and sessions",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{
Query: &object.ListQuery{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"4", "5", "6"},
},
}},
},
},
},
want: &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []query.SearchQuery{
mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn),
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
},
{
name: "invalid argument error",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{
Query: &object.ListQuery{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: nil},
},
},
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := listSessionsRequestToQuery(tt.args.ctx, tt.args.req)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_sessionQueriesToQuery(t *testing.T) {
type args struct {
ctx context.Context
queries []*session.SearchQuery
}
tests := []struct {
name string
args args
want []query.SearchQuery
wantErr error
}{
{
name: "creator only",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
},
want: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
{
name: "invalid argument",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
queries: []*session.SearchQuery{
{Query: nil},
},
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
{
name: "creator and sessions",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"4", "5", "6"},
},
}},
},
},
want: []query.SearchQuery{
mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn),
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sessionQueriesToQuery(tt.args.ctx, tt.args.queries)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_sessionQueryToQuery(t *testing.T) {
type args struct {
query *session.SearchQuery
}
tests := []struct {
name string
args args
want query.SearchQuery
wantErr error
}{
{
name: "invalid argument",
args: args{&session.SearchQuery{
Query: nil,
}},
wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
{
name: "query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
},
}},
want: mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sessionQueryToQuery(tt.args.query)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func mustUserLoginNamesSearchQuery(t testing.TB, value string) query.SearchQuery {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery("bar")
require.NoError(t, err)
return loginNameQuery
}
func Test_userCheck(t *testing.T) {
type args struct {
user *session.CheckUser
}
tests := []struct {
name string
args args
want userSearch
wantErr error
}{
{
name: "nil user",
args: args{nil},
want: nil,
},
{
name: "by user id",
args: args{&session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: "foo",
},
}},
want: userSearchByID{"foo"},
},
{
name: "by user id",
args: args{&session.CheckUser{
Search: &session.CheckUser_LoginName{
LoginName: "bar",
},
}},
want: userSearchByLoginName{mustUserLoginNamesSearchQuery(t, "bar")},
},
{
name: "unimplemented error",
args: args{&session.CheckUser{
Search: nil,
}},
wantErr: caos_errs.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := userCheck(tt.args.user)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -21,11 +21,7 @@ func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp
case *user.SetEmailRequest_ReturnCode:
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
case *user.SetEmailRequest_IsVerified:
if v.IsVerified {
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), resourceOwner, req.GetEmail())
} else {
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
}
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), resourceOwner, req.GetEmail())
case nil:
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
default:

View File

@@ -0,0 +1,210 @@
//go:build integration
package user_test
import (
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
"google.golang.org/protobuf/types/known/timestamppb"
)
func createHumanUser(t *testing.T) *user.AddHumanUserResponse {
resp, err := Client.AddHumanUser(CTX, &user.AddHumanUserRequest{
Organisation: &object.Organisation{
Org: &object.Organisation_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
FirstName: "Mickey",
LastName: "Mouse",
},
Email: &user.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
})
require.NoError(t, err)
require.NotEmpty(t, resp.GetUserId())
return resp
}
func TestServer_SetEmail(t *testing.T) {
userID := createHumanUser(t).GetUserId()
tests := []struct {
name string
req *user.SetEmailRequest
want *user.SetEmailResponse
wantErr bool
}{
{
name: "default verfication",
req: &user.SetEmailRequest{
UserId: userID,
Email: "default-verifier@mouse.com",
},
want: &user.SetEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "custom url template",
req: &user.SetEmailRequest{
UserId: userID,
Email: "custom-url@mouse.com",
Verification: &user.SetEmailRequest_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
},
},
},
want: &user.SetEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "template error",
req: &user.SetEmailRequest{
UserId: userID,
Email: "custom-url@mouse.com",
Verification: &user.SetEmailRequest_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("{{"),
},
},
},
wantErr: true,
},
{
name: "return code",
req: &user.SetEmailRequest{
UserId: userID,
Email: "return-code@mouse.com",
Verification: &user.SetEmailRequest_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
want: &user.SetEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
VerificationCode: gu.Ptr("xxx"),
},
},
{
name: "is verified true",
req: &user.SetEmailRequest{
UserId: userID,
Email: "verified-true@mouse.com",
Verification: &user.SetEmailRequest_IsVerified{
IsVerified: true,
},
},
want: &user.SetEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "is verified false",
req: &user.SetEmailRequest{
UserId: userID,
Email: "verified-false@mouse.com",
Verification: &user.SetEmailRequest_IsVerified{
IsVerified: false,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.SetEmail(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
if tt.want.GetVerificationCode() != "" {
assert.NotEmpty(t, got.GetVerificationCode())
}
})
}
}
func TestServer_VerifyEmail(t *testing.T) {
userResp := createHumanUser(t)
tests := []struct {
name string
req *user.VerifyEmailRequest
want *user.VerifyEmailResponse
wantErr bool
}{
{
name: "wrong code",
req: &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: "xxx",
},
wantErr: true,
},
{
name: "wrong user",
req: &user.VerifyEmailRequest{
UserId: "xxx",
VerificationCode: userResp.GetEmailCode(),
},
wantErr: true,
},
{
name: "verify user",
req: &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetEmailCode(),
},
want: &user.VerifyEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.VerifyEmail(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
@@ -19,10 +19,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
if err != nil {
return nil, err
}
orgID := req.GetOrganisation().GetOrgId()
if orgID == "" {
orgID = authz.GetCtxData(ctx).OrgID
}
orgID := authz.GetCtxData(ctx).OrgID
err = s.command.AddHuman(ctx, orgID, human, false)
if err != nil {
return nil, err

View File

@@ -0,0 +1,317 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
"google.golang.org/protobuf/types/known/timestamppb"
)
var (
CTX context.Context
ErrCTX context.Context
Tester *integration.Tester
Client user.UserServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(time.Hour)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX, ErrCTX = Tester.WithSystemAuthorization(ctx, integration.OrgOwner), errCtx
Client = user.NewUserServiceClient(Tester.GRPCClientConn)
return m.Run()
}())
}
func TestServer_AddHumanUser(t *testing.T) {
type args struct {
ctx context.Context
req *user.AddHumanUserRequest
}
tests := []struct {
name string
args args
want *user.AddHumanUserResponse
wantErr bool
}{
{
name: "default verification",
args: args{
CTX,
&user.AddHumanUserRequest{
Organisation: &object.Organisation{
Org: &object.Organisation_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
FirstName: "Donald",
LastName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &user.SetHumanEmail{},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
},
},
want: &user.AddHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "return verification code",
args: args{
CTX,
&user.AddHumanUserRequest{
Organisation: &object.Organisation{
Org: &object.Organisation_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
FirstName: "Donald",
LastName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
},
},
want: &user.AddHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
EmailCode: gu.Ptr("something"),
},
},
{
name: "custom template",
args: args{
CTX,
&user.AddHumanUserRequest{
Organisation: &object.Organisation{
Org: &object.Organisation_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
FirstName: "Donald",
LastName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
},
},
},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
},
},
want: &user.AddHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "custom template error",
args: args{
CTX,
&user.AddHumanUserRequest{
Organisation: &object.Organisation{
Org: &object.Organisation_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
FirstName: "Donald",
LastName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("{{"),
},
},
},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
},
},
wantErr: true,
},
{
name: "missing REQUIRED profile",
args: args{
CTX,
&user.AddHumanUserRequest{
Organisation: &object.Organisation{
Org: &object.Organisation_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
},
},
wantErr: true,
},
{
name: "missing REQUIRED email",
args: args{
CTX,
&user.AddHumanUserRequest{
Organisation: &object.Organisation{
Org: &object.Organisation_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
FirstName: "Donald",
LastName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
},
},
wantErr: true,
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
userID := fmt.Sprint(time.Now().UnixNano() + int64(i))
tt.args.req.UserId = &userID
if email := tt.args.req.GetEmail(); email != nil {
email.Email = fmt.Sprintf("%s@me.now", userID)
}
if tt.want != nil {
tt.want.UserId = userID
}
got, err := Client.AddHumanUser(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want.GetUserId(), got.GetUserId())
if tt.want.GetEmailCode() != "" {
assert.NotEmpty(t, got.GetEmailCode())
}
integration.AssertDetails(t, tt.want, got)
})
}
}