mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
feat(api): new session service (#5801)
* backup new protoc plugin * backup * session * backup * initial implementation * change to specific events * implement tests * cleanup * refactor: use new protoc plugin for api v2 * change package * simplify code * cleanup * cleanup * fix merge * start queries * fix tests * improve returned values * add token to projection * tests * test db map * update query * permission checks * fix tests and linting * rework token creation * i18n * refactor token check and fix tests * session to PB test * request to query tests * cleanup proto * test user check * add comment * simplify database map type * Update docs/docs/guides/integrate/access-zitadel-system-api.md Co-authored-by: Tim Möhlmann <tim+github@zitadel.com> * fix test * cleanup * docs --------- Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
parent
74377c2c37
commit
c2cb84cd24
@ -77,6 +77,7 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
@ -52,6 +52,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
@ -50,6 +50,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
@ -129,7 +130,21 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
||||
return fmt.Errorf("cannot start eventstore for queries: %w", err)
|
||||
}
|
||||
|
||||
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, keys.SAML, config.InternalAuthZ.RolePermissionMappings)
|
||||
sessionTokenVerifier := internal_authz.SessionTokenVerifier(keys.OIDC)
|
||||
|
||||
queries, err := query.StartQueries(
|
||||
ctx,
|
||||
eventstoreClient,
|
||||
dbClient,
|
||||
config.Projections,
|
||||
config.SystemDefaults,
|
||||
keys.IDPConfig,
|
||||
keys.OTP,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
config.InternalAuthZ.RolePermissionMappings,
|
||||
sessionTokenVerifier,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start queries: %w", err)
|
||||
}
|
||||
@ -138,6 +153,9 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error starting authz repo: %w", err)
|
||||
}
|
||||
permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
|
||||
}
|
||||
|
||||
storage, err := config.AssetStorage.NewStorage(dbClient.DB)
|
||||
if err != nil {
|
||||
@ -165,7 +183,8 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
&http.Client{},
|
||||
authZRepo,
|
||||
permissionCheck,
|
||||
sessionTokenVerifier,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start commands: %w", err)
|
||||
@ -195,7 +214,22 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = startAPIs(ctx, clock, router, commands, queries, eventstoreClient, dbClient, config, storage, authZRepo, keys, queries, usageReporter)
|
||||
err = startAPIs(
|
||||
ctx,
|
||||
clock,
|
||||
router,
|
||||
commands,
|
||||
queries,
|
||||
eventstoreClient,
|
||||
dbClient,
|
||||
config,
|
||||
storage,
|
||||
authZRepo,
|
||||
keys,
|
||||
queries,
|
||||
usageReporter,
|
||||
permissionCheck,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -239,6 +273,7 @@ func startAPIs(
|
||||
keys *encryptionKeys,
|
||||
quotaQuerier logstore.QuotaQuerier,
|
||||
usageReporter logstore.UsageReporter,
|
||||
permissionCheck domain.PermissionCheck,
|
||||
) error {
|
||||
repo := struct {
|
||||
authz_repo.Repository
|
||||
@ -294,7 +329,7 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, user.CreateServer(commands, queries, keys.User)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
|
||||
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||
return err
|
||||
}
|
||||
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
|
||||
|
@ -86,7 +86,7 @@ If your system is exposed without TLS or on a dedicated port, be sure to provide
|
||||
If you want to manually create a JWT for a test, you can also use our [ZITADEL Tools](https://github.com/zitadel/zitadel-tools). Download the latest release and run:
|
||||
|
||||
```bash
|
||||
./key2jwt -audience=https://custom-domain.com -key=system-user-1.pem -issuer=system-user-1
|
||||
zitadel-tools key2jwt --audience=https://custom-domain.com --key=system-user-1.pem --issuer=system-user-1
|
||||
```
|
||||
|
||||
## Call the System API
|
||||
|
@ -266,6 +266,13 @@ module.exports = {
|
||||
sidebarOptions: {
|
||||
groupPathsBy: "tag",
|
||||
},
|
||||
},
|
||||
session: {
|
||||
specPath: ".artifacts/openapi/zitadel/session/v2alpha/session_service.swagger.json",
|
||||
outputDir: "docs/apis/session_service",
|
||||
sidebarOptions: {
|
||||
groupPathsBy: "tag",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -412,6 +412,20 @@ module.exports = {
|
||||
},
|
||||
items: require("./docs/apis/user_service/sidebar.js"),
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Session Lifecycle (Alpha)",
|
||||
link: {
|
||||
type: "generated-index",
|
||||
title: "Session Service API (Alpha)",
|
||||
slug: "/apis/session_service",
|
||||
description:
|
||||
"This API is intended to manage sessions in a ZITADEL instance.\n"+
|
||||
"\n"+
|
||||
"This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.",
|
||||
},
|
||||
items: require("./docs/apis/session_service/sidebar.js"),
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Assets",
|
||||
|
2
go.mod
2
go.mod
@ -192,7 +192,7 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/sys v0.7.0
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
@ -14,11 +14,16 @@ const (
|
||||
authenticated = "authenticated"
|
||||
)
|
||||
|
||||
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgIDHeader string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
|
||||
// CheckUserAuthorization verifies that:
|
||||
// - the token is active,
|
||||
// - the organisation (**either** provided by ID or verified domain) exists
|
||||
// - the user is permitted to call the requested endpoint (permission option in proto)
|
||||
// it will pass the [CtxData] and permission of the user into the ctx [context.Context]
|
||||
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
|
||||
ctx, span := tracing.NewServerInterceptorSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgIDHeader, verifier, method)
|
||||
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ const (
|
||||
MemberTypeIam
|
||||
)
|
||||
|
||||
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID string, t *TokenVerifier, method string) (_ CtxData, err error) {
|
||||
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t *TokenVerifier, method string) (_ CtxData, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@ -82,14 +82,15 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID string, t *To
|
||||
if err := checkOrigin(ctx, origins); err != nil {
|
||||
return CtxData{}, err
|
||||
}
|
||||
if orgID == "" {
|
||||
if orgID == "" && orgDomain == "" {
|
||||
orgID = resourceOwner
|
||||
}
|
||||
|
||||
err = t.ExistsOrg(ctx, orgID)
|
||||
verifiedOrgID, err := t.ExistsOrg(ctx, orgID, orgDomain)
|
||||
if err != nil {
|
||||
err = retry(func() error {
|
||||
return t.ExistsOrg(ctx, orgID)
|
||||
verifiedOrgID, err = t.ExistsOrg(ctx, orgID, orgDomain)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return CtxData{}, errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
|
||||
@ -98,7 +99,7 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID string, t *To
|
||||
|
||||
return CtxData{
|
||||
UserID: userID,
|
||||
OrgID: orgID,
|
||||
OrgID: verifiedOrgID,
|
||||
ProjectID: projectID,
|
||||
AgentID: agentID,
|
||||
PreferredLanguage: prefLang,
|
||||
|
@ -7,12 +7,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||
ctxData := GetCtxData(ctx)
|
||||
if allowSelf && ctxData.UserID == resourceID {
|
||||
return nil
|
||||
}
|
||||
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, ctxData, orgID)
|
||||
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) {
|
||||
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, client
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID string) error {
|
||||
return nil
|
||||
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
|
||||
return orgID, nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
|
||||
|
@ -3,6 +3,8 @@ package authz
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -17,7 +19,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
BearerPrefix = "Bearer "
|
||||
BearerPrefix = "Bearer "
|
||||
SessionTokenFormat = "sess_%s:%s"
|
||||
)
|
||||
|
||||
type TokenVerifier struct {
|
||||
@ -36,7 +39,7 @@ type authZRepo interface {
|
||||
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
|
||||
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
||||
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
|
||||
ExistsOrg(ctx context.Context, orgID string) error
|
||||
ExistsOrg(ctx context.Context, id, domain string) (string, error)
|
||||
}
|
||||
|
||||
func Start(authZRepo authZRepo, issuer string, keys map[string]*SystemAPIUser) (v *TokenVerifier) {
|
||||
@ -144,10 +147,10 @@ func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clien
|
||||
return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID)
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) ExistsOrg(ctx context.Context, orgID string) (err error) {
|
||||
func (v *TokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
return v.authZRepo.ExistsOrg(ctx, orgID)
|
||||
return v.authZRepo.ExistsOrg(ctx, id, domain)
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) CheckAuthMethod(method string) (Option, bool) {
|
||||
@ -165,3 +168,20 @@ func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, meth
|
||||
}
|
||||
return t.VerifyAccessToken(ctx, parts[1], method)
|
||||
}
|
||||
|
||||
func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
||||
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
||||
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
||||
var token string
|
||||
token, err = algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
|
||||
spanPasswordComparison.EndWithError(err)
|
||||
if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) {
|
||||
return caos_errs.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -34,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.(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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
379
internal/api/grpc/session/v2/session_test.go
Normal file
379
internal/api/grpc/session/v2/session_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -62,7 +62,7 @@ func authorize(r *http.Request, verifier *authz.TokenVerifier, authConfig authz.
|
||||
return nil, errors.New("auth header missing")
|
||||
}
|
||||
|
||||
ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), verifier, authConfig, authOpt, r.RequestURI)
|
||||
ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, authConfig, authOpt, r.RequestURI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
usr_grant_repo "github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||
"github.com/zitadel/zitadel/internal/static"
|
||||
@ -29,7 +30,7 @@ import (
|
||||
type Commands struct {
|
||||
httpClient *http.Client
|
||||
|
||||
checkPermission permissionCheck
|
||||
checkPermission domain.PermissionCheck
|
||||
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||
|
||||
eventstore *eventstore.Eventstore
|
||||
@ -50,6 +51,8 @@ type Commands struct {
|
||||
domainVerificationAlg crypto.EncryptionAlgorithm
|
||||
domainVerificationGenerator crypto.Generator
|
||||
domainVerificationValidator func(domain, token, verifier string, checkType api_http.CheckType) error
|
||||
sessionTokenCreator func(sessionID string) (id string, token string, err error)
|
||||
sessionTokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
|
||||
|
||||
multifactors domain.MultifactorConfigs
|
||||
webauthnConfig *webauthn_helper.Config
|
||||
@ -71,24 +74,21 @@ func StartCommands(
|
||||
externalDomain string,
|
||||
externalSecure bool,
|
||||
externalPort uint16,
|
||||
idpConfigEncryption,
|
||||
otpEncryption,
|
||||
smtpEncryption,
|
||||
smsEncryption,
|
||||
userEncryption,
|
||||
domainVerificationEncryption,
|
||||
oidcEncryption,
|
||||
samlEncryption crypto.EncryptionAlgorithm,
|
||||
idpConfigEncryption, otpEncryption, smtpEncryption, smsEncryption, userEncryption, domainVerificationEncryption, oidcEncryption, samlEncryption crypto.EncryptionAlgorithm,
|
||||
httpClient *http.Client,
|
||||
membershipsResolver authz.MembershipsResolver,
|
||||
permissionCheck domain.PermissionCheck,
|
||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
||||
) (repo *Commands, err error) {
|
||||
if externalDomain == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
||||
}
|
||||
idGenerator := id.SonyFlakeGenerator()
|
||||
// reuse the oidcEncryption to be able to handle both tokens in the interceptor later on
|
||||
sessionAlg := oidcEncryption
|
||||
repo = &Commands{
|
||||
eventstore: es,
|
||||
static: staticStore,
|
||||
idGenerator: id.SonyFlakeGenerator(),
|
||||
idGenerator: idGenerator,
|
||||
zitadelRoles: zitadelRoles,
|
||||
externalDomain: externalDomain,
|
||||
externalSecure: externalSecure,
|
||||
@ -107,10 +107,10 @@ func StartCommands(
|
||||
certificateAlgorithm: samlEncryption,
|
||||
webauthnConfig: webAuthN,
|
||||
httpClient: httpClient,
|
||||
checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||
return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf)
|
||||
},
|
||||
newEmailCode: newEmailCode,
|
||||
checkPermission: permissionCheck,
|
||||
newEmailCode: newEmailCode,
|
||||
sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg),
|
||||
sessionTokenVerifier: sessionTokenVerifier,
|
||||
}
|
||||
|
||||
instance_repo.RegisterEventMappers(repo.eventstore)
|
||||
@ -121,6 +121,7 @@ func StartCommands(
|
||||
keypair.RegisterEventMappers(repo.eventstore)
|
||||
action.RegisterEventMappers(repo.eventstore)
|
||||
quota.RegisterEventMappers(repo.eventstore)
|
||||
session.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
||||
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
@ -19,6 +20,7 @@ import (
|
||||
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||
)
|
||||
@ -38,6 +40,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
|
||||
usergrant.RegisterEventMappers(es)
|
||||
key_repo.RegisterEventMappers(es)
|
||||
action_repo.RegisterEventMappers(es)
|
||||
session.RegisterEventMappers(es)
|
||||
return es
|
||||
}
|
||||
|
||||
@ -125,6 +128,11 @@ func expectFilter(events ...*repository.Event) expect {
|
||||
m.ExpectFilterEvents(events...)
|
||||
}
|
||||
}
|
||||
func expectFilterError(err error) expect {
|
||||
return func(m *mock.MockRepository) {
|
||||
m.ExpectFilterEventsError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func expectFilterOrgDomainNotFound() expect {
|
||||
return func(m *mock.MockRepository) {
|
||||
@ -250,14 +258,14 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMockPermissionCheckAllowed() permissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||
func newMockPermissionCheckAllowed() domain.PermissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func newMockPermissionCheckNotAllowed() permissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||
func newMockPermissionCheckNotAllowed() domain.PermissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return errors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type permissionCheck func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error)
|
||||
|
||||
const (
|
||||
permissionUserWrite = "user.write"
|
||||
)
|
225
internal/command/session.go
Normal file
225
internal/command/session.go
Normal file
@ -0,0 +1,225 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
type SessionCheck func(ctx context.Context, cmd *SessionChecks) error
|
||||
|
||||
type SessionChecks struct {
|
||||
checks []SessionCheck
|
||||
|
||||
sessionWriteModel *SessionWriteModel
|
||||
passwordWriteModel *HumanPasswordWriteModel
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
createToken func(sessionID string) (id string, token string, err error)
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func (c *Commands) NewSessionChecks(checks []SessionCheck, session *SessionWriteModel) *SessionChecks {
|
||||
return &SessionChecks{
|
||||
checks: checks,
|
||||
sessionWriteModel: session,
|
||||
eventstore: c.eventstore,
|
||||
userPasswordAlg: c.userPasswordAlg,
|
||||
createToken: c.sessionTokenCreator,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckUser defines a user check to be executed for a session update
|
||||
func CheckUser(id string) SessionCheck {
|
||||
return func(ctx context.Context, cmd *SessionChecks) error {
|
||||
if cmd.sessionWriteModel.UserID != "" && id != "" && cmd.sessionWriteModel.UserID != id {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "", "user change not possible")
|
||||
}
|
||||
return cmd.sessionWriteModel.UserChecked(ctx, id, cmd.now())
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPassword defines a password check to be executed for a session update
|
||||
func CheckPassword(password string) SessionCheck {
|
||||
return func(ctx context.Context, cmd *SessionChecks) error {
|
||||
if cmd.sessionWriteModel.UserID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfw3f", "Errors.User.UserIDMissing")
|
||||
}
|
||||
cmd.passwordWriteModel = NewHumanPasswordWriteModel(cmd.sessionWriteModel.UserID, "")
|
||||
err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.passwordWriteModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.passwordWriteModel.UserState == domain.UserStateUnspecified || cmd.passwordWriteModel.UserState == domain.UserStateDeleted {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.User.NotFound")
|
||||
}
|
||||
|
||||
if cmd.passwordWriteModel.Secret == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-WEf3t", "Errors.User.Password.NotSet")
|
||||
}
|
||||
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
||||
err = crypto.CompareHash(cmd.passwordWriteModel.Secret, []byte(password), cmd.userPasswordAlg)
|
||||
spanPasswordComparison.EndWithError(err)
|
||||
if err != nil {
|
||||
//TODO: maybe we want to reset the session in the future https://github.com/zitadel/zitadel/issues/5807
|
||||
return caos_errs.ThrowInvalidArgument(err, "COMMAND-SAF3g", "Errors.User.Password.Invalid")
|
||||
}
|
||||
cmd.sessionWriteModel.PasswordChecked(ctx, cmd.now())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check will execute the checks specified and return an error on the first occurrence
|
||||
func (s *SessionChecks) Check(ctx context.Context) error {
|
||||
for _, check := range s.checks {
|
||||
if err := check(ctx, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionChecks) commands(ctx context.Context) (string, []eventstore.Command, error) {
|
||||
if len(s.sessionWriteModel.commands) == 0 {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
tokenID, token, err := s.createToken(s.sessionWriteModel.AggregateID)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
s.sessionWriteModel.SetToken(ctx, tokenID)
|
||||
return token, s.sessionWriteModel.commands, nil
|
||||
}
|
||||
|
||||
func (c *Commands) CreateSession(ctx context.Context, checks []SessionCheck, metadata map[string][]byte) (set *SessionChanged, err error) {
|
||||
sessionID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := c.NewSessionChecks(checks, sessionWriteModel)
|
||||
cmd.sessionWriteModel.Start(ctx)
|
||||
return c.updateSession(ctx, cmd, metadata)
|
||||
}
|
||||
|
||||
func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken string, checks []SessionCheck, metadata map[string][]byte) (set *SessionChanged, err error) {
|
||||
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := c.NewSessionChecks(checks, sessionWriteModel)
|
||||
return c.updateSession(ctx, cmd, metadata)
|
||||
}
|
||||
|
||||
func (c *Commands) TerminateSession(ctx context.Context, sessionID, sessionToken string) (*domain.ObjectDetails, error) {
|
||||
sessionWriteModel := NewSessionWriteModel(sessionID, "")
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionDelete); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sessionWriteModel.State != domain.SessionStateActive {
|
||||
return writeModelToObjectDetails(&sessionWriteModel.WriteModel), nil
|
||||
}
|
||||
terminate := session.NewTerminateEvent(ctx, &session.NewAggregate(sessionWriteModel.AggregateID, sessionWriteModel.ResourceOwner).Aggregate)
|
||||
pushedEvents, err := c.eventstore.Push(ctx, terminate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(sessionWriteModel, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&sessionWriteModel.WriteModel), nil
|
||||
}
|
||||
|
||||
// updateSession execute the [SessionChecks] where new events will be created and as well as for metadata (changes)
|
||||
func (c *Commands) updateSession(ctx context.Context, checks *SessionChecks, metadata map[string][]byte) (set *SessionChanged, err error) {
|
||||
if checks.sessionWriteModel.State == domain.SessionStateTerminated {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMAND-SAjeh", "Errors.Session.Terminated")
|
||||
}
|
||||
if err := checks.Check(ctx); err != nil {
|
||||
// TODO: how to handle failed checks (e.g. pw wrong) https://github.com/zitadel/zitadel/issues/5807
|
||||
return nil, err
|
||||
}
|
||||
checks.sessionWriteModel.ChangeMetadata(ctx, metadata)
|
||||
sessionToken, cmds, err := checks.commands(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cmds) == 0 {
|
||||
return sessionWriteModelToSessionChanged(checks.sessionWriteModel), nil
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(checks.sessionWriteModel, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changed := sessionWriteModelToSessionChanged(checks.sessionWriteModel)
|
||||
changed.NewToken = sessionToken
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// sessionPermission will check that the provided sessionToken is correct or
|
||||
// if empty, check that the caller is granted the necessary permission
|
||||
func (c *Commands) sessionPermission(ctx context.Context, sessionWriteModel *SessionWriteModel, sessionToken, permission string) (err error) {
|
||||
if sessionToken == "" {
|
||||
return c.checkPermission(ctx, permission, authz.GetCtxData(ctx).OrgID, sessionWriteModel.AggregateID)
|
||||
}
|
||||
return c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID)
|
||||
}
|
||||
|
||||
func sessionTokenCreator(idGenerator id.Generator, sessionAlg crypto.EncryptionAlgorithm) func(sessionID string) (id string, token string, err error) {
|
||||
return func(sessionID string) (id string, token string, err error) {
|
||||
id, err = idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
encrypted, err := sessionAlg.Encrypt([]byte(fmt.Sprintf(authz.SessionTokenFormat, sessionID, id)))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return id, base64.RawURLEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
}
|
||||
|
||||
type SessionChanged struct {
|
||||
*domain.ObjectDetails
|
||||
ID string
|
||||
NewToken string
|
||||
}
|
||||
|
||||
func sessionWriteModelToSessionChanged(wm *SessionWriteModel) *SessionChanged {
|
||||
return &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: wm.ProcessedSequence,
|
||||
EventDate: wm.ChangeDate,
|
||||
ResourceOwner: wm.ResourceOwner,
|
||||
},
|
||||
ID: wm.AggregateID,
|
||||
}
|
||||
}
|
139
internal/command/session_model.go
Normal file
139
internal/command/session_model.go
Normal file
@ -0,0 +1,139 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
)
|
||||
|
||||
type SessionWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
TokenID string
|
||||
UserID string
|
||||
UserCheckedAt time.Time
|
||||
PasswordCheckedAt time.Time
|
||||
Metadata map[string][]byte
|
||||
State domain.SessionState
|
||||
|
||||
commands []eventstore.Command
|
||||
aggregate *eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewSessionWriteModel(sessionID string, resourceOwner string) *SessionWriteModel {
|
||||
return &SessionWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: sessionID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
Metadata: make(map[string][]byte),
|
||||
aggregate: &session.NewAggregate(sessionID, resourceOwner).Aggregate,
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *session.AddedEvent:
|
||||
wm.reduceAdded(e)
|
||||
case *session.UserCheckedEvent:
|
||||
wm.reduceUserChecked(e)
|
||||
case *session.PasswordCheckedEvent:
|
||||
wm.reducePasswordChecked(e)
|
||||
case *session.TokenSetEvent:
|
||||
wm.reduceTokenSet(e)
|
||||
case *session.TerminateEvent:
|
||||
wm.reduceTerminate()
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(session.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
session.AddedType,
|
||||
session.UserCheckedType,
|
||||
session.PasswordCheckedType,
|
||||
session.TokenSetType,
|
||||
session.MetadataSetType,
|
||||
session.TerminateType,
|
||||
).
|
||||
Builder()
|
||||
|
||||
if wm.ResourceOwner != "" {
|
||||
query.ResourceOwner(wm.ResourceOwner)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) reduceAdded(e *session.AddedEvent) {
|
||||
wm.State = domain.SessionStateActive
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) reduceUserChecked(e *session.UserCheckedEvent) {
|
||||
wm.UserID = e.UserID
|
||||
wm.UserCheckedAt = e.CheckedAt
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) reducePasswordChecked(e *session.PasswordCheckedEvent) {
|
||||
wm.PasswordCheckedAt = e.CheckedAt
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) reduceTokenSet(e *session.TokenSetEvent) {
|
||||
wm.TokenID = e.TokenID
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) reduceTerminate() {
|
||||
wm.State = domain.SessionStateTerminated
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) Start(ctx context.Context) {
|
||||
wm.commands = append(wm.commands, session.NewAddedEvent(ctx, wm.aggregate))
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error {
|
||||
wm.commands = append(wm.commands, session.NewUserCheckedEvent(ctx, wm.aggregate, userID, checkedAt))
|
||||
// set the userID so other checks can use it
|
||||
wm.UserID = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) PasswordChecked(ctx context.Context, checkedAt time.Time) {
|
||||
wm.commands = append(wm.commands, session.NewPasswordCheckedEvent(ctx, wm.aggregate, checkedAt))
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) SetToken(ctx context.Context, tokenID string) {
|
||||
wm.commands = append(wm.commands, session.NewTokenSetEvent(ctx, wm.aggregate, tokenID))
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) ChangeMetadata(ctx context.Context, metadata map[string][]byte) {
|
||||
var changed bool
|
||||
for key, value := range metadata {
|
||||
currentValue, exists := wm.Metadata[key]
|
||||
|
||||
if len(value) != 0 {
|
||||
// if a value is provided, and it's not equal, change it
|
||||
if !bytes.Equal(currentValue, value) {
|
||||
wm.Metadata[key] = value
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
// if there's no / an empty value, we only need to remove it on existing entries
|
||||
if exists {
|
||||
delete(wm.Metadata, key)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
wm.commands = append(wm.commands, session.NewMetadataSetEvent(ctx, wm.aggregate, wm.Metadata))
|
||||
}
|
||||
}
|
547
internal/command/session_test.go
Normal file
547
internal/command/session_test.go
Normal file
@ -0,0 +1,547 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
func TestCommands_CreateSession(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
tokenCreator func(sessionID string) (string, string, error)
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
checks []SessionCheck
|
||||
metadata map[string][]byte
|
||||
}
|
||||
type res struct {
|
||||
want *SessionChanged
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"id generator fails",
|
||||
fields{
|
||||
idGenerator: mock.NewIDGeneratorExpectError(t, caos_errs.ThrowInternal(nil, "id", "generator failed")),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowInternal(nil, "id", "generator failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"eventstore failed",
|
||||
fields{
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilterError(caos_errs.ThrowInternal(nil, "id", "filter failed")),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowInternal(nil, "id", "filter failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"empty session",
|
||||
fields{
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate),
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenCreator: func(sessionID string) (string, string, error) {
|
||||
return "tokenID",
|
||||
"token",
|
||||
nil
|
||||
},
|
||||
},
|
||||
args{
|
||||
ctx: authz.NewMockContext("", "org1", ""),
|
||||
},
|
||||
res{
|
||||
want: &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{ResourceOwner: "org1"},
|
||||
ID: "sessionID",
|
||||
NewToken: "token",
|
||||
},
|
||||
},
|
||||
},
|
||||
// the rest is tested in the Test_updateSession
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
sessionTokenCreator: tt.fields.tokenCreator,
|
||||
}
|
||||
got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_UpdateSession(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
sessionID string
|
||||
sessionToken string
|
||||
checks []SessionCheck
|
||||
metadata map[string][]byte
|
||||
}
|
||||
type res struct {
|
||||
want *SessionChanged
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"eventstore failed",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilterError(caos_errs.ThrowInternal(nil, "id", "filter failed")),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowInternal(nil, "id", "filter failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid session token",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID")),
|
||||
),
|
||||
),
|
||||
tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
||||
return caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
|
||||
},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "invalid",
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"no change",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID")),
|
||||
),
|
||||
),
|
||||
tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "token",
|
||||
},
|
||||
res{
|
||||
want: &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
ID: "sessionID",
|
||||
NewToken: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
// the rest is tested in the Test_updateSession
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
sessionTokenVerifier: tt.fields.tokenVerifier,
|
||||
}
|
||||
got, err := c.UpdateSession(tt.args.ctx, tt.args.sessionID, tt.args.sessionToken, tt.args.checks, tt.args.metadata)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_updateSession(t *testing.T) {
|
||||
testNow := time.Now()
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
checks *SessionChecks
|
||||
metadata map[string][]byte
|
||||
}
|
||||
type res struct {
|
||||
want *SessionChanged
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"terminated",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
checks: &SessionChecks{
|
||||
sessionWriteModel: &SessionWriteModel{State: domain.SessionStateTerminated},
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowPreconditionFailed(nil, "COMAND-SAjeh", "Errors.Session.Terminated"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"check failed",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
checks: &SessionChecks{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
checks: []SessionCheck{
|
||||
func(ctx context.Context, cmd *SessionChecks) error {
|
||||
return caos_errs.ThrowInternal(nil, "id", "check failed")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowInternal(nil, "id", "check failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"no change",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
checks: &SessionChecks{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
checks: []SessionCheck{},
|
||||
},
|
||||
},
|
||||
res{
|
||||
want: &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
ID: "sessionID",
|
||||
NewToken: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"set user, password, metadata and token",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"userID", testNow),
|
||||
session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
testNow),
|
||||
session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
map[string][]byte{"key": []byte("value")}),
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID"),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
checks: &SessionChecks{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
checks: []SessionCheck{
|
||||
CheckUser("userID"),
|
||||
CheckPassword("password"),
|
||||
},
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
}, false, ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
createToken: func(sessionID string) (string, string, error) {
|
||||
return "tokenID",
|
||||
"token",
|
||||
nil
|
||||
},
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
now: func() time.Time {
|
||||
return testNow
|
||||
},
|
||||
},
|
||||
metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
res{
|
||||
want: &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
ID: "sessionID",
|
||||
NewToken: "token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
}
|
||||
got, err := c.updateSession(tt.args.ctx, tt.args.checks, tt.args.metadata)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_TerminateSession(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
sessionID string
|
||||
sessionToken string
|
||||
}
|
||||
type res struct {
|
||||
want *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"eventstore failed",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilterError(caos_errs.ThrowInternal(nil, "id", "filter failed")),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowInternal(nil, "id", "filter failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid session token",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID")),
|
||||
),
|
||||
),
|
||||
tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
||||
return caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
|
||||
},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "invalid",
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"not active",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID")),
|
||||
eventFromEventPusher(
|
||||
session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
),
|
||||
),
|
||||
tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "token",
|
||||
},
|
||||
res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"push failed",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID"),
|
||||
),
|
||||
),
|
||||
expectPushFailed(
|
||||
caos_errs.ThrowInternal(nil, "id", "pushed failed"),
|
||||
eventPusherToEvents(
|
||||
session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
),
|
||||
),
|
||||
tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "token",
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowInternal(nil, "id", "pushed failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"terminate",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID"),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewTerminateEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
),
|
||||
),
|
||||
tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "token",
|
||||
},
|
||||
res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
sessionTokenVerifier: tt.fields.tokenVerifier,
|
||||
}
|
||||
got, err := c.TerminateSession(tt.args.ctx, tt.args.sessionID, tt.args.sessionToken)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
@ -42,7 +43,7 @@ func (c *Commands) ChangeUserEmailVerified(ctx context.Context, userID, resource
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, false); err != nil {
|
||||
if err = c.checkPermission(ctx, domain.PermissionUserWrite, cmd.aggregate.ResourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
|
||||
@ -70,8 +71,10 @@ func (c *Commands) changeUserEmailWithGenerator(ctx context.Context, userID, res
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, true); err != nil {
|
||||
return nil, err
|
||||
if authz.GetCtxData(ctx).UserID != userID {
|
||||
if err = c.checkPermission(ctx, domain.PermissionUserWrite, cmd.aggregate.ResourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
|
||||
return nil, err
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
func TestCommands_ChangeUserEmail(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
checkPermission permissionCheck
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
@ -174,7 +174,7 @@ func TestCommands_ChangeUserEmail(t *testing.T) {
|
||||
func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
checkPermission permissionCheck
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
@ -300,7 +300,7 @@ func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) {
|
||||
func TestCommands_ChangeUserEmailReturnCode(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
checkPermission permissionCheck
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
@ -410,7 +410,7 @@ func TestCommands_ChangeUserEmailReturnCode(t *testing.T) {
|
||||
func TestCommands_ChangeUserEmailVerified(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
checkPermission permissionCheck
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
@ -569,7 +569,7 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) {
|
||||
func TestCommands_changeUserEmailWithGenerator(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
checkPermission permissionCheck
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
|
@ -2,13 +2,14 @@ package database
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jackc/pgtype"
|
||||
)
|
||||
|
||||
type StringArray []string
|
||||
|
||||
// Scan implements the `database/sql.Scanner` interface.
|
||||
// Scan implements the [database/sql.Scanner] interface.
|
||||
func (s *StringArray) Scan(src any) error {
|
||||
array := new(pgtype.TextArray)
|
||||
if err := array.Scan(src); err != nil {
|
||||
@ -20,7 +21,7 @@ func (s *StringArray) Scan(src any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the `database/sql/driver.Valuer` interface.
|
||||
// Value implements the [database/sql/driver.Valuer] interface.
|
||||
func (s StringArray) Value() (driver.Value, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, nil
|
||||
@ -40,7 +41,7 @@ type enumField interface {
|
||||
|
||||
type EnumArray[F enumField] []F
|
||||
|
||||
// Scan implements the `database/sql.Scanner` interface.
|
||||
// Scan implements the [database/sql.Scanner] interface.
|
||||
func (s *EnumArray[F]) Scan(src any) error {
|
||||
array := new(pgtype.Int2Array)
|
||||
if err := array.Scan(src); err != nil {
|
||||
@ -57,7 +58,7 @@ func (s *EnumArray[F]) Scan(src any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the `database/sql/driver.Valuer` interface.
|
||||
// Value implements the [database/sql/driver.Valuer] interface.
|
||||
func (s EnumArray[F]) Value() (driver.Value, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, nil
|
||||
@ -70,3 +71,25 @@ func (s EnumArray[F]) Value() (driver.Value, error) {
|
||||
|
||||
return array.Value()
|
||||
}
|
||||
|
||||
type Map[V any] map[string]V
|
||||
|
||||
// Scan implements the [database/sql.Scanner] interface.
|
||||
func (m *Map[V]) Scan(src any) error {
|
||||
bytea := new(pgtype.Bytea)
|
||||
if err := bytea.Scan(src); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(bytea.Bytes) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytea.Bytes, &m)
|
||||
}
|
||||
|
||||
// Value implements the [database/sql/driver.Valuer] interface.
|
||||
func (m Map[V]) Value() (driver.Value, error) {
|
||||
if len(m) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
119
internal/database/type_test.go
Normal file
119
internal/database/type_test.go
Normal file
@ -0,0 +1,119 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMap_Scan(t *testing.T) {
|
||||
type args struct {
|
||||
src any
|
||||
}
|
||||
type res[V any] struct {
|
||||
want Map[V]
|
||||
err bool
|
||||
}
|
||||
type testCase[V any] struct {
|
||||
name string
|
||||
m Map[V]
|
||||
args args
|
||||
res[V]
|
||||
}
|
||||
tests := []testCase[string]{
|
||||
{
|
||||
"null",
|
||||
Map[string]{},
|
||||
args{src: "invalid"},
|
||||
res[string]{
|
||||
want: Map[string]{},
|
||||
err: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"null",
|
||||
Map[string]{},
|
||||
args{src: nil},
|
||||
res[string]{
|
||||
want: Map[string]{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"empty",
|
||||
Map[string]{},
|
||||
args{src: []byte(`{}`)},
|
||||
res[string]{
|
||||
want: Map[string]{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"set",
|
||||
Map[string]{},
|
||||
args{src: []byte(`{"key": "value"}`)},
|
||||
res[string]{
|
||||
want: Map[string]{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.m.Scan(tt.args.src); (err != nil) != tt.res.err {
|
||||
t.Errorf("Scan() error = %v, wantErr %v", err, tt.res.err)
|
||||
}
|
||||
assert.Equal(t, tt.res.want, tt.m)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap_Value(t *testing.T) {
|
||||
type res struct {
|
||||
want driver.Value
|
||||
err bool
|
||||
}
|
||||
type testCase[V any] struct {
|
||||
name string
|
||||
m Map[V]
|
||||
res res
|
||||
}
|
||||
tests := []testCase[string]{
|
||||
{
|
||||
"nil",
|
||||
nil,
|
||||
res{
|
||||
want: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"empty",
|
||||
Map[string]{},
|
||||
res{
|
||||
want: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"set",
|
||||
Map[string]{
|
||||
"key": "value",
|
||||
},
|
||||
res{
|
||||
want: driver.Value([]byte(`{"key":"value"}`)),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.m.Value()
|
||||
if tt.res.err {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
if !tt.res.err {
|
||||
require.NoError(t, err)
|
||||
assert.Equalf(t, tt.res.want, got, "Value()")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type Permissions struct {
|
||||
Permissions []string
|
||||
}
|
||||
@ -21,3 +23,12 @@ func (p *Permissions) appendPermission(ctxID, permission string) {
|
||||
}
|
||||
p.Permissions = append(p.Permissions, permission)
|
||||
}
|
||||
|
||||
type PermissionCheck func(ctx context.Context, permission, orgID, resourceID string) (err error)
|
||||
|
||||
const (
|
||||
PermissionUserWrite = "user.write"
|
||||
PermissionSessionRead = "session.read"
|
||||
PermissionSessionWrite = "session.write"
|
||||
PermissionSessionDelete = "session.delete"
|
||||
)
|
||||
|
9
internal/domain/session.go
Normal file
9
internal/domain/session.go
Normal file
@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
type SessionState int32
|
||||
|
||||
const (
|
||||
SessionStateUnspecified SessionState = iota
|
||||
SessionStateActive
|
||||
SessionStateTerminated
|
||||
)
|
@ -25,3 +25,9 @@ func ExpectID(t *testing.T, id string) *MockGenerator {
|
||||
m.EXPECT().Next().Return(id, nil)
|
||||
return m
|
||||
}
|
||||
|
||||
func NewIDGeneratorExpectError(t *testing.T, err error) *MockGenerator {
|
||||
m := NewMockGenerator(gomock.NewController(t))
|
||||
m.EXPECT().Next().Return("", err)
|
||||
return m
|
||||
}
|
||||
|
@ -180,12 +180,20 @@ func (q *Queries) IsOrgUnique(ctx context.Context, name, domain string) (isUniqu
|
||||
return scan(row)
|
||||
}
|
||||
|
||||
func (q *Queries) ExistsOrg(ctx context.Context, id string) (err error) {
|
||||
func (q *Queries) ExistsOrg(ctx context.Context, id, domain string) (verifiedID string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
_, err = q.OrgByID(ctx, true, id)
|
||||
return err
|
||||
var org *Org
|
||||
if id != "" {
|
||||
org, err = q.OrgByID(ctx, true, id)
|
||||
} else {
|
||||
org, err = q.OrgByVerifiedDomain(ctx, domain)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return org.ID, nil
|
||||
}
|
||||
|
||||
func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) {
|
||||
|
@ -65,6 +65,7 @@ var (
|
||||
NotificationsProjection interface{}
|
||||
NotificationsQuotaProjection interface{}
|
||||
DeviceAuthProjection *deviceAuthProjection
|
||||
SessionProjection *sessionProjection
|
||||
)
|
||||
|
||||
type projection interface {
|
||||
@ -141,6 +142,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es *eventstore.Eventsto
|
||||
SecurityPolicyProjection = newSecurityPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["security_policies"]))
|
||||
NotificationPolicyProjection = newNotificationPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["notification_policies"]))
|
||||
DeviceAuthProjection = newDeviceAuthProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["device_auth"]))
|
||||
SessionProjection = newSessionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["sessions"]))
|
||||
newProjectionsList()
|
||||
return nil
|
||||
}
|
||||
@ -237,5 +239,6 @@ func newProjectionsList() {
|
||||
SecurityPolicyProjection,
|
||||
NotificationPolicyProjection,
|
||||
DeviceAuthProjection,
|
||||
SessionProjection,
|
||||
}
|
||||
}
|
||||
|
221
internal/query/projection/session.go
Normal file
221
internal/query/projection/session.go
Normal file
@ -0,0 +1,221 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
)
|
||||
|
||||
const (
|
||||
SessionsProjectionTable = "projections.sessions"
|
||||
|
||||
SessionColumnID = "id"
|
||||
SessionColumnCreationDate = "creation_date"
|
||||
SessionColumnChangeDate = "change_date"
|
||||
SessionColumnSequence = "sequence"
|
||||
SessionColumnState = "state"
|
||||
SessionColumnResourceOwner = "resource_owner"
|
||||
SessionColumnInstanceID = "instance_id"
|
||||
SessionColumnCreator = "creator"
|
||||
SessionColumnUserID = "user_id"
|
||||
SessionColumnUserCheckedAt = "user_checked_at"
|
||||
SessionColumnPasswordCheckedAt = "password_checked_at"
|
||||
SessionColumnMetadata = "metadata"
|
||||
SessionColumnTokenID = "token_id"
|
||||
)
|
||||
|
||||
type sessionProjection struct {
|
||||
crdb.StatementHandler
|
||||
}
|
||||
|
||||
func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfig) *sessionProjection {
|
||||
p := new(sessionProjection)
|
||||
config.ProjectionName = SessionsProjectionTable
|
||||
config.Reducers = p.reducers()
|
||||
config.InitCheck = crdb.NewMultiTableCheck(
|
||||
crdb.NewTable([]*crdb.Column{
|
||||
crdb.NewColumn(SessionColumnID, crdb.ColumnTypeText),
|
||||
crdb.NewColumn(SessionColumnCreationDate, crdb.ColumnTypeTimestamp),
|
||||
crdb.NewColumn(SessionColumnChangeDate, crdb.ColumnTypeTimestamp),
|
||||
crdb.NewColumn(SessionColumnSequence, crdb.ColumnTypeInt64),
|
||||
crdb.NewColumn(SessionColumnState, crdb.ColumnTypeEnum),
|
||||
crdb.NewColumn(SessionColumnResourceOwner, crdb.ColumnTypeText),
|
||||
crdb.NewColumn(SessionColumnInstanceID, crdb.ColumnTypeText),
|
||||
crdb.NewColumn(SessionColumnCreator, crdb.ColumnTypeText),
|
||||
crdb.NewColumn(SessionColumnUserID, crdb.ColumnTypeText, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnUserCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnPasswordCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()),
|
||||
},
|
||||
crdb.NewPrimaryKey(SessionColumnInstanceID, SessionColumnID),
|
||||
),
|
||||
)
|
||||
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: session.AggregateType,
|
||||
EventRedusers: []handler.EventReducer{
|
||||
{
|
||||
Event: session.AddedType,
|
||||
Reduce: p.reduceSessionAdded,
|
||||
},
|
||||
{
|
||||
Event: session.UserCheckedType,
|
||||
Reduce: p.reduceUserChecked,
|
||||
},
|
||||
{
|
||||
Event: session.PasswordCheckedType,
|
||||
Reduce: p.reducePasswordChecked,
|
||||
},
|
||||
{
|
||||
Event: session.TokenSetType,
|
||||
Reduce: p.reduceTokenSet,
|
||||
},
|
||||
{
|
||||
Event: session.MetadataSetType,
|
||||
Reduce: p.reduceMetadataSet,
|
||||
},
|
||||
{
|
||||
Event: session.TerminateType,
|
||||
Reduce: p.reduceSessionTerminated,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Aggregate: instance.AggregateType,
|
||||
EventRedusers: []handler.EventReducer{
|
||||
{
|
||||
Event: instance.InstanceRemovedEventType,
|
||||
Reduce: reduceInstanceRemovedHelper(SMSColumnInstanceID),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reduceSessionAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.AddedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Sfrgf", "reduce.wrong.event.type %s", session.AddedType)
|
||||
}
|
||||
|
||||
return crdb.NewCreateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(SessionColumnID, e.Aggregate().ID),
|
||||
handler.NewCol(SessionColumnInstanceID, e.Aggregate().InstanceID),
|
||||
handler.NewCol(SessionColumnCreationDate, e.CreationDate()),
|
||||
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(SessionColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||
handler.NewCol(SessionColumnState, domain.SessionStateActive),
|
||||
handler.NewCol(SessionColumnSequence, e.Sequence()),
|
||||
handler.NewCol(SessionColumnCreator, e.User),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reduceUserChecked(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.UserCheckedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-saDg5", "reduce.wrong.event.type %s", session.UserCheckedType)
|
||||
}
|
||||
return crdb.NewUpdateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(SessionColumnSequence, e.Sequence()),
|
||||
handler.NewCol(SessionColumnUserID, e.UserID),
|
||||
handler.NewCol(SessionColumnUserCheckedAt, e.CheckedAt),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(SessionColumnID, e.Aggregate().ID),
|
||||
handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reducePasswordChecked(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.PasswordCheckedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SDgrb", "reduce.wrong.event.type %s", session.PasswordCheckedType)
|
||||
}
|
||||
|
||||
return crdb.NewUpdateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(SessionColumnSequence, e.Sequence()),
|
||||
handler.NewCol(SessionColumnPasswordCheckedAt, e.CheckedAt),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(SessionColumnID, e.Aggregate().ID),
|
||||
handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reduceTokenSet(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.TokenSetEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SAfd3", "reduce.wrong.event.type %s", session.TokenSetType)
|
||||
}
|
||||
|
||||
return crdb.NewUpdateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(SessionColumnSequence, e.Sequence()),
|
||||
handler.NewCol(SessionColumnTokenID, e.TokenID),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(SessionColumnID, e.Aggregate().ID),
|
||||
handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reduceMetadataSet(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.MetadataSetEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SAfd3", "reduce.wrong.event.type %s", session.MetadataSetType)
|
||||
}
|
||||
|
||||
return crdb.NewUpdateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(SessionColumnSequence, e.Sequence()),
|
||||
handler.NewCol(SessionColumnMetadata, e.Metadata),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(SessionColumnID, e.Aggregate().ID),
|
||||
handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reduceSessionTerminated(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.TerminateEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SAftn", "reduce.wrong.event.type %s", session.TerminateType)
|
||||
}
|
||||
|
||||
return crdb.NewDeleteStatement(
|
||||
e,
|
||||
[]handler.Condition{
|
||||
handler.NewCond(SessionColumnID, e.Aggregate().ID),
|
||||
handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
260
internal/query/projection/session_test.go
Normal file
260
internal/query/projection/session_test.go
Normal file
@ -0,0 +1,260 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
)
|
||||
|
||||
func TestSessionProjection_reduces(t *testing.T) {
|
||||
type args struct {
|
||||
event func(t *testing.T) eventstore.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||
want wantReduce
|
||||
}{
|
||||
{
|
||||
name: "instance reduceSessionAdded",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
session.AddedType,
|
||||
session.AggregateType,
|
||||
[]byte(`{}`),
|
||||
), session.AddedEventMapper),
|
||||
},
|
||||
reduce: (&sessionProjection{}).reduceSessionAdded,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("session"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.sessions (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
"ro-id",
|
||||
domain.SessionStateActive,
|
||||
uint64(15),
|
||||
"editor-user",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceUserChecked",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
session.AddedType,
|
||||
session.AggregateType,
|
||||
[]byte(`{
|
||||
"userId": "user-id",
|
||||
"checkedAt": "2023-05-04T00:00:00Z"
|
||||
}`),
|
||||
), session.UserCheckedEventMapper),
|
||||
},
|
||||
reduce: (&sessionProjection{}).reduceUserChecked,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("session"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
"user-id",
|
||||
time.Date(2023, time.May, 4, 0, 0, 0, 0, time.UTC),
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reducePasswordChecked",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
session.AddedType,
|
||||
session.AggregateType,
|
||||
[]byte(`{
|
||||
"checkedAt": "2023-05-04T00:00:00Z"
|
||||
}`),
|
||||
), session.PasswordCheckedEventMapper),
|
||||
},
|
||||
reduce: (&sessionProjection{}).reducePasswordChecked,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("session"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
time.Date(2023, time.May, 4, 0, 0, 0, 0, time.UTC),
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceTokenSet",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
session.TokenSetType,
|
||||
session.AggregateType,
|
||||
[]byte(`{
|
||||
"tokenID": "tokenID"
|
||||
}`),
|
||||
), session.TokenSetEventMapper),
|
||||
},
|
||||
reduce: (&sessionProjection{}).reduceTokenSet,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("session"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
"tokenID",
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceMetadataSet",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
session.MetadataSetType,
|
||||
session.AggregateType,
|
||||
[]byte(`{
|
||||
"metadata": {
|
||||
"key": "dmFsdWU="
|
||||
}
|
||||
}`),
|
||||
), session.MetadataSetEventMapper),
|
||||
},
|
||||
reduce: (&sessionProjection{}).reduceMetadataSet,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("session"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceSessionTerminated",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
session.TerminateType,
|
||||
session.AggregateType,
|
||||
[]byte(`{}`),
|
||||
), session.TerminateEventMapper),
|
||||
},
|
||||
reduce: (&sessionProjection{}).reduceSessionTerminated,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("session"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.sessions WHERE (id = $1) AND (instance_id = $2)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceInstanceRemoved",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(instance.InstanceRemovedEventType),
|
||||
instance.AggregateType,
|
||||
nil,
|
||||
), instance.InstanceRemovedEventMapper),
|
||||
},
|
||||
reduce: reduceInstanceRemovedHelper(SessionColumnInstanceID),
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("instance"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.sessions WHERE (instance_id = $1)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := baseEvent(t)
|
||||
got, err := tt.reduce(event)
|
||||
if !errors.IsErrorInvalidArgument(err) {
|
||||
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||
}
|
||||
|
||||
event = tt.args.event(t)
|
||||
got, err = tt.reduce(event)
|
||||
assertReduce(t, got, err, SessionsProjectionTable, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||
)
|
||||
@ -30,7 +31,8 @@ type Queries struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
client *database.DB
|
||||
|
||||
idpConfigEncryption crypto.EncryptionAlgorithm
|
||||
idpConfigEncryption crypto.EncryptionAlgorithm
|
||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
|
||||
|
||||
DefaultLanguage language.Tag
|
||||
LoginDir http.FileSystem
|
||||
@ -43,7 +45,16 @@ type Queries struct {
|
||||
multifactors domain.MultifactorConfigs
|
||||
}
|
||||
|
||||
func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *database.DB, projections projection.Config, defaults sd.SystemDefaults, idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm, zitadelRoles []authz.RoleMapping) (repo *Queries, err error) {
|
||||
func StartQueries(
|
||||
ctx context.Context,
|
||||
es *eventstore.Eventstore,
|
||||
sqlClient *database.DB,
|
||||
projections projection.Config,
|
||||
defaults sd.SystemDefaults,
|
||||
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
||||
zitadelRoles []authz.RoleMapping,
|
||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
||||
) (repo *Queries, err error) {
|
||||
statikLoginFS, err := fs.NewWithNamespace("login")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start login statik dir")
|
||||
@ -63,6 +74,7 @@ func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *dat
|
||||
LoginTranslationFileContents: make(map[string][]byte),
|
||||
NotificationTranslationFileContents: make(map[string][]byte),
|
||||
zitadelRoles: zitadelRoles,
|
||||
sessionTokenVerifier: sessionTokenVerifier,
|
||||
}
|
||||
iam_repo.RegisterEventMappers(repo.eventstore)
|
||||
usr_repo.RegisterEventMappers(repo.eventstore)
|
||||
@ -71,6 +83,7 @@ func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *dat
|
||||
action.RegisterEventMappers(repo.eventstore)
|
||||
keypair.RegisterEventMappers(repo.eventstore)
|
||||
usergrant.RegisterEventMappers(repo.eventstore)
|
||||
session.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
repo.idpConfigEncryption = idpConfigEncryption
|
||||
repo.multifactors = domain.MultifactorConfigs{
|
||||
|
320
internal/query/session.go
Normal file
320
internal/query/session.go
Normal file
@ -0,0 +1,320 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
errs "errors"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
type Sessions struct {
|
||||
SearchResponse
|
||||
Sessions []*Session
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
Sequence uint64
|
||||
State domain.SessionState
|
||||
ResourceOwner string
|
||||
Creator string
|
||||
UserFactor SessionUserFactor
|
||||
PasswordFactor SessionPasswordFactor
|
||||
Metadata map[string][]byte
|
||||
}
|
||||
|
||||
type SessionUserFactor struct {
|
||||
UserID string
|
||||
UserCheckedAt time.Time
|
||||
LoginName string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
type SessionPasswordFactor struct {
|
||||
PasswordCheckedAt time.Time
|
||||
}
|
||||
|
||||
type SessionsSearchQueries struct {
|
||||
SearchRequest
|
||||
Queries []SearchQuery
|
||||
}
|
||||
|
||||
func (q *SessionsSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
||||
query = q.SearchRequest.toQuery(query)
|
||||
for _, q := range q.Queries {
|
||||
query = q.toQuery(query)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
var (
|
||||
sessionsTable = table{
|
||||
name: projection.SessionsProjectionTable,
|
||||
instanceIDCol: projection.SessionColumnInstanceID,
|
||||
}
|
||||
SessionColumnID = Column{
|
||||
name: projection.SessionColumnID,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnCreationDate = Column{
|
||||
name: projection.SessionColumnCreationDate,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnChangeDate = Column{
|
||||
name: projection.SessionColumnChangeDate,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnSequence = Column{
|
||||
name: projection.SessionColumnSequence,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnState = Column{
|
||||
name: projection.SessionColumnState,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnResourceOwner = Column{
|
||||
name: projection.SessionColumnResourceOwner,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnInstanceID = Column{
|
||||
name: projection.SessionColumnInstanceID,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnCreator = Column{
|
||||
name: projection.SessionColumnCreator,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnUserID = Column{
|
||||
name: projection.SessionColumnUserID,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnUserCheckedAt = Column{
|
||||
name: projection.SessionColumnUserCheckedAt,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnPasswordCheckedAt = Column{
|
||||
name: projection.SessionColumnPasswordCheckedAt,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnMetadata = Column{
|
||||
name: projection.SessionColumnMetadata,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnToken = Column{
|
||||
name: projection.SessionColumnTokenID,
|
||||
table: sessionsTable,
|
||||
}
|
||||
)
|
||||
|
||||
func (q *Queries) SessionByID(ctx context.Context, id, sessionToken string) (_ *Session, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
query, scan := prepareSessionQuery(ctx, q.client)
|
||||
stmt, args, err := query.Where(
|
||||
sq.Eq{
|
||||
SessionColumnID.identifier(): id,
|
||||
SessionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
},
|
||||
).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "QUERY-dn9JW", "Errors.Query.SQLStatement")
|
||||
}
|
||||
|
||||
row := q.client.QueryRowContext(ctx, stmt, args...)
|
||||
session, tokenID, err := scan(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sessionToken == "" {
|
||||
return session, nil
|
||||
}
|
||||
if err := q.sessionTokenVerifier(ctx, sessionToken, session.ID, tokenID); err != nil {
|
||||
return nil, errors.ThrowPermissionDenied(nil, "QUERY-dsfr3", "Errors.PermissionDenied")
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (q *Queries) SearchSessions(ctx context.Context, queries *SessionsSearchQueries) (_ *Sessions, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
query, scan := prepareSessionsQuery(ctx, q.client)
|
||||
stmt, args, err := queries.toQuery(query).
|
||||
Where(sq.Eq{
|
||||
SessionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(err, "QUERY-sn9Jf", "Errors.Query.InvalidRequest")
|
||||
}
|
||||
|
||||
rows, err := q.client.QueryContext(ctx, stmt, args...)
|
||||
if err != nil || rows.Err() != nil {
|
||||
return nil, errors.ThrowInternal(err, "QUERY-Sfg42", "Errors.Internal")
|
||||
}
|
||||
sessions, err := scan(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessions.LatestSequence, err = q.latestSequence(ctx, sessionsTable)
|
||||
return sessions, err
|
||||
}
|
||||
|
||||
func NewSessionIDsSearchQuery(ids []string) (SearchQuery, error) {
|
||||
list := make([]interface{}, len(ids))
|
||||
for i, value := range ids {
|
||||
list[i] = value
|
||||
}
|
||||
return NewListQuery(SessionColumnID, list, ListIn)
|
||||
}
|
||||
|
||||
func NewSessionCreatorSearchQuery(creator string) (SearchQuery, error) {
|
||||
return NewTextQuery(SessionColumnCreator, creator, TextEquals)
|
||||
}
|
||||
|
||||
func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, string, error)) {
|
||||
return sq.Select(
|
||||
SessionColumnID.identifier(),
|
||||
SessionColumnCreationDate.identifier(),
|
||||
SessionColumnChangeDate.identifier(),
|
||||
SessionColumnSequence.identifier(),
|
||||
SessionColumnState.identifier(),
|
||||
SessionColumnResourceOwner.identifier(),
|
||||
SessionColumnCreator.identifier(),
|
||||
SessionColumnUserID.identifier(),
|
||||
SessionColumnUserCheckedAt.identifier(),
|
||||
LoginNameNameCol.identifier(),
|
||||
HumanDisplayNameCol.identifier(),
|
||||
SessionColumnPasswordCheckedAt.identifier(),
|
||||
SessionColumnMetadata.identifier(),
|
||||
SessionColumnToken.identifier(),
|
||||
).From(sessionsTable.identifier()).
|
||||
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
|
||||
LeftJoin(join(HumanUserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Session, string, error) {
|
||||
session := new(Session)
|
||||
|
||||
var (
|
||||
userID sql.NullString
|
||||
userCheckedAt sql.NullTime
|
||||
loginName sql.NullString
|
||||
displayName sql.NullString
|
||||
passwordCheckedAt sql.NullTime
|
||||
metadata database.Map[[]byte]
|
||||
token sql.NullString
|
||||
)
|
||||
|
||||
err := row.Scan(
|
||||
&session.ID,
|
||||
&session.CreationDate,
|
||||
&session.ChangeDate,
|
||||
&session.Sequence,
|
||||
&session.State,
|
||||
&session.ResourceOwner,
|
||||
&session.Creator,
|
||||
&userID,
|
||||
&userCheckedAt,
|
||||
&loginName,
|
||||
&displayName,
|
||||
&passwordCheckedAt,
|
||||
&metadata,
|
||||
&token,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if errs.Is(err, sql.ErrNoRows) {
|
||||
return nil, "", errors.ThrowNotFound(err, "QUERY-SFeaa", "Errors.Session.NotExisting")
|
||||
}
|
||||
return nil, "", errors.ThrowInternal(err, "QUERY-SAder", "Errors.Internal")
|
||||
}
|
||||
|
||||
session.UserFactor.UserID = userID.String
|
||||
session.UserFactor.UserCheckedAt = userCheckedAt.Time
|
||||
session.UserFactor.LoginName = loginName.String
|
||||
session.UserFactor.DisplayName = displayName.String
|
||||
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
|
||||
session.Metadata = metadata
|
||||
|
||||
return session, token.String, nil
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Sessions, error)) {
|
||||
return sq.Select(
|
||||
SessionColumnID.identifier(),
|
||||
SessionColumnCreationDate.identifier(),
|
||||
SessionColumnChangeDate.identifier(),
|
||||
SessionColumnSequence.identifier(),
|
||||
SessionColumnState.identifier(),
|
||||
SessionColumnResourceOwner.identifier(),
|
||||
SessionColumnCreator.identifier(),
|
||||
SessionColumnUserID.identifier(),
|
||||
SessionColumnUserCheckedAt.identifier(),
|
||||
LoginNameNameCol.identifier(),
|
||||
HumanDisplayNameCol.identifier(),
|
||||
SessionColumnPasswordCheckedAt.identifier(),
|
||||
SessionColumnMetadata.identifier(),
|
||||
countColumn.identifier(),
|
||||
).From(sessionsTable.identifier()).
|
||||
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
|
||||
LeftJoin(join(HumanUserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Sessions, error) {
|
||||
sessions := &Sessions{Sessions: []*Session{}}
|
||||
|
||||
for rows.Next() {
|
||||
session := new(Session)
|
||||
|
||||
var (
|
||||
userID sql.NullString
|
||||
userCheckedAt sql.NullTime
|
||||
loginName sql.NullString
|
||||
displayName sql.NullString
|
||||
passwordCheckedAt sql.NullTime
|
||||
metadata database.Map[[]byte]
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
&session.ID,
|
||||
&session.CreationDate,
|
||||
&session.ChangeDate,
|
||||
&session.Sequence,
|
||||
&session.State,
|
||||
&session.ResourceOwner,
|
||||
&session.Creator,
|
||||
&userID,
|
||||
&userCheckedAt,
|
||||
&loginName,
|
||||
&displayName,
|
||||
&passwordCheckedAt,
|
||||
&metadata,
|
||||
&sessions.Count,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "QUERY-SAfeg", "Errors.Internal")
|
||||
}
|
||||
session.UserFactor.UserID = userID.String
|
||||
session.UserFactor.UserCheckedAt = userCheckedAt.Time
|
||||
session.UserFactor.LoginName = loginName.String
|
||||
session.UserFactor.DisplayName = displayName.String
|
||||
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
|
||||
session.Metadata = metadata
|
||||
|
||||
sessions.Sessions = append(sessions.Sessions, session)
|
||||
}
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
}
|
396
internal/query/sessions_test.go
Normal file
396
internal/query/sessions_test.go
Normal file
@ -0,0 +1,396 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
errs "github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions.id,` +
|
||||
` projections.sessions.creation_date,` +
|
||||
` projections.sessions.change_date,` +
|
||||
` projections.sessions.sequence,` +
|
||||
` projections.sessions.state,` +
|
||||
` projections.sessions.resource_owner,` +
|
||||
` projections.sessions.creator,` +
|
||||
` projections.sessions.user_id,` +
|
||||
` projections.sessions.user_checked_at,` +
|
||||
` projections.login_names2.login_name,` +
|
||||
` projections.users8_humans.display_name,` +
|
||||
` projections.sessions.password_checked_at,` +
|
||||
` projections.sessions.metadata,` +
|
||||
` projections.sessions.token_id` +
|
||||
` FROM projections.sessions` +
|
||||
` LEFT JOIN projections.login_names2 ON projections.sessions.user_id = projections.login_names2.user_id AND projections.sessions.instance_id = projections.login_names2.instance_id` +
|
||||
` LEFT JOIN projections.users8_humans ON projections.sessions.user_id = projections.users8_humans.user_id AND projections.sessions.instance_id = projections.users8_humans.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`)
|
||||
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions.id,` +
|
||||
` projections.sessions.creation_date,` +
|
||||
` projections.sessions.change_date,` +
|
||||
` projections.sessions.sequence,` +
|
||||
` projections.sessions.state,` +
|
||||
` projections.sessions.resource_owner,` +
|
||||
` projections.sessions.creator,` +
|
||||
` projections.sessions.user_id,` +
|
||||
` projections.sessions.user_checked_at,` +
|
||||
` projections.login_names2.login_name,` +
|
||||
` projections.users8_humans.display_name,` +
|
||||
` projections.sessions.password_checked_at,` +
|
||||
` projections.sessions.metadata,` +
|
||||
` COUNT(*) OVER ()` +
|
||||
` FROM projections.sessions` +
|
||||
` LEFT JOIN projections.login_names2 ON projections.sessions.user_id = projections.login_names2.user_id AND projections.sessions.instance_id = projections.login_names2.instance_id` +
|
||||
` LEFT JOIN projections.users8_humans ON projections.sessions.user_id = projections.users8_humans.user_id AND projections.sessions.instance_id = projections.users8_humans.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`)
|
||||
|
||||
sessionCols = []string{
|
||||
"id",
|
||||
"creation_date",
|
||||
"change_date",
|
||||
"sequence",
|
||||
"state",
|
||||
"resource_owner",
|
||||
"creator",
|
||||
"user_id",
|
||||
"user_checked_at",
|
||||
"login_name",
|
||||
"display_name",
|
||||
"password_checked_at",
|
||||
"metadata",
|
||||
"token",
|
||||
}
|
||||
|
||||
sessionsCols = []string{
|
||||
"id",
|
||||
"creation_date",
|
||||
"change_date",
|
||||
"sequence",
|
||||
"state",
|
||||
"resource_owner",
|
||||
"creator",
|
||||
"user_id",
|
||||
"user_checked_at",
|
||||
"login_name",
|
||||
"display_name",
|
||||
"password_checked_at",
|
||||
"metadata",
|
||||
"count",
|
||||
}
|
||||
)
|
||||
|
||||
func Test_SessionsPrepare(t *testing.T) {
|
||||
type want struct {
|
||||
sqlExpectations sqlExpectation
|
||||
err checkErr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare interface{}
|
||||
want want
|
||||
object interface{}
|
||||
}{
|
||||
{
|
||||
name: "prepareSessionsQuery no result",
|
||||
prepare: prepareSessionsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueries(
|
||||
expectedSessionsQuery,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
object: &Sessions{Sessions: []*Session{}},
|
||||
},
|
||||
{
|
||||
name: "prepareSessionQuery",
|
||||
prepare: prepareSessionsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueries(
|
||||
expectedSessionsQuery,
|
||||
sessionsCols,
|
||||
[][]driver.Value{
|
||||
{
|
||||
"session-id",
|
||||
testNow,
|
||||
testNow,
|
||||
uint64(20211109),
|
||||
domain.SessionStateActive,
|
||||
"ro",
|
||||
"creator",
|
||||
"user-id",
|
||||
testNow,
|
||||
"login-name",
|
||||
"display-name",
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
object: &Sessions{
|
||||
SearchResponse: SearchResponse{
|
||||
Count: 1,
|
||||
},
|
||||
Sessions: []*Session{
|
||||
{
|
||||
ID: "session-id",
|
||||
CreationDate: testNow,
|
||||
ChangeDate: testNow,
|
||||
Sequence: 20211109,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "ro",
|
||||
Creator: "creator",
|
||||
UserFactor: SessionUserFactor{
|
||||
UserID: "user-id",
|
||||
UserCheckedAt: testNow,
|
||||
LoginName: "login-name",
|
||||
DisplayName: "display-name",
|
||||
},
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
},
|
||||
Metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareSessionsQuery multiple result",
|
||||
prepare: prepareSessionsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueries(
|
||||
expectedSessionsQuery,
|
||||
sessionsCols,
|
||||
[][]driver.Value{
|
||||
{
|
||||
"session-id",
|
||||
testNow,
|
||||
testNow,
|
||||
uint64(20211109),
|
||||
domain.SessionStateActive,
|
||||
"ro",
|
||||
"creator",
|
||||
"user-id",
|
||||
testNow,
|
||||
"login-name",
|
||||
"display-name",
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
{
|
||||
"session-id2",
|
||||
testNow,
|
||||
testNow,
|
||||
uint64(20211109),
|
||||
domain.SessionStateActive,
|
||||
"ro",
|
||||
"creator2",
|
||||
"user-id2",
|
||||
testNow,
|
||||
"login-name2",
|
||||
"display-name2",
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
object: &Sessions{
|
||||
SearchResponse: SearchResponse{
|
||||
Count: 2,
|
||||
},
|
||||
Sessions: []*Session{
|
||||
{
|
||||
ID: "session-id",
|
||||
CreationDate: testNow,
|
||||
ChangeDate: testNow,
|
||||
Sequence: 20211109,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "ro",
|
||||
Creator: "creator",
|
||||
UserFactor: SessionUserFactor{
|
||||
UserID: "user-id",
|
||||
UserCheckedAt: testNow,
|
||||
LoginName: "login-name",
|
||||
DisplayName: "display-name",
|
||||
},
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
},
|
||||
Metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "session-id2",
|
||||
CreationDate: testNow,
|
||||
ChangeDate: testNow,
|
||||
Sequence: 20211109,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "ro",
|
||||
Creator: "creator2",
|
||||
UserFactor: SessionUserFactor{
|
||||
UserID: "user-id2",
|
||||
UserCheckedAt: testNow,
|
||||
LoginName: "login-name2",
|
||||
DisplayName: "display-name2",
|
||||
},
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
},
|
||||
Metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareSessionsQuery sql err",
|
||||
prepare: prepareSessionsQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueryErr(
|
||||
expectedSessionsQuery,
|
||||
sql.ErrConnDone,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errors.Is(err, sql.ErrConnDone) {
|
||||
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SessionPrepare(t *testing.T) {
|
||||
type want struct {
|
||||
sqlExpectations sqlExpectation
|
||||
err checkErr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare interface{}
|
||||
want want
|
||||
object interface{}
|
||||
}{
|
||||
{
|
||||
name: "prepareSessionQuery no result",
|
||||
prepare: prepareSessionQueryTesting(t, ""),
|
||||
want: want{
|
||||
sqlExpectations: mockQueries(
|
||||
expectedSessionQuery,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errs.IsNotFound(err) {
|
||||
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*Session)(nil),
|
||||
},
|
||||
{
|
||||
name: "prepareSessionQuery found",
|
||||
prepare: prepareSessionQueryTesting(t, "tokenID"),
|
||||
want: want{
|
||||
sqlExpectations: mockQuery(
|
||||
expectedSessionQuery,
|
||||
sessionCols,
|
||||
[]driver.Value{
|
||||
"session-id",
|
||||
testNow,
|
||||
testNow,
|
||||
uint64(20211109),
|
||||
domain.SessionStateActive,
|
||||
"ro",
|
||||
"creator",
|
||||
"user-id",
|
||||
testNow,
|
||||
"login-name",
|
||||
"display-name",
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
"tokenID",
|
||||
},
|
||||
),
|
||||
},
|
||||
object: &Session{
|
||||
ID: "session-id",
|
||||
CreationDate: testNow,
|
||||
ChangeDate: testNow,
|
||||
Sequence: 20211109,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "ro",
|
||||
Creator: "creator",
|
||||
UserFactor: SessionUserFactor{
|
||||
UserID: "user-id",
|
||||
UserCheckedAt: testNow,
|
||||
LoginName: "login-name",
|
||||
DisplayName: "display-name",
|
||||
},
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
},
|
||||
Metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareSessionQuery sql err",
|
||||
prepare: prepareSessionQueryTesting(t, ""),
|
||||
want: want{
|
||||
sqlExpectations: mockQueryErr(
|
||||
expectedSessionQuery,
|
||||
sql.ErrConnDone,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errors.Is(err, sql.ErrConnDone) {
|
||||
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSessionQueryTesting(t *testing.T, token string) func(context.Context, prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, error)) {
|
||||
return func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, error)) {
|
||||
builder, scan := prepareSessionQuery(ctx, db)
|
||||
return builder, func(row *sql.Row) (*Session, error) {
|
||||
session, tokenID, err := scan(row)
|
||||
require.Equal(t, tokenID, token)
|
||||
return session, err
|
||||
}
|
||||
}
|
||||
}
|
25
internal/repository/session/aggregate.go
Normal file
25
internal/repository/session/aggregate.go
Normal file
@ -0,0 +1,25 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "session"
|
||||
AggregateVersion = "v1"
|
||||
)
|
||||
|
||||
type Aggregate struct {
|
||||
eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewAggregate(id, resourceOwner string) *Aggregate {
|
||||
return &Aggregate{
|
||||
Aggregate: eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
12
internal/repository/session/eventstore.go
Normal file
12
internal/repository/session/eventstore.go
Normal file
@ -0,0 +1,12 @@
|
||||
package session
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/eventstore"
|
||||
|
||||
func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||
es.RegisterFilterEventMapper(AggregateType, AddedType, AddedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, UserCheckedType, UserCheckedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, PasswordCheckedType, PasswordCheckedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, TerminateType, TerminateEventMapper)
|
||||
}
|
255
internal/repository/session/session.go
Normal file
255
internal/repository/session/session.go
Normal file
@ -0,0 +1,255 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionEventPrefix = "session."
|
||||
AddedType = sessionEventPrefix + "added"
|
||||
UserCheckedType = sessionEventPrefix + "user.checked"
|
||||
PasswordCheckedType = sessionEventPrefix + "password.checked"
|
||||
TokenSetType = sessionEventPrefix + "token.set"
|
||||
MetadataSetType = sessionEventPrefix + "metadata.set"
|
||||
TerminateType = sessionEventPrefix + "terminated"
|
||||
)
|
||||
|
||||
type AddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *AddedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *AddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAddedEvent(ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *AddedEvent {
|
||||
return &AddedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
AddedType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func AddedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
added := &AddedEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
err := json.Unmarshal(event.Data, added)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "SESSION-DG4gn", "unable to unmarshal session added")
|
||||
}
|
||||
|
||||
return added, nil
|
||||
}
|
||||
|
||||
type UserCheckedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
UserID string `json:"userID"`
|
||||
CheckedAt time.Time `json:"checkedAt"`
|
||||
}
|
||||
|
||||
func (e *UserCheckedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *UserCheckedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewUserCheckedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
userID string,
|
||||
checkedAt time.Time,
|
||||
) *UserCheckedEvent {
|
||||
return &UserCheckedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
UserCheckedType,
|
||||
),
|
||||
UserID: userID,
|
||||
CheckedAt: checkedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func UserCheckedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
added := &UserCheckedEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
err := json.Unmarshal(event.Data, added)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "SESSION-DSGn5", "unable to unmarshal user checked")
|
||||
}
|
||||
|
||||
return added, nil
|
||||
}
|
||||
|
||||
type PasswordCheckedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
CheckedAt time.Time `json:"checkedAt"`
|
||||
}
|
||||
|
||||
func (e *PasswordCheckedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *PasswordCheckedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPasswordCheckedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
checkedAt time.Time,
|
||||
) *PasswordCheckedEvent {
|
||||
return &PasswordCheckedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
PasswordCheckedType,
|
||||
),
|
||||
CheckedAt: checkedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func PasswordCheckedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
added := &PasswordCheckedEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
err := json.Unmarshal(event.Data, added)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "SESSION-DGt21", "unable to unmarshal password checked")
|
||||
}
|
||||
|
||||
return added, nil
|
||||
}
|
||||
|
||||
type TokenSetEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
TokenID string `json:"tokenID"`
|
||||
}
|
||||
|
||||
func (e *TokenSetEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *TokenSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTokenSetEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
tokenID string,
|
||||
) *TokenSetEvent {
|
||||
return &TokenSetEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
TokenSetType,
|
||||
),
|
||||
TokenID: tokenID,
|
||||
}
|
||||
}
|
||||
|
||||
func TokenSetEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
added := &TokenSetEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
err := json.Unmarshal(event.Data, added)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "SESSION-Sf3va", "unable to unmarshal token set")
|
||||
}
|
||||
|
||||
return added, nil
|
||||
}
|
||||
|
||||
type MetadataSetEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Metadata map[string][]byte `json:"metadata"`
|
||||
}
|
||||
|
||||
func (e *MetadataSetEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *MetadataSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMetadataSetEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
metadata map[string][]byte,
|
||||
) *MetadataSetEvent {
|
||||
return &MetadataSetEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
MetadataSetType,
|
||||
),
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func MetadataSetEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
added := &MetadataSetEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
err := json.Unmarshal(event.Data, added)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "SESSION-BD21d", "unable to unmarshal metadata set")
|
||||
}
|
||||
|
||||
return added, nil
|
||||
}
|
||||
|
||||
type TerminateEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *TerminateEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *TerminateEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTerminateEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *TerminateEvent {
|
||||
return &TerminateEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
TerminateType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func TerminateEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
return &TerminateEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}, nil
|
||||
}
|
@ -470,6 +470,11 @@ Errors:
|
||||
Execution:
|
||||
StorageFailed: Das Speichern des Action Logs in der Datenbank ist fehlgeschlagen
|
||||
ScanFailed: Das Abfragen der verbrauchten Actions Sekunden ist fehlgeschlagen
|
||||
Session:
|
||||
NotExisting: Session existiert nicht
|
||||
Terminated: Session bereits beendet
|
||||
Token:
|
||||
Invalid: Session Token ist ungültig
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
@ -470,6 +470,11 @@ Errors:
|
||||
Execution:
|
||||
StorageFailed: Storing action execution log to database failed
|
||||
ScanFailed: Querying usage for action execution seconds failed
|
||||
Session:
|
||||
NotExisting: Session does not exist
|
||||
Terminated: Session already terminated
|
||||
Token:
|
||||
Invalid: Session Token is invalid
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
@ -470,6 +470,11 @@ Errors:
|
||||
Execution:
|
||||
StorageFailed: Ha fallado el almacenaje del registro de ejecución de acciones en la base de datos
|
||||
ScanFailed: La consulta de uso de los segundos de ejecuciónde acciones ha fallado
|
||||
Session:
|
||||
NotExisting: La sesión no existe
|
||||
Terminated: Sesión ya terminada
|
||||
Token:
|
||||
Invalid: El identificador de sesión no es válido
|
||||
|
||||
AggregateTypes:
|
||||
action: Acción
|
||||
|
@ -470,6 +470,11 @@ Errors:
|
||||
Execution:
|
||||
StorageFailed: L'enregistrement du journal d'action dans la base de données a échoué
|
||||
ScanFailed: L'interrogation des secondes d'action consommées a échoué
|
||||
Session:
|
||||
NotExisting: La session n'existe pas
|
||||
Terminated: La session est déjà terminée
|
||||
Token:
|
||||
Invalid: Le jeton de session n'est pas valide
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
@ -470,6 +470,11 @@ Errors:
|
||||
Execution:
|
||||
StorageFailed: Il salvataggio del registro delle azioni nel database non è riuscito
|
||||
ScanFailed: La query dei secondi delle azioni utilizzate non è riuscita
|
||||
Session:
|
||||
NotExisting: La sessione non esiste
|
||||
Terminated: Sessione già terminata
|
||||
Token:
|
||||
Invalid: Il token della sessione non è valido
|
||||
|
||||
AggregateTypes:
|
||||
action: Azione
|
||||
|
@ -459,6 +459,11 @@ Errors:
|
||||
Execution:
|
||||
StorageFailed: アクション実行ログのデータベースへの保存に失敗しました
|
||||
ScanFailed: アクション実行時間を取得する使用状況クエリに失敗しました
|
||||
Session:
|
||||
NotExisting: セッションが存在しない
|
||||
Terminated: セッションはすでに終了しています
|
||||
Token:
|
||||
Invalid: セッショントークンが無効です
|
||||
|
||||
AggregateTypes:
|
||||
action: アクション
|
||||
|
@ -470,6 +470,11 @@ Errors:
|
||||
Execution:
|
||||
StorageFailed: Zapisywanie dziennika wykonania akcji do bazy danych nie powiodło się
|
||||
ScanFailed: Zapytanie o użycie dla sekund wykonania akcji nie powiodło się
|
||||
Session:
|
||||
NotExisting: Sesja nie istnieje
|
||||
Terminated: Sesja już zakończona
|
||||
Token:
|
||||
Invalid: Token sesji jest nieprawidłowy
|
||||
|
||||
AggregateTypes:
|
||||
action: Działanie
|
||||
|
@ -470,6 +470,11 @@ Errors:
|
||||
Execution:
|
||||
StorageFailed: 将行动执行日志存储到数据库失败
|
||||
ScanFailed: Q查询动作执行秒数的使用情况失败
|
||||
Session:
|
||||
NotExisting: 会话不存在
|
||||
Terminated: 会话已经终止
|
||||
Token:
|
||||
Invalid: 会话令牌是无效的
|
||||
|
||||
AggregateTypes:
|
||||
action: 动作
|
||||
|
@ -1,5 +0,0 @@
|
||||
package user
|
||||
|
||||
func (r *AddHumanUserRequest) AuthContext() string {
|
||||
return r.GetOrganisation().GetOrgId()
|
||||
}
|
@ -11,9 +11,35 @@ import "validate/validate.proto";
|
||||
message Organisation {
|
||||
oneof org {
|
||||
string org_id = 1;
|
||||
string org_domain = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ListQuery {
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
|
||||
json_schema: {
|
||||
title: "General List Query"
|
||||
description: "Object unspecific list filters like offset, limit and asc/desc."
|
||||
}
|
||||
};
|
||||
uint64 offset = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"0\"";
|
||||
}
|
||||
];
|
||||
uint32 limit = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "100";
|
||||
description: "Maximum amount of events returned. The default is set to 1000 in https://github.com/zitadel/zitadel/blob/new-eventstore/cmd/zitadel/startup.yaml. If the limit exceeds the maximum configured ZITADEL will throw an error. If no limit is present the default is taken.";
|
||||
}
|
||||
];
|
||||
bool asc = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "default is descending"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message Details {
|
||||
//sequence represents the order of events. It's always counting
|
||||
//
|
||||
@ -38,3 +64,21 @@ message Details {
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message ListDetails {
|
||||
uint64 total_result = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"2\"";
|
||||
}
|
||||
];
|
||||
uint64 processed_sequence = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"267831\"";
|
||||
}
|
||||
];
|
||||
google.protobuf.Timestamp timestamp = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "the last time the projection got updated"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@ -2,11 +2,90 @@ syntax = "proto3";
|
||||
|
||||
package zitadel.session.v2alpha;
|
||||
|
||||
import "zitadel/user/v2alpha/user.proto";
|
||||
import "google/api/field_behavior.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha;session";
|
||||
|
||||
message Session {
|
||||
string id = 1;
|
||||
zitadel.user.v2alpha.User user = 2;
|
||||
string id = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"id of the session\"";
|
||||
}
|
||||
];
|
||||
google.protobuf.Timestamp creation_date = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"time when the session was created\"";
|
||||
}
|
||||
];
|
||||
google.protobuf.Timestamp change_date = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"time when the session was last updated\"";
|
||||
}
|
||||
];
|
||||
uint64 sequence = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"sequence of the session\"";
|
||||
}
|
||||
];
|
||||
Factors factors = 5 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"checked factors of the session, e.g. the user, password and more\"";
|
||||
}
|
||||
];
|
||||
map<string, bytes> metadata = 6 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"custom key value list\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message Factors {
|
||||
UserFactor user = 1;
|
||||
PasswordFactor password = 2;
|
||||
}
|
||||
|
||||
message UserFactor {
|
||||
google.protobuf.Timestamp verified_at = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"time when the user was last checked\"";
|
||||
}
|
||||
];
|
||||
string id = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"id of the checked user\"";
|
||||
}
|
||||
];
|
||||
string login_name = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"login name of the checked user\"";
|
||||
}
|
||||
];
|
||||
string display_name = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"display name of the checked user\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message PasswordFactor {
|
||||
google.protobuf.Timestamp verified_at = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"time when the password was last checked\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SearchQuery {
|
||||
oneof query {
|
||||
option (validate.required) = true;
|
||||
|
||||
IDsQuery ids_query = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message IDsQuery {
|
||||
repeated string ids = 1;
|
||||
}
|
||||
|
@ -3,21 +3,82 @@ syntax = "proto3";
|
||||
package zitadel.session.v2alpha;
|
||||
|
||||
|
||||
import "zitadel/object/v2alpha/object.proto";
|
||||
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||
import "zitadel/session/v2alpha/session.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/field_behavior.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha;session";
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||
info: {
|
||||
title: "Session Service";
|
||||
version: "2.0-alpha";
|
||||
description: "This API is intended to manage sessions in a ZITADEL instance. This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.";
|
||||
contact:{
|
||||
name: "ZITADEL"
|
||||
url: "https://zitadel.com"
|
||||
email: "hi@zitadel.com"
|
||||
}
|
||||
license: {
|
||||
name: "Apache 2.0",
|
||||
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
|
||||
};
|
||||
};
|
||||
schemes: HTTPS;
|
||||
schemes: HTTP;
|
||||
|
||||
consumes: "application/json";
|
||||
consumes: "application/grpc";
|
||||
|
||||
produces: "application/json";
|
||||
produces: "application/grpc";
|
||||
|
||||
consumes: "application/grpc-web+proto";
|
||||
produces: "application/grpc-web+proto";
|
||||
|
||||
host: "$ZITADEL_DOMAIN";
|
||||
base_path: "/";
|
||||
|
||||
external_docs: {
|
||||
description: "Detailed information about ZITADEL",
|
||||
url: "https://zitadel.com/docs"
|
||||
}
|
||||
|
||||
responses: {
|
||||
key: "403";
|
||||
value: {
|
||||
description: "Returned when the user does not have permission to access the resource.";
|
||||
schema: {
|
||||
json_schema: {
|
||||
ref: "#/definitions/rpcStatus";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
key: "404";
|
||||
value: {
|
||||
description: "Returned when the resource does not exist.";
|
||||
schema: {
|
||||
json_schema: {
|
||||
ref: "#/definitions/rpcStatus";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
service SessionService {
|
||||
|
||||
// GetSession is to demonstrate an authenticated request, where the authenticated user (usage of another grpc package) is returned
|
||||
//
|
||||
// this request is subject to change and currently used for demonstration only
|
||||
rpc GetSession (GetSessionRequest) returns (GetSessionResponse) {
|
||||
// Search sessions
|
||||
rpc ListSessions (ListSessionsRequest) returns (ListSessionsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v2alpha/sessions/{id}"
|
||||
post: "/v2alpha/sessions/_search"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
@ -25,12 +86,280 @@ service SessionService {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Search sessions";
|
||||
description: "Search for sessions"
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
responses: {
|
||||
key: "400";
|
||||
value: {
|
||||
description: "invalid list query";
|
||||
schema: {
|
||||
json_schema: {
|
||||
ref: "#/definitions/rpcStatus";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// GetSession a session
|
||||
rpc GetSession (GetSessionRequest) returns (GetSessionResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v2alpha/sessions/{session_id}"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Get a session";
|
||||
description: "Get a session and all its information like the time of the user or password verification"
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Create a new session
|
||||
rpc CreateSession (CreateSessionRequest) returns (CreateSessionResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2alpha/sessions"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
http_response: {
|
||||
success_code: 201
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Create a new session";
|
||||
description: "Create a new session. A token will be returned, which is required for further updates of the session."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Update a session
|
||||
rpc SetSession (SetSessionRequest) returns (SetSessionResponse) {
|
||||
option (google.api.http) = {
|
||||
patch: "/v2alpha/sessions/{session_id}"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Update an existing session";
|
||||
description: "Update an existing session with new information."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Terminate a session
|
||||
rpc DeleteSession (DeleteSessionRequest) returns (DeleteSessionResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v2alpha/sessions/{session_id}"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Terminate an existing session";
|
||||
description: "Terminate your own session or if granted any other session."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message ListSessionsRequest{
|
||||
zitadel.object.v2alpha.ListQuery query = 1;
|
||||
repeated SearchQuery queries = 2;
|
||||
}
|
||||
|
||||
message ListSessionsResponse{
|
||||
zitadel.object.v2alpha.ListDetails details = 1;
|
||||
repeated Session sessions = 2;
|
||||
}
|
||||
|
||||
message GetSessionRequest{
|
||||
string id = 1;
|
||||
string session_id = 1;
|
||||
optional string session_token = 2;
|
||||
}
|
||||
message GetSessionResponse{
|
||||
Session session = 1;
|
||||
}
|
||||
|
||||
message CreateSessionRequest{
|
||||
Checks checks = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"Check for user and password. Successful checks will be stated as factors on the session.\"";
|
||||
}
|
||||
];
|
||||
map<string, bytes> metadata = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"custom key value list to be stored on the session\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message CreateSessionResponse{
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
string session_id = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"id of the session\"";
|
||||
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
|
||||
}
|
||||
];
|
||||
string session_token = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"token of the session, which is required for further updates of the session or the request other resources\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetSessionRequest{
|
||||
string session_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
description: "\"id of the session to update\"";
|
||||
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
|
||||
}
|
||||
];
|
||||
string session_token = 2 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
description: "\"token of the session, previously returned on the create / update request\"";
|
||||
}
|
||||
];
|
||||
Checks checks = 3[
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"Check for user and password. Successful checks will be stated as factors on the session.\"";
|
||||
}
|
||||
];
|
||||
map<string, bytes> metadata = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"custom key value list to be stored on the session\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetSessionResponse{
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
string session_token = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"token of the session, which is required for further updates of the session or the request other resources\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message DeleteSessionRequest{
|
||||
string session_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
description: "\"id of the session to terminate\"";
|
||||
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
|
||||
}
|
||||
];
|
||||
optional string session_token = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"token of the session, previously returned on the create / update request\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message DeleteSessionResponse{
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message Checks {
|
||||
optional CheckUser user = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"checks the user and updates the session on success\"";
|
||||
}
|
||||
];
|
||||
optional CheckPassword password = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"Checks the password and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message CheckUser {
|
||||
oneof search {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
|
||||
}
|
||||
];
|
||||
string login_name = 2 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"mini@mouse.com\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
message CheckPassword {
|
||||
string password = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"V3ryS3cure!\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user