mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-25 20:48:33 +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,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -52,6 +52,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -50,6 +50,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
|
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
|
||||||
"github.com/zitadel/zitadel/internal/database"
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/id"
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
"github.com/zitadel/zitadel/internal/logstore"
|
"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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot start queries: %w", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error starting authz repo: %w", err)
|
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)
|
storage, err := config.AssetStorage.NewStorage(dbClient.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -165,7 +183,8 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
|||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
keys.SAML,
|
keys.SAML,
|
||||||
&http.Client{},
|
&http.Client{},
|
||||||
authZRepo,
|
permissionCheck,
|
||||||
|
sessionTokenVerifier,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot start commands: %w", err)
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -239,6 +273,7 @@ func startAPIs(
|
|||||||
keys *encryptionKeys,
|
keys *encryptionKeys,
|
||||||
quotaQuerier logstore.QuotaQuerier,
|
quotaQuerier logstore.QuotaQuerier,
|
||||||
usageReporter logstore.UsageReporter,
|
usageReporter logstore.UsageReporter,
|
||||||
|
permissionCheck domain.PermissionCheck,
|
||||||
) error {
|
) error {
|
||||||
repo := struct {
|
repo := struct {
|
||||||
authz_repo.Repository
|
authz_repo.Repository
|
||||||
@ -294,7 +329,7 @@ func startAPIs(
|
|||||||
if err := apis.RegisterService(ctx, user.CreateServer(commands, queries, keys.User)); err != nil {
|
if err := apis.RegisterService(ctx, user.CreateServer(commands, queries, keys.User)); err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
|
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:
|
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
|
```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
|
## Call the System API
|
||||||
|
@ -266,6 +266,13 @@ module.exports = {
|
|||||||
sidebarOptions: {
|
sidebarOptions: {
|
||||||
groupPathsBy: "tag",
|
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"),
|
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",
|
type: "category",
|
||||||
label: "Assets",
|
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/otel/exporters/otlp/otlptrace v1.14.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||||
golang.org/x/mod v0.10.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
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
@ -14,11 +14,16 @@ const (
|
|||||||
authenticated = "authenticated"
|
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)
|
ctx, span := tracing.NewServerInterceptorSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ const (
|
|||||||
MemberTypeIam
|
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)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
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 {
|
if err := checkOrigin(ctx, origins); err != nil {
|
||||||
return CtxData{}, err
|
return CtxData{}, err
|
||||||
}
|
}
|
||||||
if orgID == "" {
|
if orgID == "" && orgDomain == "" {
|
||||||
orgID = resourceOwner
|
orgID = resourceOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.ExistsOrg(ctx, orgID)
|
verifiedOrgID, err := t.ExistsOrg(ctx, orgID, orgDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = retry(func() error {
|
err = retry(func() error {
|
||||||
return t.ExistsOrg(ctx, orgID)
|
verifiedOrgID, err = t.ExistsOrg(ctx, orgID, orgDomain)
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CtxData{}, errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
|
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{
|
return CtxData{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
OrgID: orgID,
|
OrgID: verifiedOrgID,
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
AgentID: agentID,
|
AgentID: agentID,
|
||||||
PreferredLanguage: prefLang,
|
PreferredLanguage: prefLang,
|
||||||
|
@ -7,12 +7,8 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) {
|
||||||
ctxData := GetCtxData(ctx)
|
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID)
|
||||||
if allowSelf && ctxData.UserID == resourceID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, ctxData, orgID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,8 @@ func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, client
|
|||||||
return "", nil, nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID string) error {
|
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
|
||||||
return nil
|
return orgID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
|
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
|
||||||
|
@ -3,6 +3,8 @@ package authz
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -17,7 +19,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BearerPrefix = "Bearer "
|
BearerPrefix = "Bearer "
|
||||||
|
SessionTokenFormat = "sess_%s:%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenVerifier struct {
|
type TokenVerifier struct {
|
||||||
@ -36,7 +39,7 @@ type authZRepo interface {
|
|||||||
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
|
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
|
||||||
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
||||||
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err 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) {
|
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)
|
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)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
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) {
|
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)
|
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"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,3 +18,21 @@ func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
|||||||
}
|
}
|
||||||
return 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")
|
return nil, status.Error(codes.Unauthenticated, "auth header missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var orgDomain string
|
||||||
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
|
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
|
||||||
if o, ok := req.(OrganisationFromRequest); ok {
|
if o, ok := req.(OrganisationFromRequest); ok {
|
||||||
orgID = o.OrganisationFromRequest().GetOrgId()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (v *verifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
|
||||||
return "", nil, nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
func (v *verifierMock) ExistsOrg(ctx context.Context, orgID string) error {
|
func (v *verifierMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
|
||||||
return nil
|
return orgID, nil
|
||||||
}
|
}
|
||||||
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
|
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
|
@ -6,16 +6,18 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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)
|
var _ session.SessionServiceServer = (*Server)(nil)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
session.UnimplementedSessionServiceServer
|
session.UnimplementedSessionServiceServer
|
||||||
command *command.Commands
|
command *command.Commands
|
||||||
query *query.Queries
|
query *query.Queries
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct{}
|
type Config struct{}
|
||||||
@ -23,10 +25,12 @@ type Config struct{}
|
|||||||
func CreateServer(
|
func CreateServer(
|
||||||
command *command.Commands,
|
command *command.Commands,
|
||||||
query *query.Queries,
|
query *query.Queries,
|
||||||
|
checkPermission domain.PermissionCheck,
|
||||||
) *Server {
|
) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
command: command,
|
command: command,
|
||||||
query: query,
|
query: query,
|
||||||
|
checkPermission: checkPermission,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,16 +3,260 @@ package session
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
"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) {
|
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{
|
return &session.GetSessionResponse{
|
||||||
Session: &session.Session{
|
Session: sessionToPb(res),
|
||||||
Id: req.Id,
|
|
||||||
User: &user.User{Id: authz.GetCtxData(ctx).UserID},
|
|
||||||
},
|
|
||||||
}, nil
|
}, 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/command"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
"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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
orgID := req.GetOrganisation().GetOrgId()
|
orgID := authz.GetCtxData(ctx).OrgID
|
||||||
if orgID == "" {
|
|
||||||
orgID = authz.GetCtxData(ctx).OrgID
|
|
||||||
}
|
|
||||||
err = s.command.AddHuman(ctx, orgID, human, false)
|
err = s.command.AddHuman(ctx, orgID, human, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -62,7 +62,7 @@ func authorize(r *http.Request, verifier *authz.TokenVerifier, authConfig authz.
|
|||||||
return nil, errors.New("auth header missing")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
|
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
|
||||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/session"
|
||||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||||
usr_grant_repo "github.com/zitadel/zitadel/internal/repository/usergrant"
|
usr_grant_repo "github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||||
"github.com/zitadel/zitadel/internal/static"
|
"github.com/zitadel/zitadel/internal/static"
|
||||||
@ -29,7 +30,7 @@ import (
|
|||||||
type Commands struct {
|
type Commands struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|
||||||
checkPermission permissionCheck
|
checkPermission domain.PermissionCheck
|
||||||
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||||
|
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
@ -50,6 +51,8 @@ type Commands struct {
|
|||||||
domainVerificationAlg crypto.EncryptionAlgorithm
|
domainVerificationAlg crypto.EncryptionAlgorithm
|
||||||
domainVerificationGenerator crypto.Generator
|
domainVerificationGenerator crypto.Generator
|
||||||
domainVerificationValidator func(domain, token, verifier string, checkType api_http.CheckType) error
|
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
|
multifactors domain.MultifactorConfigs
|
||||||
webauthnConfig *webauthn_helper.Config
|
webauthnConfig *webauthn_helper.Config
|
||||||
@ -71,24 +74,21 @@ func StartCommands(
|
|||||||
externalDomain string,
|
externalDomain string,
|
||||||
externalSecure bool,
|
externalSecure bool,
|
||||||
externalPort uint16,
|
externalPort uint16,
|
||||||
idpConfigEncryption,
|
idpConfigEncryption, otpEncryption, smtpEncryption, smsEncryption, userEncryption, domainVerificationEncryption, oidcEncryption, samlEncryption crypto.EncryptionAlgorithm,
|
||||||
otpEncryption,
|
|
||||||
smtpEncryption,
|
|
||||||
smsEncryption,
|
|
||||||
userEncryption,
|
|
||||||
domainVerificationEncryption,
|
|
||||||
oidcEncryption,
|
|
||||||
samlEncryption crypto.EncryptionAlgorithm,
|
|
||||||
httpClient *http.Client,
|
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) {
|
) (repo *Commands, err error) {
|
||||||
if externalDomain == "" {
|
if externalDomain == "" {
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
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{
|
repo = &Commands{
|
||||||
eventstore: es,
|
eventstore: es,
|
||||||
static: staticStore,
|
static: staticStore,
|
||||||
idGenerator: id.SonyFlakeGenerator(),
|
idGenerator: idGenerator,
|
||||||
zitadelRoles: zitadelRoles,
|
zitadelRoles: zitadelRoles,
|
||||||
externalDomain: externalDomain,
|
externalDomain: externalDomain,
|
||||||
externalSecure: externalSecure,
|
externalSecure: externalSecure,
|
||||||
@ -107,10 +107,10 @@ func StartCommands(
|
|||||||
certificateAlgorithm: samlEncryption,
|
certificateAlgorithm: samlEncryption,
|
||||||
webauthnConfig: webAuthN,
|
webauthnConfig: webAuthN,
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
checkPermission: permissionCheck,
|
||||||
return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf)
|
newEmailCode: newEmailCode,
|
||||||
},
|
sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg),
|
||||||
newEmailCode: newEmailCode,
|
sessionTokenVerifier: sessionTokenVerifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
instance_repo.RegisterEventMappers(repo.eventstore)
|
instance_repo.RegisterEventMappers(repo.eventstore)
|
||||||
@ -121,6 +121,7 @@ func StartCommands(
|
|||||||
keypair.RegisterEventMappers(repo.eventstore)
|
keypair.RegisterEventMappers(repo.eventstore)
|
||||||
action.RegisterEventMappers(repo.eventstore)
|
action.RegisterEventMappers(repo.eventstore)
|
||||||
quota.RegisterEventMappers(repo.eventstore)
|
quota.RegisterEventMappers(repo.eventstore)
|
||||||
|
session.RegisterEventMappers(repo.eventstore)
|
||||||
|
|
||||||
repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
||||||
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)
|
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
|
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
|
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"
|
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||||
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||||
)
|
)
|
||||||
@ -38,6 +40,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
|
|||||||
usergrant.RegisterEventMappers(es)
|
usergrant.RegisterEventMappers(es)
|
||||||
key_repo.RegisterEventMappers(es)
|
key_repo.RegisterEventMappers(es)
|
||||||
action_repo.RegisterEventMappers(es)
|
action_repo.RegisterEventMappers(es)
|
||||||
|
session.RegisterEventMappers(es)
|
||||||
return es
|
return es
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,6 +128,11 @@ func expectFilter(events ...*repository.Event) expect {
|
|||||||
m.ExpectFilterEvents(events...)
|
m.ExpectFilterEvents(events...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func expectFilterError(err error) expect {
|
||||||
|
return func(m *mock.MockRepository) {
|
||||||
|
m.ExpectFilterEventsError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func expectFilterOrgDomainNotFound() expect {
|
func expectFilterOrgDomainNotFound() expect {
|
||||||
return func(m *mock.MockRepository) {
|
return func(m *mock.MockRepository) {
|
||||||
@ -250,14 +258,14 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockPermissionCheckAllowed() permissionCheck {
|
func newMockPermissionCheckAllowed() domain.PermissionCheck {
|
||||||
return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockPermissionCheckNotAllowed() permissionCheck {
|
func newMockPermissionCheckNotAllowed() domain.PermissionCheck {
|
||||||
return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||||
return errors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")
|
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/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
@ -42,7 +43,7 @@ func (c *Commands) ChangeUserEmailVerified(ctx context.Context, userID, resource
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, true); err != nil {
|
if authz.GetCtxData(ctx).UserID != userID {
|
||||||
return nil, err
|
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 {
|
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -24,7 +24,7 @@ import (
|
|||||||
func TestCommands_ChangeUserEmail(t *testing.T) {
|
func TestCommands_ChangeUserEmail(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
checkPermission permissionCheck
|
checkPermission domain.PermissionCheck
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
userID string
|
userID string
|
||||||
@ -174,7 +174,7 @@ func TestCommands_ChangeUserEmail(t *testing.T) {
|
|||||||
func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) {
|
func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
checkPermission permissionCheck
|
checkPermission domain.PermissionCheck
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
userID string
|
userID string
|
||||||
@ -300,7 +300,7 @@ func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) {
|
|||||||
func TestCommands_ChangeUserEmailReturnCode(t *testing.T) {
|
func TestCommands_ChangeUserEmailReturnCode(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
checkPermission permissionCheck
|
checkPermission domain.PermissionCheck
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
userID string
|
userID string
|
||||||
@ -410,7 +410,7 @@ func TestCommands_ChangeUserEmailReturnCode(t *testing.T) {
|
|||||||
func TestCommands_ChangeUserEmailVerified(t *testing.T) {
|
func TestCommands_ChangeUserEmailVerified(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
checkPermission permissionCheck
|
checkPermission domain.PermissionCheck
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
userID string
|
userID string
|
||||||
@ -569,7 +569,7 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) {
|
|||||||
func TestCommands_changeUserEmailWithGenerator(t *testing.T) {
|
func TestCommands_changeUserEmailWithGenerator(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
checkPermission permissionCheck
|
checkPermission domain.PermissionCheck
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
userID string
|
userID string
|
||||||
|
@ -2,13 +2,14 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/jackc/pgtype"
|
"github.com/jackc/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StringArray []string
|
type StringArray []string
|
||||||
|
|
||||||
// Scan implements the `database/sql.Scanner` interface.
|
// Scan implements the [database/sql.Scanner] interface.
|
||||||
func (s *StringArray) Scan(src any) error {
|
func (s *StringArray) Scan(src any) error {
|
||||||
array := new(pgtype.TextArray)
|
array := new(pgtype.TextArray)
|
||||||
if err := array.Scan(src); err != nil {
|
if err := array.Scan(src); err != nil {
|
||||||
@ -20,7 +21,7 @@ func (s *StringArray) Scan(src any) error {
|
|||||||
return nil
|
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) {
|
func (s StringArray) Value() (driver.Value, error) {
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -40,7 +41,7 @@ type enumField interface {
|
|||||||
|
|
||||||
type EnumArray[F enumField] []F
|
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 {
|
func (s *EnumArray[F]) Scan(src any) error {
|
||||||
array := new(pgtype.Int2Array)
|
array := new(pgtype.Int2Array)
|
||||||
if err := array.Scan(src); err != nil {
|
if err := array.Scan(src); err != nil {
|
||||||
@ -57,7 +58,7 @@ func (s *EnumArray[F]) Scan(src any) error {
|
|||||||
return nil
|
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) {
|
func (s EnumArray[F]) Value() (driver.Value, error) {
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -70,3 +71,25 @@ func (s EnumArray[F]) Value() (driver.Value, error) {
|
|||||||
|
|
||||||
return array.Value()
|
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
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
type Permissions struct {
|
type Permissions struct {
|
||||||
Permissions []string
|
Permissions []string
|
||||||
}
|
}
|
||||||
@ -21,3 +23,12 @@ func (p *Permissions) appendPermission(ctxID, permission string) {
|
|||||||
}
|
}
|
||||||
p.Permissions = append(p.Permissions, permission)
|
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)
|
m.EXPECT().Next().Return(id, nil)
|
||||||
return m
|
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)
|
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)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
_, err = q.OrgByID(ctx, true, id)
|
var org *Org
|
||||||
return err
|
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) {
|
func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) {
|
||||||
|
@ -65,6 +65,7 @@ var (
|
|||||||
NotificationsProjection interface{}
|
NotificationsProjection interface{}
|
||||||
NotificationsQuotaProjection interface{}
|
NotificationsQuotaProjection interface{}
|
||||||
DeviceAuthProjection *deviceAuthProjection
|
DeviceAuthProjection *deviceAuthProjection
|
||||||
|
SessionProjection *sessionProjection
|
||||||
)
|
)
|
||||||
|
|
||||||
type projection interface {
|
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"]))
|
SecurityPolicyProjection = newSecurityPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["security_policies"]))
|
||||||
NotificationPolicyProjection = newNotificationPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["notification_policies"]))
|
NotificationPolicyProjection = newNotificationPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["notification_policies"]))
|
||||||
DeviceAuthProjection = newDeviceAuthProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["device_auth"]))
|
DeviceAuthProjection = newDeviceAuthProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["device_auth"]))
|
||||||
|
SessionProjection = newSessionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["sessions"]))
|
||||||
newProjectionsList()
|
newProjectionsList()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -237,5 +239,6 @@ func newProjectionsList() {
|
|||||||
SecurityPolicyProjection,
|
SecurityPolicyProjection,
|
||||||
NotificationPolicyProjection,
|
NotificationPolicyProjection,
|
||||||
DeviceAuthProjection,
|
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/keypair"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
"github.com/zitadel/zitadel/internal/repository/project"
|
"github.com/zitadel/zitadel/internal/repository/project"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/session"
|
||||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||||
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
"github.com/zitadel/zitadel/internal/repository/usergrant"
|
||||||
)
|
)
|
||||||
@ -30,7 +31,8 @@ type Queries struct {
|
|||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
client *database.DB
|
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
|
DefaultLanguage language.Tag
|
||||||
LoginDir http.FileSystem
|
LoginDir http.FileSystem
|
||||||
@ -43,7 +45,16 @@ type Queries struct {
|
|||||||
multifactors domain.MultifactorConfigs
|
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")
|
statikLoginFS, err := fs.NewWithNamespace("login")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to start login statik dir")
|
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),
|
LoginTranslationFileContents: make(map[string][]byte),
|
||||||
NotificationTranslationFileContents: make(map[string][]byte),
|
NotificationTranslationFileContents: make(map[string][]byte),
|
||||||
zitadelRoles: zitadelRoles,
|
zitadelRoles: zitadelRoles,
|
||||||
|
sessionTokenVerifier: sessionTokenVerifier,
|
||||||
}
|
}
|
||||||
iam_repo.RegisterEventMappers(repo.eventstore)
|
iam_repo.RegisterEventMappers(repo.eventstore)
|
||||||
usr_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)
|
action.RegisterEventMappers(repo.eventstore)
|
||||||
keypair.RegisterEventMappers(repo.eventstore)
|
keypair.RegisterEventMappers(repo.eventstore)
|
||||||
usergrant.RegisterEventMappers(repo.eventstore)
|
usergrant.RegisterEventMappers(repo.eventstore)
|
||||||
|
session.RegisterEventMappers(repo.eventstore)
|
||||||
|
|
||||||
repo.idpConfigEncryption = idpConfigEncryption
|
repo.idpConfigEncryption = idpConfigEncryption
|
||||||
repo.multifactors = domain.MultifactorConfigs{
|
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:
|
Execution:
|
||||||
StorageFailed: Das Speichern des Action Logs in der Datenbank ist fehlgeschlagen
|
StorageFailed: Das Speichern des Action Logs in der Datenbank ist fehlgeschlagen
|
||||||
ScanFailed: Das Abfragen der verbrauchten Actions Sekunden 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:
|
AggregateTypes:
|
||||||
action: Action
|
action: Action
|
||||||
|
@ -470,6 +470,11 @@ Errors:
|
|||||||
Execution:
|
Execution:
|
||||||
StorageFailed: Storing action execution log to database failed
|
StorageFailed: Storing action execution log to database failed
|
||||||
ScanFailed: Querying usage for action execution seconds 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:
|
AggregateTypes:
|
||||||
action: Action
|
action: Action
|
||||||
|
@ -470,6 +470,11 @@ Errors:
|
|||||||
Execution:
|
Execution:
|
||||||
StorageFailed: Ha fallado el almacenaje del registro de ejecución de acciones en la base de datos
|
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
|
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:
|
AggregateTypes:
|
||||||
action: Acción
|
action: Acción
|
||||||
|
@ -470,6 +470,11 @@ Errors:
|
|||||||
Execution:
|
Execution:
|
||||||
StorageFailed: L'enregistrement du journal d'action dans la base de données a échoué
|
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é
|
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:
|
AggregateTypes:
|
||||||
action: Action
|
action: Action
|
||||||
|
@ -470,6 +470,11 @@ Errors:
|
|||||||
Execution:
|
Execution:
|
||||||
StorageFailed: Il salvataggio del registro delle azioni nel database non è riuscito
|
StorageFailed: Il salvataggio del registro delle azioni nel database non è riuscito
|
||||||
ScanFailed: La query dei secondi delle azioni utilizzate non è riuscita
|
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:
|
AggregateTypes:
|
||||||
action: Azione
|
action: Azione
|
||||||
|
@ -459,6 +459,11 @@ Errors:
|
|||||||
Execution:
|
Execution:
|
||||||
StorageFailed: アクション実行ログのデータベースへの保存に失敗しました
|
StorageFailed: アクション実行ログのデータベースへの保存に失敗しました
|
||||||
ScanFailed: アクション実行時間を取得する使用状況クエリに失敗しました
|
ScanFailed: アクション実行時間を取得する使用状況クエリに失敗しました
|
||||||
|
Session:
|
||||||
|
NotExisting: セッションが存在しない
|
||||||
|
Terminated: セッションはすでに終了しています
|
||||||
|
Token:
|
||||||
|
Invalid: セッショントークンが無効です
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: アクション
|
action: アクション
|
||||||
|
@ -470,6 +470,11 @@ Errors:
|
|||||||
Execution:
|
Execution:
|
||||||
StorageFailed: Zapisywanie dziennika wykonania akcji do bazy danych nie powiodło się
|
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ę
|
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:
|
AggregateTypes:
|
||||||
action: Działanie
|
action: Działanie
|
||||||
|
@ -470,6 +470,11 @@ Errors:
|
|||||||
Execution:
|
Execution:
|
||||||
StorageFailed: 将行动执行日志存储到数据库失败
|
StorageFailed: 将行动执行日志存储到数据库失败
|
||||||
ScanFailed: Q查询动作执行秒数的使用情况失败
|
ScanFailed: Q查询动作执行秒数的使用情况失败
|
||||||
|
Session:
|
||||||
|
NotExisting: 会话不存在
|
||||||
|
Terminated: 会话已经终止
|
||||||
|
Token:
|
||||||
|
Invalid: 会话令牌是无效的
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: 动作
|
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 {
|
message Organisation {
|
||||||
oneof org {
|
oneof org {
|
||||||
string org_id = 1;
|
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 {
|
message Details {
|
||||||
//sequence represents the order of events. It's always counting
|
//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;
|
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";
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha;session";
|
||||||
|
|
||||||
message Session {
|
message Session {
|
||||||
string id = 1;
|
string id = 1 [
|
||||||
zitadel.user.v2alpha.User user = 2;
|
(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;
|
package zitadel.session.v2alpha;
|
||||||
|
|
||||||
|
|
||||||
|
import "zitadel/object/v2alpha/object.proto";
|
||||||
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||||
import "zitadel/session/v2alpha/session.proto";
|
import "zitadel/session/v2alpha/session.proto";
|
||||||
import "google/api/annotations.proto";
|
import "google/api/annotations.proto";
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
import "validate/validate.proto";
|
import "validate/validate.proto";
|
||||||
|
|
||||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha;session";
|
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 {
|
service SessionService {
|
||||||
|
|
||||||
// GetSession is to demonstrate an authenticated request, where the authenticated user (usage of another grpc package) is returned
|
// Search sessions
|
||||||
//
|
rpc ListSessions (ListSessionsRequest) returns (ListSessionsResponse) {
|
||||||
// this request is subject to change and currently used for demonstration only
|
|
||||||
rpc GetSession (GetSessionRequest) returns (GetSessionResponse) {
|
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/v2alpha/sessions/{id}"
|
post: "/v2alpha/sessions/_search"
|
||||||
|
body: "*"
|
||||||
};
|
};
|
||||||
|
|
||||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
@ -25,12 +86,280 @@ service SessionService {
|
|||||||
permission: "authenticated"
|
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{
|
message GetSessionRequest{
|
||||||
string id = 1;
|
string session_id = 1;
|
||||||
|
optional string session_token = 2;
|
||||||
}
|
}
|
||||||
message GetSessionResponse{
|
message GetSessionResponse{
|
||||||
Session session = 1;
|
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…
x
Reference in New Issue
Block a user