Merge branch 'clean-transactional-propsal' into rt-domains

This commit is contained in:
adlerhurst
2025-07-25 19:10:35 +02:00
986 changed files with 106220 additions and 36435 deletions

View File

@@ -0,0 +1,347 @@
package query
import (
"context"
"database/sql"
"slices"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Administrators struct {
SearchResponse
Administrators []*Administrator
}
type Administrator struct {
Roles database.TextArray[string]
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
User *UserAdministrator
Org *OrgAdministrator
Instance *InstanceAdministrator
Project *ProjectAdministrator
ProjectGrant *ProjectGrantAdministrator
}
type UserAdministrator struct {
UserID string
LoginName string
DisplayName string
ResourceOwner string
}
type OrgAdministrator struct {
OrgID string
Name string
}
type InstanceAdministrator struct {
InstanceID string
Name string
}
type ProjectAdministrator struct {
ProjectID string
Name string
ResourceOwner string
}
type ProjectGrantAdministrator struct {
ProjectID string
ProjectName string
GrantID string
GrantedOrgID string
ResourceOwner string
}
func NewAdministratorUserResourceOwnerSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(UserResourceOwnerCol, value, TextEquals)
}
func NewAdministratorUserLoginNameSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(LoginNameNameCol, value, TextEquals)
}
func NewAdministratorUserDisplayNameSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(HumanDisplayNameCol, value, TextEquals)
}
func administratorInstancePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
InstanceMemberResourceOwner,
domain.PermissionInstanceMemberRead,
OwnedRowsPermissionOption(InstanceMemberUserID),
)
return query.JoinClause(join, args...)
}
func administratorOrgPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
OrgMemberResourceOwner,
domain.PermissionOrgMemberRead,
OwnedRowsPermissionOption(OrgMemberUserID),
)
return query.JoinClause(join, args...)
}
func administratorProjectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
ProjectMemberResourceOwner,
domain.PermissionProjectMemberRead,
WithProjectsPermissionOption(ProjectMemberProjectID),
OwnedRowsPermissionOption(ProjectMemberUserID),
)
return query.JoinClause(join, args...)
}
func administratorProjectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
ProjectGrantMemberResourceOwner,
domain.PermissionProjectGrantMemberRead,
WithProjectsPermissionOption(ProjectMemberProjectID),
OwnedRowsPermissionOption(ProjectGrantMemberUserID),
)
return query.JoinClause(join, args...)
}
func administratorsCheckPermission(ctx context.Context, administrators *Administrators, permissionCheck domain.PermissionCheck) {
selfUserID := authz.GetCtxData(ctx).UserID
administrators.Administrators = slices.DeleteFunc(administrators.Administrators,
func(administrator *Administrator) bool {
if administrator.User != nil && administrator.User.UserID == selfUserID {
return false
}
if administrator.ProjectGrant != nil {
return administratorProjectGrantCheckPermission(ctx, administrator.ProjectGrant.ResourceOwner, administrator.ProjectGrant.ProjectID, administrator.ProjectGrant.GrantID, administrator.ProjectGrant.GrantedOrgID, permissionCheck) != nil
}
if administrator.Project != nil {
return permissionCheck(ctx, domain.PermissionProjectMemberRead, administrator.Project.ResourceOwner, administrator.Project.ProjectID) != nil
}
if administrator.Org != nil {
return permissionCheck(ctx, domain.PermissionOrgMemberRead, administrator.Org.OrgID, administrator.Org.OrgID) != nil
}
if administrator.Instance != nil {
return permissionCheck(ctx, domain.PermissionInstanceMemberRead, administrator.Instance.InstanceID, administrator.Instance.InstanceID) != nil
}
return true
},
)
}
func administratorProjectGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, grantedOrgID string, permissionCheck domain.PermissionCheck) error {
if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, resourceOwner, grantID); err != nil {
if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, grantedOrgID, grantID); err != nil {
if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, resourceOwner, projectID); err != nil {
return err
}
}
}
return nil
}
func (q *Queries) SearchAdministrators(ctx context.Context, queries *MembershipSearchQuery, permissionCheck domain.PermissionCheck) (*Administrators, error) {
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
admins, err := q.searchAdministrators(ctx, queries, permissionCheckV2)
if err != nil {
return nil, err
}
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
administratorsCheckPermission(ctx, admins, permissionCheck)
}
return admins, nil
}
func (q *Queries) searchAdministrators(ctx context.Context, queries *MembershipSearchQuery, permissionCheckV2 bool) (administrators *Administrators, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, queryArgs, scan := prepareAdministratorsQuery(ctx, queries, permissionCheckV2)
eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "QUERY-TODO", "Errors.Query.InvalidRequest")
}
latestState, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable)
if err != nil {
return nil, err
}
queryArgs = append(queryArgs, args...)
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
administrators, err = scan(rows)
return err
}, stmt, queryArgs...)
if err != nil {
return nil, err
}
administrators.State = latestState
return administrators, nil
}
func prepareAdministratorsQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Administrators, error)) {
query, args := getMembershipFromQuery(ctx, queries, permissionV2)
return sq.Select(
MembershipUserID.identifier(),
membershipRoles.identifier(),
MembershipCreationDate.identifier(),
MembershipChangeDate.identifier(),
membershipResourceOwner.identifier(),
membershipOrgID.identifier(),
membershipIAMID.identifier(),
membershipProjectID.identifier(),
membershipGrantID.identifier(),
ProjectGrantColumnGrantedOrgID.identifier(),
ProjectColumnResourceOwner.identifier(),
ProjectColumnName.identifier(),
OrgColumnName.identifier(),
InstanceColumnName.identifier(),
LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(),
MachineNameCol.identifier(),
HumanAvatarURLCol.identifier(),
UserTypeCol.identifier(),
UserResourceOwnerCol.identifier(),
countColumn.identifier(),
).From(query).
LeftJoin(join(ProjectColumnID, membershipProjectID)).
LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)).
LeftJoin(join(OrgColumnID, membershipOrgID)).
LeftJoin(join(InstanceColumnID, membershipInstanceID)).
LeftJoin(join(HumanUserIDCol, OrgMemberUserID)).
LeftJoin(join(MachineUserIDCol, OrgMemberUserID)).
LeftJoin(join(UserIDCol, OrgMemberUserID)).
LeftJoin(join(LoginNameUserIDCol, OrgMemberUserID)).
Where(
sq.Eq{LoginNameIsPrimaryCol.identifier(): true},
).PlaceholderFormat(sq.Dollar),
args,
func(rows *sql.Rows) (*Administrators, error) {
administrators := make([]*Administrator, 0)
var count uint64
for rows.Next() {
var (
administrator = new(Administrator)
userID = sql.NullString{}
orgID = sql.NullString{}
instanceID = sql.NullString{}
projectID = sql.NullString{}
grantID = sql.NullString{}
grantedOrgID = sql.NullString{}
projectName = sql.NullString{}
orgName = sql.NullString{}
instanceName = sql.NullString{}
projectResourceOwner = sql.NullString{}
loginName = sql.NullString{}
displayName = sql.NullString{}
machineName = sql.NullString{}
avatarURL = sql.NullString{}
userType = sql.NullInt32{}
userResourceOwner = sql.NullString{}
)
err := rows.Scan(
&userID,
&administrator.Roles,
&administrator.CreationDate,
&administrator.ChangeDate,
&administrator.ResourceOwner,
&orgID,
&instanceID,
&projectID,
&grantID,
&grantedOrgID,
&projectResourceOwner,
&projectName,
&orgName,
&instanceName,
&loginName,
&displayName,
&machineName,
&avatarURL,
&userType,
&userResourceOwner,
&count,
)
if err != nil {
return nil, err
}
if userID.Valid {
administrator.User = &UserAdministrator{
UserID: userID.String,
LoginName: loginName.String,
DisplayName: displayName.String,
ResourceOwner: userResourceOwner.String,
}
}
if orgID.Valid {
administrator.Org = &OrgAdministrator{
OrgID: orgID.String,
Name: orgName.String,
}
}
if instanceID.Valid {
administrator.Instance = &InstanceAdministrator{
InstanceID: instanceID.String,
Name: instanceName.String,
}
}
if projectID.Valid && grantID.Valid && grantedOrgID.Valid {
administrator.ProjectGrant = &ProjectGrantAdministrator{
ProjectID: projectID.String,
ProjectName: projectName.String,
GrantID: grantID.String,
GrantedOrgID: grantedOrgID.String,
ResourceOwner: projectResourceOwner.String,
}
} else if projectID.Valid {
administrator.Project = &ProjectAdministrator{
ProjectID: projectID.String,
Name: projectName.String,
ResourceOwner: projectResourceOwner.String,
}
}
administrators = append(administrators, administrator)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-TODO", "Errors.Query.CloseRows")
}
return &Administrators{
Administrators: administrators,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}

View File

@@ -5,9 +5,11 @@ import (
"database/sql"
_ "embed"
"errors"
"slices"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/muhlemmer/gu"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
@@ -307,6 +309,19 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo
return app, err
}
func (q *Queries) AppByIDWithPermission(ctx context.Context, appID string, activeOnly bool, permissionCheck domain.PermissionCheck) (*App, error) {
app, err := q.AppByID(ctx, appID, activeOnly)
if err != nil {
return nil, err
}
if err := appCheckPermission(ctx, app.ResourceOwner, app.ProjectID, permissionCheck); err != nil {
return nil, err
}
return app, nil
}
func (q *Queries) AppByID(ctx context.Context, appID string, activeOnly bool) (app *App, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -455,27 +470,6 @@ func (q *Queries) ProjectIDFromClientID(ctx context.Context, appID string) (id s
return id, err
}
func (q *Queries) ProjectByOIDCClientID(ctx context.Context, id string) (project *Project, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
stmt, scan := prepareProjectByOIDCAppQuery()
eq := sq.Eq{
AppOIDCConfigColumnClientID.identifier(): id,
AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}
query, args, err := stmt.Where(eq).ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-XhJi4", "Errors.Query.SQLStatement")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
project, err = scan(row)
return err
}, query, args...)
return project, err
}
func (q *Queries) AppByOIDCClientID(ctx context.Context, clientID string) (app *App, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -526,11 +520,25 @@ func (q *Queries) AppByClientID(ctx context.Context, clientID string) (app *App,
return app, err
}
func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, withOwnerRemoved bool) (apps *Apps, err error) {
func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, permissionCheck domain.PermissionCheck) (*Apps, error) {
apps, err := q.searchApps(ctx, queries, PermissionV2(ctx, permissionCheck))
if err != nil {
return nil, err
}
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
apps.Apps = appsCheckPermission(ctx, apps.Apps, permissionCheck)
}
return apps, nil
}
func (q *Queries) searchApps(ctx context.Context, queries *AppSearchQueries, isPermissionV2Enabled bool) (apps *Apps, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareAppsQuery()
query = appPermissionCheckV2(ctx, query, isPermissionV2Enabled, queries)
eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
@@ -548,6 +556,21 @@ func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, wit
return apps, err
}
func appPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *AppSearchQueries) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
AppColumnResourceOwner,
domain.PermissionProjectAppRead,
SingleOrgPermissionOption(queries.Queries),
WithProjectsPermissionOption(AppColumnProjectID),
)
return query.JoinClause(join, args...)
}
func (q *Queries) SearchClientIDs(ctx context.Context, queries *AppSearchQueries, shouldTriggerBulk bool) (ids []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -624,10 +647,25 @@ func (q *Queries) SAMLAppLoginVersion(ctx context.Context, appID string) (loginV
return loginVersion, nil
}
func appCheckPermission(ctx context.Context, resourceOwner string, projectID string, permissionCheck domain.PermissionCheck) error {
return permissionCheck(ctx, domain.PermissionProjectAppRead, resourceOwner, projectID)
}
// appsCheckPermission returns only the apps that the user in context has permission to read
func appsCheckPermission(ctx context.Context, apps []*App, permissionCheck domain.PermissionCheck) []*App {
return slices.DeleteFunc(apps, func(app *App) bool {
return permissionCheck(ctx, domain.PermissionProjectAppRead, app.ResourceOwner, app.ProjectID) != nil
})
}
func NewAppNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
return NewTextQuery(AppColumnName, value, method)
}
func NewAppStateSearchQuery(value domain.AppState) (SearchQuery, error) {
return NewNumberQuery(AppColumnState, int(value), NumberEquals)
}
func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(AppColumnProjectID, id, TextEquals)
}
@@ -867,48 +905,6 @@ func prepareProjectIDByAppQuery() (sq.SelectBuilder, func(*sql.Row) (projectID s
}
}
func prepareProjectByOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) {
return sq.Select(
ProjectColumnID.identifier(),
ProjectColumnCreationDate.identifier(),
ProjectColumnChangeDate.identifier(),
ProjectColumnResourceOwner.identifier(),
ProjectColumnState.identifier(),
ProjectColumnSequence.identifier(),
ProjectColumnName.identifier(),
ProjectColumnProjectRoleAssertion.identifier(),
ProjectColumnProjectRoleCheck.identifier(),
ProjectColumnHasProjectCheck.identifier(),
ProjectColumnPrivateLabelingSetting.identifier(),
).From(projectsTable.identifier()).
Join(join(AppColumnProjectID, ProjectColumnID)).
Join(join(AppOIDCConfigColumnAppID, AppColumnID)).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*Project, error) {
p := new(Project)
err := row.Scan(
&p.ID,
&p.CreationDate,
&p.ChangeDate,
&p.ResourceOwner,
&p.State,
&p.Sequence,
&p.Name,
&p.ProjectRoleAssertion,
&p.ProjectRoleCheck,
&p.HasProjectCheck,
&p.PrivateLabelingSetting,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-yxTMh", "Errors.Project.NotFound")
}
return nil, zerrors.ThrowInternal(err, "QUERY-dj2FF", "Errors.Internal")
}
return p, nil
}
}
func prepareProjectByAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) {
return sq.Select(
ProjectColumnID.identifier(),
@@ -1181,7 +1177,7 @@ func (c sqlOIDCConfig) set(app *App) {
if c.loginBaseURI.Valid {
app.OIDCConfig.LoginBaseURI = &c.loginBaseURI.String
}
compliance := domain.GetOIDCCompliance(app.OIDCConfig.Version, app.OIDCConfig.AppType, app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, app.OIDCConfig.AuthMethodType, app.OIDCConfig.RedirectURIs)
compliance := domain.GetOIDCCompliance(gu.Ptr(app.OIDCConfig.Version), gu.Ptr(app.OIDCConfig.AppType), app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, gu.Ptr(app.OIDCConfig.AuthMethodType), app.OIDCConfig.RedirectURIs)
app.OIDCConfig.ComplianceProblems = compliance.Problems
var err error

View File

@@ -98,6 +98,7 @@ type AuthNKey struct {
ChangeDate time.Time
ResourceOwner string
Sequence uint64
ApplicationID string
Expiration time.Time
Type domain.AuthNKeyType
@@ -222,6 +223,19 @@ func (q *Queries) SearchAuthNKeysData(ctx context.Context, queries *AuthNKeySear
return authNKeys, err
}
func (q *Queries) GetAuthNKeyByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck, queries ...SearchQuery) (*AuthNKey, error) {
key, err := q.GetAuthNKeyByID(ctx, shouldTriggerBulk, id, queries...)
if err != nil {
return nil, err
}
if err := appCheckPermission(ctx, key.ResourceOwner, key.AggregateID, permissionCheck); err != nil {
return nil, err
}
return key, nil
}
func (q *Queries) GetAuthNKeyByID(ctx context.Context, shouldTriggerBulk bool, id string, queries ...SearchQuery) (key *AuthNKey, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -254,34 +268,6 @@ func (q *Queries) GetAuthNKeyByID(ctx context.Context, shouldTriggerBulk bool, i
return key, err
}
func (q *Queries) GetAuthNKeyPublicKeyByIDAndIdentifier(ctx context.Context, id string, identifier string) (key []byte, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
stmt, scan := prepareAuthNKeyPublicKeyQuery()
eq := sq.And{
sq.Eq{
AuthNKeyColumnID.identifier(): id,
AuthNKeyColumnIdentifier.identifier(): identifier,
AuthNKeyColumnEnabled.identifier(): true,
AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
},
sq.Gt{
AuthNKeyColumnExpiration.identifier(): time.Now(),
},
}
query, args, err := stmt.Where(eq).ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-DAb32", "Errors.Query.SQLStatement")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
key, err = scan(row)
return err
}, query, args...)
return key, err
}
func NewAuthNKeyResourceOwnerQuery(id string) (SearchQuery, error) {
return NewTextQuery(AuthNKeyColumnResourceOwner, id, TextEquals)
}
@@ -358,6 +344,7 @@ func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys
AuthNKeyColumnSequence.identifier(),
AuthNKeyColumnExpiration.identifier(),
AuthNKeyColumnType.identifier(),
AuthNKeyColumnObjectID.identifier(),
countColumn.identifier(),
).From(authNKeyTable.identifier()).
PlaceholderFormat(sq.Dollar)
@@ -376,6 +363,7 @@ func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys
&authNKey.Sequence,
&authNKey.Expiration,
&authNKey.Type,
&authNKey.ApplicationID,
&count,
)
if err != nil {
@@ -429,26 +417,6 @@ func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, er
}
}
func prepareAuthNKeyPublicKeyQuery() (sq.SelectBuilder, func(row *sql.Row) ([]byte, error)) {
return sq.Select(
AuthNKeyColumnPublicKey.identifier(),
).From(authNKeyTable.identifier()).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) ([]byte, error) {
var publicKey []byte
err := row.Scan(
&publicKey,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-SDf32", "Errors.AuthNKey.NotFound")
}
return nil, zerrors.ThrowInternal(err, "QUERY-Bfs2a", "Errors.Internal")
}
return publicKey, nil
}
}
func prepareAuthNKeysDataQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeysData, error)) {
return sq.Select(
AuthNKeyColumnID.identifier(),

View File

@@ -26,6 +26,7 @@ var (
` projections.authn_keys2.sequence,` +
` projections.authn_keys2.expiration,` +
` projections.authn_keys2.type,` +
` projections.authn_keys2.object_id,` +
` COUNT(*) OVER ()` +
` FROM projections.authn_keys2`
prepareAuthNKeysCols = []string{
@@ -37,6 +38,7 @@ var (
"sequence",
"expiration",
"type",
"object_id",
"count",
}
@@ -129,6 +131,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
uint64(20211109),
testNow,
1,
"app1",
},
},
),
@@ -147,6 +150,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
Sequence: 20211109,
Expiration: testNow,
Type: domain.AuthNKeyTypeJSON,
ApplicationID: "app1",
},
},
},
@@ -168,6 +172,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
uint64(20211109),
testNow,
1,
"app1",
},
{
"id-2",
@@ -178,6 +183,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
uint64(20211109),
testNow,
1,
"app1",
},
},
),
@@ -196,6 +202,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
Sequence: 20211109,
Expiration: testNow,
Type: domain.AuthNKeyTypeJSON,
ApplicationID: "app1",
},
{
ID: "id-2",
@@ -206,6 +213,7 @@ func Test_AuthNKeyPrepares(t *testing.T) {
Sequence: 20211109,
Expiration: testNow,
Type: domain.AuthNKeyTypeJSON,
ApplicationID: "app1",
},
},
},
@@ -423,55 +431,6 @@ func Test_AuthNKeyPrepares(t *testing.T) {
},
object: (*AuthNKey)(nil),
},
{
name: "prepareAuthNKeyPublicKeyQuery no result",
prepare: prepareAuthNKeyPublicKeyQuery,
want: want{
sqlExpectations: mockQueriesScanErr(
regexp.QuoteMeta(prepareAuthNKeyPublicKeyStmt),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: ([]byte)(nil),
},
{
name: "prepareAuthNKeyPublicKeyQuery found",
prepare: prepareAuthNKeyPublicKeyQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(prepareAuthNKeyPublicKeyStmt),
prepareAuthNKeyPublicKeyCols,
[]driver.Value{
[]byte("publicKey"),
},
),
},
object: []byte("publicKey"),
},
{
name: "prepareAuthNKeyPublicKeyQuery sql err",
prepare: prepareAuthNKeyPublicKeyQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(prepareAuthNKeyPublicKeyStmt),
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: ([]byte)(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -0,0 +1,256 @@
package query
import (
"context"
"crypto/md5"
"database/sql"
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"
"time"
"dario.cat/mergo"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/logging"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/v2/org"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
)
var (
//go:embed v2-default.json
defaultLoginTranslations []byte
defaultSystemTranslations map[language.Tag]map[string]any
hostedLoginTranslationTable = table{
name: projection.HostedLoginTranslationTable,
instanceIDCol: projection.HostedLoginTranslationInstanceIDCol,
}
hostedLoginTranslationColInstanceID = Column{
name: projection.HostedLoginTranslationInstanceIDCol,
table: hostedLoginTranslationTable,
}
hostedLoginTranslationColResourceOwner = Column{
name: projection.HostedLoginTranslationAggregateIDCol,
table: hostedLoginTranslationTable,
}
hostedLoginTranslationColResourceOwnerType = Column{
name: projection.HostedLoginTranslationAggregateTypeCol,
table: hostedLoginTranslationTable,
}
hostedLoginTranslationColLocale = Column{
name: projection.HostedLoginTranslationLocaleCol,
table: hostedLoginTranslationTable,
}
hostedLoginTranslationColFile = Column{
name: projection.HostedLoginTranslationFileCol,
table: hostedLoginTranslationTable,
}
hostedLoginTranslationColEtag = Column{
name: projection.HostedLoginTranslationEtagCol,
table: hostedLoginTranslationTable,
}
)
func init() {
err := json.Unmarshal(defaultLoginTranslations, &defaultSystemTranslations)
if err != nil {
panic(err)
}
}
type HostedLoginTranslations struct {
SearchResponse
HostedLoginTranslations []*HostedLoginTranslation
}
type HostedLoginTranslation struct {
AggregateID string
Sequence uint64
CreationDate time.Time
ChangeDate time.Time
Locale string
File map[string]any
LevelType string
LevelID string
Etag string
}
func (q *Queries) GetHostedLoginTranslation(ctx context.Context, req *settings.GetHostedLoginTranslationRequest) (res *settings.GetHostedLoginTranslationResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
inst := authz.GetInstance(ctx)
defaultInstLang := inst.DefaultLanguage()
lang, err := language.BCP47.Parse(req.GetLocale())
if err != nil || lang.IsRoot() {
return nil, zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid")
}
parentLang := lang.Parent()
if parentLang.IsRoot() {
parentLang = lang
}
sysTranslation, systemEtag, err := getSystemTranslation(parentLang, defaultInstLang)
if err != nil {
return nil, err
}
var levelID, resourceOwner string
switch t := req.GetLevel().(type) {
case *settings.GetHostedLoginTranslationRequest_System:
return getTranslationOutputMessage(sysTranslation, systemEtag)
case *settings.GetHostedLoginTranslationRequest_Instance:
levelID = authz.GetInstance(ctx).InstanceID()
resourceOwner = instance.AggregateType
case *settings.GetHostedLoginTranslationRequest_OrganizationId:
levelID = t.OrganizationId
resourceOwner = org.AggregateType
default:
return nil, zerrors.ThrowInvalidArgument(nil, "QUERY-YB6Sri", "Errors.Arguments.Level.Invalid")
}
stmt, scan := prepareHostedLoginTranslationQuery()
langORBaseLang := sq.Or{
sq.Eq{hostedLoginTranslationColLocale.identifier(): lang.String()},
sq.Eq{hostedLoginTranslationColLocale.identifier(): parentLang.String()},
}
eq := sq.Eq{
hostedLoginTranslationColInstanceID.identifier(): inst.InstanceID(),
hostedLoginTranslationColResourceOwner.identifier(): levelID,
hostedLoginTranslationColResourceOwnerType.identifier(): resourceOwner,
}
query, args, err := stmt.Where(eq).Where(langORBaseLang).ToSql()
if err != nil {
logging.WithError(err).Error("unable to generate sql statement")
return nil, zerrors.ThrowInternal(err, "QUERY-ZgCMux", "Errors.Query.SQLStatement")
}
var trs []*HostedLoginTranslation
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
trs, err = scan(rows)
return err
}, query, args...)
if err != nil {
logging.WithError(err).Error("failed to query translations")
return nil, zerrors.ThrowInternal(err, "QUERY-6k1zjx", "Errors.Internal")
}
requestedTranslation, parentTranslation := &HostedLoginTranslation{}, &HostedLoginTranslation{}
for _, tr := range trs {
if tr == nil {
continue
}
if tr.LevelType == resourceOwner {
requestedTranslation = tr
} else {
parentTranslation = tr
}
}
if !req.GetIgnoreInheritance() {
// There is no record for the requested level, set the upper level etag
if requestedTranslation.Etag == "" {
requestedTranslation.Etag = parentTranslation.Etag
}
// Case where Level == ORGANIZATION -> Check if we have an instance level translation
// If so, merge it with the translations we have
if parentTranslation != nil && parentTranslation.LevelType == instance.AggregateType {
if err := mergo.Merge(&requestedTranslation.File, parentTranslation.File); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-pdgEJd", "Errors.Query.MergeTranslations")
}
}
// The DB query returned no results, we have to set the system translation etag
if requestedTranslation.Etag == "" {
requestedTranslation.Etag = systemEtag
}
// Merge the system translations
if err := mergo.Merge(&requestedTranslation.File, sysTranslation); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-HdprNF", "Errors.Query.MergeTranslations")
}
}
return getTranslationOutputMessage(requestedTranslation.File, requestedTranslation.Etag)
}
func getSystemTranslation(lang, instanceDefaultLang language.Tag) (map[string]any, string, error) {
translation, ok := defaultSystemTranslations[lang]
if !ok {
translation, ok = defaultSystemTranslations[instanceDefaultLang]
if !ok {
return nil, "", zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", lang)
}
}
hash := md5.Sum(fmt.Append(nil, translation))
return translation, hex.EncodeToString(hash[:]), nil
}
func prepareHostedLoginTranslationQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*HostedLoginTranslation, error)) {
return sq.Select(
hostedLoginTranslationColFile.identifier(),
hostedLoginTranslationColResourceOwnerType.identifier(),
hostedLoginTranslationColEtag.identifier(),
).From(hostedLoginTranslationTable.identifier()).
Limit(2).
PlaceholderFormat(sq.Dollar),
func(r *sql.Rows) ([]*HostedLoginTranslation, error) {
translations := make([]*HostedLoginTranslation, 0, 2)
for r.Next() {
var rawTranslation json.RawMessage
translation := &HostedLoginTranslation{}
err := r.Scan(
&rawTranslation,
&translation.LevelType,
&translation.Etag,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal(rawTranslation, &translation.File); err != nil {
return nil, err
}
translations = append(translations, translation)
}
if err := r.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-oc7r7i", "Errors.Query.CloseRows")
}
return translations, nil
}
}
func getTranslationOutputMessage(translation map[string]any, etag string) (*settings.GetHostedLoginTranslationResponse, error) {
protoTranslation, err := structpb.NewStruct(translation)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-70ppPp", "Errors.Protobuf.ConvertToStruct")
}
return &settings.GetHostedLoginTranslationResponse{
Translations: protoTranslation,
Etag: etag,
}, nil
}

View File

@@ -0,0 +1,337 @@
package query
import (
"crypto/md5"
"database/sql"
"database/sql/driver"
"encoding/hex"
"encoding/json"
"fmt"
"maps"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"google.golang.org/protobuf/runtime/protoimpl"
"google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/database/mock"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
)
func TestGetSystemTranslation(t *testing.T) {
okTranslation := defaultLoginTranslations
parsedOKTranslation := map[string]map[string]any{}
require.Nil(t, json.Unmarshal(okTranslation, &parsedOKTranslation))
hashOK := md5.Sum(fmt.Append(nil, parsedOKTranslation["de"]))
tt := []struct {
testName string
inputLanguage language.Tag
inputInstanceLanguage language.Tag
systemTranslationToSet []byte
expectedLanguage map[string]any
expectedEtag string
expectedError error
}{
{
testName: "when neither input language nor system default language have translation should return not found error",
systemTranslationToSet: okTranslation,
inputLanguage: language.MustParse("ro"),
inputInstanceLanguage: language.MustParse("fr"),
expectedError: zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", "ro"),
},
{
testName: "when input language has no translation should fallback onto instance default",
systemTranslationToSet: okTranslation,
inputLanguage: language.MustParse("ro"),
inputInstanceLanguage: language.MustParse("de"),
expectedLanguage: parsedOKTranslation["de"],
expectedEtag: hex.EncodeToString(hashOK[:]),
},
{
testName: "when input language has translation should return it",
systemTranslationToSet: okTranslation,
inputLanguage: language.MustParse("de"),
inputInstanceLanguage: language.MustParse("en"),
expectedLanguage: parsedOKTranslation["de"],
expectedEtag: hex.EncodeToString(hashOK[:]),
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Given
defaultLoginTranslations = tc.systemTranslationToSet
// When
translation, etag, err := getSystemTranslation(tc.inputLanguage, tc.inputInstanceLanguage)
// Verify
require.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedLanguage, translation)
assert.Equal(t, tc.expectedEtag, etag)
})
}
}
func TestGetTranslationOutput(t *testing.T) {
t.Parallel()
validMap := map[string]any{"loginHeader": "A login header"}
protoMap, err := structpb.NewStruct(validMap)
require.NoError(t, err)
hash := md5.Sum(fmt.Append(nil, validMap))
encodedHash := hex.EncodeToString(hash[:])
tt := []struct {
testName string
inputTranslation map[string]any
expectedError error
expectedResponse *settings.GetHostedLoginTranslationResponse
}{
{
testName: "when unparsable map should return internal error",
inputTranslation: map[string]any{"\xc5z": "something"},
expectedError: zerrors.ThrowInternal(protoimpl.X.NewError("invalid UTF-8 in string: %q", "\xc5z"), "QUERY-70ppPp", "Errors.Protobuf.ConvertToStruct"),
},
{
testName: "when input translation is valid should return expected response message",
inputTranslation: validMap,
expectedResponse: &settings.GetHostedLoginTranslationResponse{
Translations: protoMap,
Etag: hex.EncodeToString(hash[:]),
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
res, err := getTranslationOutputMessage(tc.inputTranslation, encodedHash)
// Verify
require.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedResponse, res)
})
}
}
func TestGetHostedLoginTranslation(t *testing.T) {
query := `SELECT projections.hosted_login_translations.file, projections.hosted_login_translations.aggregate_type, projections.hosted_login_translations.etag
FROM projections.hosted_login_translations
WHERE projections.hosted_login_translations.aggregate_id = $1
AND projections.hosted_login_translations.aggregate_type = $2
AND projections.hosted_login_translations.instance_id = $3
AND (projections.hosted_login_translations.locale = $4 OR projections.hosted_login_translations.locale = $5)
LIMIT 2`
okTranslation := defaultLoginTranslations
parsedOKTranslation := map[string]map[string]any{}
require.NoError(t, json.Unmarshal(okTranslation, &parsedOKTranslation))
protoDefaultTranslation, err := structpb.NewStruct(parsedOKTranslation["en"])
require.Nil(t, err)
defaultWithDBTranslations := maps.Clone(parsedOKTranslation["en"])
defaultWithDBTranslations["test"] = "translation"
defaultWithDBTranslations["test2"] = "translation2"
protoDefaultWithDBTranslation, err := structpb.NewStruct(defaultWithDBTranslations)
require.NoError(t, err)
nilProtoDefaultMap, err := structpb.NewStruct(nil)
require.NoError(t, err)
hashDefaultTranslations := md5.Sum(fmt.Append(nil, parsedOKTranslation["en"]))
tt := []struct {
testName string
defaultInstanceLanguage language.Tag
sqlExpectations []mock.Expectation
inputRequest *settings.GetHostedLoginTranslationRequest
expectedError error
expectedResult *settings.GetHostedLoginTranslationResponse
}{
{
testName: "when input language is invalid should return invalid argument error",
inputRequest: &settings.GetHostedLoginTranslationRequest{},
expectedError: zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid"),
},
{
testName: "when input language is root should return invalid argument error",
defaultInstanceLanguage: language.English,
inputRequest: &settings.GetHostedLoginTranslationRequest{
Locale: "root",
},
expectedError: zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid"),
},
{
testName: "when no system translation is available should return not found error",
defaultInstanceLanguage: language.Romanian,
inputRequest: &settings.GetHostedLoginTranslationRequest{
Locale: "ro-RO",
},
expectedError: zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", "ro"),
},
{
testName: "when requesting system translation should return it",
defaultInstanceLanguage: language.English,
inputRequest: &settings.GetHostedLoginTranslationRequest{
Locale: "en-US",
Level: &settings.GetHostedLoginTranslationRequest_System{},
},
expectedResult: &settings.GetHostedLoginTranslationResponse{
Translations: protoDefaultTranslation,
Etag: hex.EncodeToString(hashDefaultTranslations[:]),
},
},
{
testName: "when querying DB fails should return internal error",
defaultInstanceLanguage: language.English,
sqlExpectations: []mock.Expectation{
mock.ExpectQuery(
query,
mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"),
mock.WithQueryErr(sql.ErrConnDone),
),
},
inputRequest: &settings.GetHostedLoginTranslationRequest{
Locale: "en-US",
Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{
OrganizationId: "123",
},
},
expectedError: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-6k1zjx", "Errors.Internal"),
},
{
testName: "when querying DB returns no result should return system translations",
defaultInstanceLanguage: language.English,
sqlExpectations: []mock.Expectation{
mock.ExpectQuery(
query,
mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"),
mock.WithQueryResult(
[]string{"file", "aggregate_type", "etag"},
[][]driver.Value{},
),
),
},
inputRequest: &settings.GetHostedLoginTranslationRequest{
Locale: "en-US",
Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{
OrganizationId: "123",
},
},
expectedResult: &settings.GetHostedLoginTranslationResponse{
Translations: protoDefaultTranslation,
Etag: hex.EncodeToString(hashDefaultTranslations[:]),
},
},
{
testName: "when querying DB returns no result and inheritance disabled should return empty result",
defaultInstanceLanguage: language.English,
sqlExpectations: []mock.Expectation{
mock.ExpectQuery(
query,
mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"),
mock.WithQueryResult(
[]string{"file", "aggregate_type", "etag"},
[][]driver.Value{},
),
),
},
inputRequest: &settings.GetHostedLoginTranslationRequest{
Locale: "en-US",
Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{
OrganizationId: "123",
},
IgnoreInheritance: true,
},
expectedResult: &settings.GetHostedLoginTranslationResponse{
Etag: "",
Translations: nilProtoDefaultMap,
},
},
{
testName: "when querying DB returns records should return merged result",
defaultInstanceLanguage: language.English,
sqlExpectations: []mock.Expectation{
mock.ExpectQuery(
query,
mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"),
mock.WithQueryResult(
[]string{"file", "aggregate_type", "etag"},
[][]driver.Value{
{[]byte(`{"test": "translation"}`), "org", "etag-org"},
{[]byte(`{"test2": "translation2"}`), "instance", "etag-instance"},
},
),
),
},
inputRequest: &settings.GetHostedLoginTranslationRequest{
Locale: "en-US",
Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{
OrganizationId: "123",
},
},
expectedResult: &settings.GetHostedLoginTranslationResponse{
Etag: "etag-org",
Translations: protoDefaultWithDBTranslation,
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Given
db := &database.DB{DB: mock.NewSQLMock(t, tc.sqlExpectations...).DB}
querier := Queries{client: db}
ctx := authz.NewMockContext("instance-id", "org-id", "user-id", authz.WithMockDefaultLanguage(tc.defaultInstanceLanguage))
// When
res, err := querier.GetHostedLoginTranslation(ctx, tc.inputRequest)
// Verify
require.Equal(t, tc.expectedError, err)
if tc.expectedError == nil {
assert.Equal(t, tc.expectedResult.GetEtag(), res.GetEtag())
assert.Equal(t, tc.expectedResult.GetTranslations().GetFields(), res.GetTranslations().GetFields())
}
})
}
}

View File

@@ -8,21 +8,18 @@ import (
)
type InstanceFeatures struct {
Details *domain.ObjectDetails
LoginDefaultOrg FeatureSource[bool]
TriggerIntrospectionProjections FeatureSource[bool]
LegacyIntrospection FeatureSource[bool]
UserSchema FeatureSource[bool]
TokenExchange FeatureSource[bool]
ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
WebKey FeatureSource[bool]
DebugOIDCParentError FeatureSource[bool]
OIDCSingleV1SessionTermination FeatureSource[bool]
DisableUserTokenEvent FeatureSource[bool]
EnableBackChannelLogout FeatureSource[bool]
LoginV2 FeatureSource[*feature.LoginV2]
PermissionCheckV2 FeatureSource[bool]
ConsoleUseV2UserApi FeatureSource[bool]
Details *domain.ObjectDetails
LoginDefaultOrg FeatureSource[bool]
UserSchema FeatureSource[bool]
TokenExchange FeatureSource[bool]
ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
DebugOIDCParentError FeatureSource[bool]
OIDCSingleV1SessionTermination FeatureSource[bool]
DisableUserTokenEvent FeatureSource[bool]
EnableBackChannelLogout FeatureSource[bool]
LoginV2 FeatureSource[*feature.LoginV2]
PermissionCheckV2 FeatureSource[bool]
ConsoleUseV2UserApi FeatureSource[bool]
}
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {

View File

@@ -63,12 +63,9 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v1.DefaultLoginInstanceEventType,
feature_v2.InstanceResetEventType,
feature_v2.InstanceLoginDefaultOrgEventType,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
feature_v2.InstanceLegacyIntrospectionEventType,
feature_v2.InstanceUserSchemaEventType,
feature_v2.InstanceTokenExchangeEventType,
feature_v2.InstanceImprovedPerformanceEventType,
feature_v2.InstanceWebKeyEventType,
feature_v2.InstanceDebugOIDCParentErrorEventType,
feature_v2.InstanceOIDCSingleV1SessionTerminationEventType,
feature_v2.InstanceDisableUserTokenEvent,
@@ -93,8 +90,6 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
return false
}
m.instance.LoginDefaultOrg = m.system.LoginDefaultOrg
m.instance.TriggerIntrospectionProjections = m.system.TriggerIntrospectionProjections
m.instance.LegacyIntrospection = m.system.LegacyIntrospection
m.instance.UserSchema = m.system.UserSchema
m.instance.TokenExchange = m.system.TokenExchange
m.instance.ImprovedPerformance = m.system.ImprovedPerformance
@@ -111,23 +106,16 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_
return err
}
switch key {
case feature.KeyUnspecified,
feature.KeyActionsDeprecated:
case feature.KeyUnspecified:
return nil
case feature.KeyLoginDefaultOrg:
features.LoginDefaultOrg.set(level, event.Value)
case feature.KeyTriggerIntrospectionProjections:
features.TriggerIntrospectionProjections.set(level, event.Value)
case feature.KeyLegacyIntrospection:
features.LegacyIntrospection.set(level, event.Value)
case feature.KeyUserSchema:
features.UserSchema.set(level, event.Value)
case feature.KeyTokenExchange:
features.TokenExchange.set(level, event.Value)
case feature.KeyImprovedPerformance:
features.ImprovedPerformance.set(level, event.Value)
case feature.KeyWebKey:
features.WebKey.set(level, event.Value)
case feature.KeyDebugOIDCParentError:
features.DebugOIDCParentError.set(level, event.Value)
case feature.KeyOIDCSingleV1SessionTermination:

View File

@@ -71,14 +71,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
Level: feature.LevelUnspecified,
Value: false,
},
TriggerIntrospectionProjections: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
LegacyIntrospection: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
},
},
{
@@ -93,14 +85,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLoginDefaultOrgEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, false,
@@ -116,14 +100,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
Level: feature.LevelInstance,
Value: false,
},
TriggerIntrospectionProjections: FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
LegacyIntrospection: FeatureSource[bool]{
Level: feature.LevelInstance,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelInstance,
Value: false,
@@ -142,14 +118,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLoginDefaultOrgEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, false,
@@ -158,10 +126,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceResetEventType,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
)),
),
),
args: args{true},
@@ -173,14 +137,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
Level: feature.LevelSystem,
Value: true,
},
TriggerIntrospectionProjections: FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
LegacyIntrospection: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
@@ -195,14 +151,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLoginDefaultOrgEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, false,
@@ -211,10 +159,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceResetEventType,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
ctx, aggregate,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
)),
),
),
args: args{false},
@@ -226,14 +170,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
Level: feature.LevelUnspecified,
Value: false,
},
TriggerIntrospectionProjections: FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
LegacyIntrospection: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,

View File

@@ -25,12 +25,6 @@ var introspectionTriggerHandlers = sync.OnceValue(func() []*handler.Handler {
)
})
// TriggerIntrospectionProjections triggers all projections
// relevant to introspection queries concurrently.
func TriggerIntrospectionProjections(ctx context.Context) {
triggerBatch(ctx, introspectionTriggerHandlers()...)
}
type AppType string
const (

View File

@@ -1,20 +1,10 @@
package query
import (
"context"
"crypto/rsa"
"database/sql"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/repository/keypair"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Key interface {
@@ -36,11 +26,6 @@ type PublicKey interface {
Key() interface{}
}
type PrivateKeys struct {
SearchResponse
Keys []PrivateKey
}
type PublicKeys struct {
SearchResponse
Keys []PublicKey
@@ -72,34 +57,6 @@ func (k *key) Sequence() uint64 {
return k.sequence
}
type privateKey struct {
key
expiry time.Time
privateKey *crypto.CryptoValue
}
func (k *privateKey) Expiry() time.Time {
return k.expiry
}
func (k *privateKey) Key() *crypto.CryptoValue {
return k.privateKey
}
type rsaPublicKey struct {
key
expiry time.Time
publicKey *rsa.PublicKey
}
func (r *rsaPublicKey) Expiry() time.Time {
return r.expiry
}
func (r *rsaPublicKey) Key() interface{} {
return r.publicKey
}
var (
keyTable = table{
name: projection.KeyProjectionTable,
@@ -157,277 +114,3 @@ var (
table: keyPrivateTable,
}
)
var (
keyPublicTable = table{
name: projection.KeyPublicTable,
instanceIDCol: projection.KeyPrivateColumnInstanceID,
}
KeyPublicColID = Column{
name: projection.KeyPublicColumnID,
table: keyPublicTable,
}
KeyPublicColExpiry = Column{
name: projection.KeyPublicColumnExpiry,
table: keyPublicTable,
}
KeyPublicColKey = Column{
name: projection.KeyPublicColumnKey,
table: keyPublicTable,
}
)
func (q *Queries) ActivePublicKeys(ctx context.Context, t time.Time) (keys *PublicKeys, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := preparePublicKeysQuery()
if t.IsZero() {
t = time.Now()
}
stmt, args, err := query.Where(
sq.And{
sq.Eq{KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()},
sq.Gt{KeyPublicColExpiry.identifier(): t},
}).ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-SDFfg", "Errors.Query.SQLStatement")
}
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
keys, err = scan(rows)
return err
}, stmt, args...)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Sghn4", "Errors.Internal")
}
keys.State, err = q.latestState(ctx, keyTable)
if !zerrors.IsNotFound(err) {
return keys, err
}
return keys, nil
}
func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (keys *PrivateKeys, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
stmt, scan := preparePrivateKeysQuery()
if t.IsZero() {
t = time.Now()
}
query, args, err := stmt.Where(
sq.And{
sq.Eq{
KeyColUse.identifier(): crypto.KeyUsageSigning,
KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
},
sq.Gt{KeyPrivateColExpiry.identifier(): t},
}).OrderBy(KeyPrivateColExpiry.identifier()).ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-SDff2", "Errors.Query.SQLStatement")
}
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
keys, err = scan(rows)
return err
}, query, args...)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-WRFG4", "Errors.Internal")
}
keys.State, err = q.latestState(ctx, keyTable)
if !zerrors.IsNotFound(err) {
return keys, err
}
return keys, nil
}
func preparePublicKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PublicKeys, error)) {
return sq.Select(
KeyColID.identifier(),
KeyColCreationDate.identifier(),
KeyColChangeDate.identifier(),
KeyColSequence.identifier(),
KeyColResourceOwner.identifier(),
KeyColAlgorithm.identifier(),
KeyColUse.identifier(),
KeyPublicColExpiry.identifier(),
KeyPublicColKey.identifier(),
countColumn.identifier(),
).From(keyTable.identifier()).
LeftJoin(join(KeyPublicColID, KeyColID)).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*PublicKeys, error) {
keys := make([]PublicKey, 0)
var count uint64
for rows.Next() {
k := new(rsaPublicKey)
var keyValue []byte
err := rows.Scan(
&k.id,
&k.creationDate,
&k.changeDate,
&k.sequence,
&k.resourceOwner,
&k.algorithm,
&k.use,
&k.expiry,
&keyValue,
&count,
)
if err != nil {
return nil, err
}
k.publicKey, err = crypto.BytesToPublicKey(keyValue)
if err != nil {
return nil, err
}
keys = append(keys, k)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-rKd6k", "Errors.Query.CloseRows")
}
return &PublicKeys{
Keys: keys,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}
func preparePrivateKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PrivateKeys, error)) {
return sq.Select(
KeyColID.identifier(),
KeyColCreationDate.identifier(),
KeyColChangeDate.identifier(),
KeyColSequence.identifier(),
KeyColResourceOwner.identifier(),
KeyColAlgorithm.identifier(),
KeyColUse.identifier(),
KeyPrivateColExpiry.identifier(),
KeyPrivateColKey.identifier(),
countColumn.identifier(),
).From(keyTable.identifier()).
LeftJoin(join(KeyPrivateColID, KeyColID)).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*PrivateKeys, error) {
keys := make([]PrivateKey, 0)
var count uint64
for rows.Next() {
k := new(privateKey)
err := rows.Scan(
&k.id,
&k.creationDate,
&k.changeDate,
&k.sequence,
&k.resourceOwner,
&k.algorithm,
&k.use,
&k.expiry,
&k.privateKey,
&count,
)
if err != nil {
return nil, err
}
keys = append(keys, k)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-rKd6k", "Errors.Query.CloseRows")
}
return &PrivateKeys{
Keys: keys,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}
type PublicKeyReadModel struct {
eventstore.ReadModel
Algorithm string
Key *crypto.CryptoValue
Expiry time.Time
Usage crypto.KeyUsage
}
func NewPublicKeyReadModel(keyID, resourceOwner string) *PublicKeyReadModel {
return &PublicKeyReadModel{
ReadModel: eventstore.ReadModel{
AggregateID: keyID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *PublicKeyReadModel) AppendEvents(events ...eventstore.Event) {
wm.ReadModel.AppendEvents(events...)
}
func (wm *PublicKeyReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *keypair.AddedEvent:
wm.Algorithm = e.Algorithm
wm.Key = e.PublicKey.Key
wm.Expiry = e.PublicKey.Expiry
wm.Usage = e.Usage
default:
}
}
return wm.ReadModel.Reduce()
}
func (wm *PublicKeyReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AwaitOpenTransactions().
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(keypair.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(keypair.AddedEventType).
Builder()
}
func (q *Queries) GetPublicKeyByID(ctx context.Context, keyID string) (_ PublicKey, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
model := NewPublicKeyReadModel(keyID, authz.GetInstance(ctx).InstanceID())
if err := q.eventstore.FilterToQueryReducer(ctx, model); err != nil {
return nil, err
}
if model.Algorithm == "" || model.Key == nil {
return nil, zerrors.ThrowNotFound(err, "QUERY-Ahf7x", "Errors.Key.NotFound")
}
keyValue, err := crypto.Decrypt(model.Key, q.keyEncryptionAlgorithm)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Ie4oh", "Errors.Internal")
}
publicKey, err := crypto.BytesToPublicKey(keyValue)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Kai2Z", "Errors.Internal")
}
return &rsaPublicKey{
key: key{
id: model.AggregateID,
creationDate: model.CreationDate,
changeDate: model.ChangeDate,
sequence: model.ProcessedSequence,
resourceOwner: model.ResourceOwner,
algorithm: model.Algorithm,
use: model.Usage,
},
expiry: model.Expiry,
publicKey: publicKey,
}, nil
}

View File

@@ -1,453 +0,0 @@
package query
import (
"context"
"crypto/rsa"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"io"
"math/big"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
preparePublicKeysStmt = `SELECT projections.keys4.id,` +
` projections.keys4.creation_date,` +
` projections.keys4.change_date,` +
` projections.keys4.sequence,` +
` projections.keys4.resource_owner,` +
` projections.keys4.algorithm,` +
` projections.keys4.use,` +
` projections.keys4_public.expiry,` +
` projections.keys4_public.key,` +
` COUNT(*) OVER ()` +
` FROM projections.keys4` +
` LEFT JOIN projections.keys4_public ON projections.keys4.id = projections.keys4_public.id AND projections.keys4.instance_id = projections.keys4_public.instance_id`
preparePublicKeysCols = []string{
"id",
"creation_date",
"change_date",
"sequence",
"resource_owner",
"algorithm",
"use",
"expiry",
"key",
"count",
}
preparePrivateKeysStmt = `SELECT projections.keys4.id,` +
` projections.keys4.creation_date,` +
` projections.keys4.change_date,` +
` projections.keys4.sequence,` +
` projections.keys4.resource_owner,` +
` projections.keys4.algorithm,` +
` projections.keys4.use,` +
` projections.keys4_private.expiry,` +
` projections.keys4_private.key,` +
` COUNT(*) OVER ()` +
` FROM projections.keys4` +
` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id`
)
func Test_KeyPrepares(t *testing.T) {
type want struct {
sqlExpectations sqlExpectation
err checkErr
}
tests := []struct {
name string
prepare interface{}
want want
object interface{}
}{
{
name: "preparePublicKeysQuery no result",
prepare: preparePublicKeysQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(preparePublicKeysStmt),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: &PublicKeys{Keys: []PublicKey{}},
},
{
name: "preparePublicKeysQuery found",
prepare: preparePublicKeysQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(preparePublicKeysStmt),
preparePublicKeysCols,
[][]driver.Value{
{
"key-id",
testNow,
testNow,
uint64(20211109),
"ro",
"RS256",
0,
testNow,
[]byte("-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsvX9P58JFxEs5C+L+H7W\nduFSWL5EPzber7C2m94klrSV6q0bAcrYQnGwFOlveThsY200hRbadKaKjHD7qIKH\nDEe0IY2PSRht33Jye52AwhkRw+M3xuQH/7R8LydnsNFk2KHpr5X2SBv42e37LjkE\nslKSaMRgJW+v0KZ30piY8QsdFRKKaVg5/Ajt1YToM1YVsdHXJ3vmXFMtypLdxwUD\ndIaLEX6pFUkU75KSuEQ/E2luT61Q3ta9kOWm9+0zvi7OMcbdekJT7mzcVnh93R1c\n13ZhQCLbh9A7si8jKFtaMWevjayrvqQABEcTN9N4Hoxcyg6l4neZtRDk75OMYcqm\nDQIDAQAB\n-----END RSA PUBLIC KEY-----\n"),
},
},
),
},
object: &PublicKeys{
SearchResponse: SearchResponse{
Count: 1,
},
Keys: []PublicKey{
&rsaPublicKey{
key: key{
id: "key-id",
creationDate: testNow,
changeDate: testNow,
sequence: 20211109,
resourceOwner: "ro",
algorithm: "RS256",
use: crypto.KeyUsageSigning,
},
expiry: testNow,
publicKey: &rsa.PublicKey{
E: 65537,
N: fromBase16("b2f5fd3f9f0917112ce42f8bf87ed676e15258be443f36deafb0b69bde2496b495eaad1b01cad84271b014e96f79386c636d348516da74a68a8c70fba882870c47b4218d8f49186ddf72727b9d80c21911c3e337c6e407ffb47c2f2767b0d164d8a1e9af95f6481bf8d9edfb2e3904b2529268c460256fafd0a677d29898f10b1d15128a695839fc08edd584e8335615b1d1d7277be65c532dca92ddc7050374868b117ea9154914ef9292b8443f13696e4fad50ded6bd90e5a6f7ed33be2ece31c6dd7a4253ee6cdc56787ddd1d5cd776614022db87d03bb22f23285b5a3167af8dacabbea40004471337d3781e8c5cca0ea5e27799b510e4ef938c61caa60d"),
},
},
},
},
},
{
name: "preparePublicKeysQuery sql err",
prepare: preparePublicKeysQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(preparePublicKeysStmt),
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: (*PublicKeys)(nil),
},
{
name: "preparePrivateKeysQuery no result",
prepare: preparePrivateKeysQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(preparePrivateKeysStmt),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: &PrivateKeys{Keys: []PrivateKey{}},
},
{
name: "preparePrivateKeysQuery found",
prepare: preparePrivateKeysQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(preparePrivateKeysStmt),
preparePublicKeysCols,
[][]driver.Value{
{
"key-id",
testNow,
testNow,
uint64(20211109),
"ro",
"RS256",
0,
testNow,
[]byte(`{"Algorithm": "enc", "Crypted": "cHJpdmF0ZUtleQ==", "CryptoType": 0, "KeyID": "id"}`),
},
},
),
},
object: &PrivateKeys{
SearchResponse: SearchResponse{
Count: 1,
},
Keys: []PrivateKey{
&privateKey{
key: key{
id: "key-id",
creationDate: testNow,
changeDate: testNow,
sequence: 20211109,
resourceOwner: "ro",
algorithm: "RS256",
use: crypto.KeyUsageSigning,
},
expiry: testNow,
privateKey: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
},
},
},
},
{
name: "preparePrivateKeysQuery sql err",
prepare: preparePrivateKeysQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(preparePrivateKeysStmt),
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: (*PrivateKeys)(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)
})
}
}
func fromBase16(base16 string) *big.Int {
i, ok := new(big.Int).SetString(base16, 16)
if !ok {
panic("bad number: " + base16)
}
return i
}
const pubKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs38btwb3c7r0tMaQpGvB
mY+mPwMU/LpfuPoC0k2t4RsKp0fv40SMl50CRrHgk395wch8PMPYbl3+8TtYAJuy
rFALIj3Ff1UcKIk0hOH5DDsfh7/q2wFuncTmS6bifYo8CfSq2vDGnM7nZnEvxY/M
fSydZdcmIqlkUpfQmtzExw9+tSe5Dxq6gn5JtlGgLgZGt69r5iMMrTEGhhVAXzNu
MZbmlCoBru+rC8ITlTX/0V1ZcsSbL8tYWhthyu9x6yjo1bH85wiVI4gs0MhU8f2a
+kjL/KGZbR14Ua2eo6tonBZLC5DHWM2TkYXgRCDPufjcgmzN0Lm91E4P8KvBcvly
6QIDAQAB
-----END PUBLIC KEY-----
`
func TestQueries_GetPublicKeyByID(t *testing.T) {
now := time.Now()
future := now.Add(time.Hour)
tests := []struct {
name string
eventstore func(*testing.T) *eventstore.Eventstore
encryption func(*testing.T) *crypto.MockEncryptionAlgorithm
want *rsaPublicKey
wantErr error
}{
{
name: "filter error",
eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe),
),
wantErr: io.ErrClosedPipe,
},
{
name: "not found error",
eventstore: expectEventstore(
expectFilter(),
),
wantErr: zerrors.ThrowNotFound(nil, "QUERY-Ahf7x", "Errors.Key.NotFound"),
},
{
name: "decrypt error",
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(key_repo.NewAddedEvent(context.Background(),
&eventstore.Aggregate{
ID: "keyID",
Type: key_repo.AggregateType,
ResourceOwner: "instanceID",
InstanceID: "instanceID",
Version: key_repo.AggregateVersion,
},
crypto.KeyUsageSigning, "alg",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "keyID",
Crypted: []byte("private"),
},
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "keyID",
Crypted: []byte("public"),
},
future,
future,
)),
),
),
encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm {
encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
expect := encryption.EXPECT()
expect.Algorithm().Return("alg")
expect.DecryptionKeyIDs().Return([]string{})
return encryption
},
wantErr: zerrors.ThrowInternal(nil, "QUERY-Ie4oh", "Errors.Internal"),
},
{
name: "parse error",
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(key_repo.NewAddedEvent(context.Background(),
&eventstore.Aggregate{
ID: "keyID",
Type: key_repo.AggregateType,
ResourceOwner: "instanceID",
InstanceID: "instanceID",
Version: key_repo.AggregateVersion,
},
crypto.KeyUsageSigning, "alg",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "keyID",
Crypted: []byte("private"),
},
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "keyID",
Crypted: []byte("public"),
},
future,
future,
)),
),
),
encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm {
encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
expect := encryption.EXPECT()
expect.Algorithm().Return("alg")
expect.DecryptionKeyIDs().Return([]string{"keyID"})
expect.Decrypt([]byte("public"), "keyID").Return([]byte("foo"), nil)
return encryption
},
wantErr: zerrors.ThrowInternal(nil, "QUERY-Kai2Z", "Errors.Internal"),
},
{
name: "success",
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(key_repo.NewAddedEvent(context.Background(),
&eventstore.Aggregate{
ID: "keyID",
Type: key_repo.AggregateType,
ResourceOwner: "instanceID",
InstanceID: "instanceID",
Version: key_repo.AggregateVersion,
},
crypto.KeyUsageSigning, "alg",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "keyID",
Crypted: []byte("private"),
},
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "keyID",
Crypted: []byte("public"),
},
future,
future,
)),
),
),
encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm {
encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
expect := encryption.EXPECT()
expect.Algorithm().Return("alg")
expect.DecryptionKeyIDs().Return([]string{"keyID"})
expect.Decrypt([]byte("public"), "keyID").Return([]byte(pubKey), nil)
return encryption
},
want: &rsaPublicKey{
key: key{
id: "keyID",
resourceOwner: "instanceID",
algorithm: "alg",
use: crypto.KeyUsageSigning,
},
expiry: future,
publicKey: func() *rsa.PublicKey {
publicKey, err := crypto.BytesToPublicKey([]byte(pubKey))
if err != nil {
panic(err)
}
return publicKey
}(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := &Queries{
eventstore: tt.eventstore(t),
}
if tt.encryption != nil {
q.keyEncryptionAlgorithm = tt.encryption(t)
}
ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")
key, err := q.GetPublicKeyByID(ctx, "keyID")
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
require.NotNil(t, key)
got := key.(*rsaPublicKey)
assert.WithinDuration(t, tt.want.expiry, got.expiry, time.Second)
tt.want.expiry = time.Time{}
got.expiry = time.Time{}
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -35,8 +35,17 @@ func NewMemberLastNameSearchQuery(method TextComparison, value string) (SearchQu
}
func NewMemberUserIDSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(membershipUserID, value, TextEquals)
return NewTextQuery(MembershipUserID, value, TextEquals)
}
func NewMemberInUserIDsSearchQuery(ids []string) (SearchQuery, error) {
list := make([]interface{}, len(ids))
for i, value := range ids {
list[i] = value
}
return NewListQuery(MembershipUserID, list, ListIn)
}
func NewMemberResourceOwnerSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(membershipResourceOwner, value, TextEquals)
}

View File

@@ -128,7 +128,7 @@ func prepareProjectGrantMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Memb
LeftJoin(join(MachineUserIDCol, ProjectGrantMemberUserID)).
LeftJoin(join(UserIDCol, ProjectGrantMemberUserID)).
LeftJoin(join(LoginNameUserIDCol, ProjectGrantMemberUserID)).
LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID)).
LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID) + " AND " + ProjectGrantMemberProjectID.identifier() + " = " + ProjectGrantColumnProjectID.identifier()).
Where(
sq.Eq{LoginNameIsPrimaryCol.identifier(): true},
).PlaceholderFormat(sq.Dollar),

View File

@@ -46,6 +46,7 @@ var (
"LEFT JOIN projections.project_grants4 " +
"ON members.grant_id = projections.project_grants4.grant_id " +
"AND members.instance_id = projections.project_grants4.instance_id " +
"AND members.project_id = projections.project_grants4.project_id " +
"WHERE projections.login_names3.is_primary = $1")
projectGrantMembersColumns = []string{
"creation_date",

View File

@@ -0,0 +1,144 @@
package projection
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
HostedLoginTranslationTable = "projections.hosted_login_translations"
HostedLoginTranslationInstanceIDCol = "instance_id"
HostedLoginTranslationCreationDateCol = "creation_date"
HostedLoginTranslationChangeDateCol = "change_date"
HostedLoginTranslationAggregateIDCol = "aggregate_id"
HostedLoginTranslationAggregateTypeCol = "aggregate_type"
HostedLoginTranslationSequenceCol = "sequence"
HostedLoginTranslationLocaleCol = "locale"
HostedLoginTranslationFileCol = "file"
HostedLoginTranslationEtagCol = "etag"
)
type hostedLoginTranslationProjection struct{}
func newHostedLoginTranslationProjection(ctx context.Context, config handler.Config) *handler.Handler {
return handler.NewHandler(ctx, &config, new(hostedLoginTranslationProjection))
}
// Init implements [handler.initializer]
func (p *hostedLoginTranslationProjection) Init() *old_handler.Check {
return handler.NewTableCheck(
handler.NewTable([]*handler.InitColumn{
handler.NewColumn(HostedLoginTranslationInstanceIDCol, handler.ColumnTypeText),
handler.NewColumn(HostedLoginTranslationCreationDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(HostedLoginTranslationChangeDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(HostedLoginTranslationAggregateIDCol, handler.ColumnTypeText),
handler.NewColumn(HostedLoginTranslationAggregateTypeCol, handler.ColumnTypeText),
handler.NewColumn(HostedLoginTranslationSequenceCol, handler.ColumnTypeInt64),
handler.NewColumn(HostedLoginTranslationLocaleCol, handler.ColumnTypeText),
handler.NewColumn(HostedLoginTranslationFileCol, handler.ColumnTypeJSONB),
handler.NewColumn(HostedLoginTranslationEtagCol, handler.ColumnTypeText),
},
handler.NewPrimaryKey(
HostedLoginTranslationInstanceIDCol,
HostedLoginTranslationAggregateIDCol,
HostedLoginTranslationAggregateTypeCol,
HostedLoginTranslationLocaleCol,
),
),
)
}
func (hltp *hostedLoginTranslationProjection) Name() string {
return HostedLoginTranslationTable
}
func (hltp *hostedLoginTranslationProjection) Reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: org.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: org.HostedLoginTranslationSet,
Reduce: hltp.reduceSet,
},
},
},
{
Aggregate: instance.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: instance.HostedLoginTranslationSet,
Reduce: hltp.reduceSet,
},
},
},
}
}
func (hltp *hostedLoginTranslationProjection) reduceSet(e eventstore.Event) (*handler.Statement, error) {
switch e := e.(type) {
case *org.HostedLoginTranslationSetEvent:
orgEvent := *e
return handler.NewUpsertStatement(
&orgEvent,
[]handler.Column{
handler.NewCol(HostedLoginTranslationInstanceIDCol, nil),
handler.NewCol(HostedLoginTranslationAggregateIDCol, nil),
handler.NewCol(HostedLoginTranslationAggregateTypeCol, nil),
handler.NewCol(HostedLoginTranslationLocaleCol, nil),
},
[]handler.Column{
handler.NewCol(HostedLoginTranslationInstanceIDCol, orgEvent.Aggregate().InstanceID),
handler.NewCol(HostedLoginTranslationAggregateIDCol, orgEvent.Aggregate().ID),
handler.NewCol(HostedLoginTranslationAggregateTypeCol, orgEvent.Aggregate().Type),
handler.NewCol(HostedLoginTranslationCreationDateCol, handler.OnlySetValueOnInsert(HostedLoginTranslationTable, orgEvent.CreationDate())),
handler.NewCol(HostedLoginTranslationChangeDateCol, orgEvent.CreationDate()),
handler.NewCol(HostedLoginTranslationSequenceCol, orgEvent.Sequence()),
handler.NewCol(HostedLoginTranslationLocaleCol, orgEvent.Language),
handler.NewCol(HostedLoginTranslationFileCol, orgEvent.Translation),
handler.NewCol(HostedLoginTranslationEtagCol, hltp.computeEtag(orgEvent.Translation)),
},
), nil
case *instance.HostedLoginTranslationSetEvent:
instanceEvent := *e
return handler.NewUpsertStatement(
&instanceEvent,
[]handler.Column{
handler.NewCol(HostedLoginTranslationInstanceIDCol, nil),
handler.NewCol(HostedLoginTranslationAggregateIDCol, nil),
handler.NewCol(HostedLoginTranslationAggregateTypeCol, nil),
handler.NewCol(HostedLoginTranslationLocaleCol, nil),
},
[]handler.Column{
handler.NewCol(HostedLoginTranslationInstanceIDCol, instanceEvent.Aggregate().InstanceID),
handler.NewCol(HostedLoginTranslationAggregateIDCol, instanceEvent.Aggregate().ID),
handler.NewCol(HostedLoginTranslationAggregateTypeCol, instanceEvent.Aggregate().Type),
handler.NewCol(HostedLoginTranslationCreationDateCol, handler.OnlySetValueOnInsert(HostedLoginTranslationTable, instanceEvent.CreationDate())),
handler.NewCol(HostedLoginTranslationChangeDateCol, instanceEvent.CreationDate()),
handler.NewCol(HostedLoginTranslationSequenceCol, instanceEvent.Sequence()),
handler.NewCol(HostedLoginTranslationLocaleCol, instanceEvent.Language),
handler.NewCol(HostedLoginTranslationFileCol, instanceEvent.Translation),
handler.NewCol(HostedLoginTranslationEtagCol, hltp.computeEtag(instanceEvent.Translation)),
},
), nil
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-AZshaa", "reduce.wrong.event.type %v", []eventstore.EventType{org.HostedLoginTranslationSet})
}
}
func (hltp *hostedLoginTranslationProjection) computeEtag(translation map[string]any) string {
hash := md5.Sum(fmt.Append(nil, translation))
return hex.EncodeToString(hash[:])
}

View File

@@ -64,14 +64,6 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceLoginDefaultOrgEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: feature_v2.InstanceLegacyIntrospectionEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: feature_v2.InstanceUserSchemaEventType,
Reduce: reduceInstanceSetFeature[bool],
@@ -84,10 +76,6 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceImprovedPerformanceEventType,
Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType],
},
{
Event: feature_v2.InstanceWebKeyEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: feature_v2.InstanceDebugOIDCParentErrorEventType,
Reduce: reduceInstanceSetFeature[bool],

View File

@@ -26,7 +26,7 @@ func TestInstanceFeaturesProjection_reduces(t *testing.T) {
args: args{
event: getEvent(
testEvent(
feature_v2.InstanceLegacyIntrospectionEventType,
feature_v2.SystemUserSchemaEventType,
feature_v2.AggregateType,
[]byte(`{"value": true}`),
), eventstore.GenericEventMapper[feature_v2.SetEvent[bool]]),
@@ -41,7 +41,7 @@ func TestInstanceFeaturesProjection_reduces(t *testing.T) {
expectedStmt: "INSERT INTO projections.instance_features2 (instance_id, key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.instance_features2.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
expectedArgs: []interface{}{
"agg-id",
"legacy_introspection",
"user_schema",
anyArg{},
anyArg{},
uint64(15),

View File

@@ -88,6 +88,7 @@ var (
UserSchemaProjection *handler.Handler
WebKeyProjection *handler.Handler
DebugEventsProjection *handler.Handler
HostedLoginTranslationProjection *handler.Handler
ProjectGrantFields *handler.FieldHandler
OrgDomainVerifiedFields *handler.FieldHandler
@@ -183,6 +184,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"]))
WebKeyProjection = newWebKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["web_keys"]))
DebugEventsProjection = newDebugEventsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_events"]))
HostedLoginTranslationProjection = newHostedLoginTranslationProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["hosted_login_translation"]))
ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsProjectGrant]))
OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified]))
@@ -363,5 +365,6 @@ func newProjectionsList() {
UserSchemaProjection,
WebKeyProjection,
DebugEventsProjection,
HostedLoginTranslationProjection,
}
}

View File

@@ -56,14 +56,6 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.SystemLoginDefaultOrgEventType,
Reduce: reduceSystemSetFeature[bool],
},
{
Event: feature_v2.SystemTriggerIntrospectionProjectionsEventType,
Reduce: reduceSystemSetFeature[bool],
},
{
Event: feature_v2.SystemLegacyIntrospectionEventType,
Reduce: reduceSystemSetFeature[bool],
},
{
Event: feature_v2.SystemUserSchemaEventType,
Reduce: reduceSystemSetFeature[bool],

View File

@@ -24,7 +24,7 @@ func TestSystemFeaturesProjection_reduces(t *testing.T) {
args: args{
event: getEvent(
testEvent(
feature_v2.SystemLegacyIntrospectionEventType,
feature_v2.SystemUserSchemaEventType,
feature_v2.AggregateType,
[]byte(`{"value": true}`),
), eventstore.GenericEventMapper[feature_v2.SetEvent[bool]]),
@@ -38,7 +38,7 @@ func TestSystemFeaturesProjection_reduces(t *testing.T) {
{
expectedStmt: "INSERT INTO projections.system_features (key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.system_features.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
expectedArgs: []interface{}{
"legacy_introspection",
"user_schema",
anyArg{},
anyArg{},
uint64(15),

View File

@@ -20,17 +20,15 @@ func (f *FeatureSource[T]) set(level feature.Level, value any) {
type SystemFeatures struct {
Details *domain.ObjectDetails
LoginDefaultOrg FeatureSource[bool]
TriggerIntrospectionProjections FeatureSource[bool]
LegacyIntrospection FeatureSource[bool]
UserSchema FeatureSource[bool]
TokenExchange FeatureSource[bool]
ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
OIDCSingleV1SessionTermination FeatureSource[bool]
DisableUserTokenEvent FeatureSource[bool]
EnableBackChannelLogout FeatureSource[bool]
LoginV2 FeatureSource[*feature.LoginV2]
PermissionCheckV2 FeatureSource[bool]
LoginDefaultOrg FeatureSource[bool]
UserSchema FeatureSource[bool]
TokenExchange FeatureSource[bool]
ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
OIDCSingleV1SessionTermination FeatureSource[bool]
DisableUserTokenEvent FeatureSource[bool]
EnableBackChannelLogout FeatureSource[bool]
LoginV2 FeatureSource[*feature.LoginV2]
PermissionCheckV2 FeatureSource[bool]
}
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {

View File

@@ -56,8 +56,6 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
EventTypes(
feature_v2.SystemResetEventType,
feature_v2.SystemLoginDefaultOrgEventType,
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
feature_v2.SystemLegacyIntrospectionEventType,
feature_v2.SystemUserSchemaEventType,
feature_v2.SystemTokenExchangeEventType,
feature_v2.SystemImprovedPerformanceEventType,
@@ -81,15 +79,10 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S
return err
}
switch key {
case feature.KeyUnspecified,
feature.KeyActionsDeprecated:
case feature.KeyUnspecified:
return nil
case feature.KeyLoginDefaultOrg:
features.LoginDefaultOrg.set(level, event.Value)
case feature.KeyTriggerIntrospectionProjections:
features.TriggerIntrospectionProjections.set(level, event.Value)
case feature.KeyLegacyIntrospection:
features.LegacyIntrospection.set(level, event.Value)
case feature.KeyUserSchema:
features.UserSchema.set(level, event.Value)
case feature.KeyTokenExchange:

View File

@@ -49,14 +49,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemLoginDefaultOrgEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, false,
@@ -71,14 +63,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
Level: feature.LevelSystem,
Value: false,
},
TriggerIntrospectionProjections: FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
LegacyIntrospection: FeatureSource[bool]{
Level: feature.LevelSystem,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelSystem,
Value: false,
@@ -93,14 +77,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemLoginDefaultOrgEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, false,
@@ -109,10 +85,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemResetEventType,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
)),
),
),
want: &SystemFeatures{
@@ -123,14 +95,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
Level: feature.LevelUnspecified,
Value: false,
},
TriggerIntrospectionProjections: FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
LegacyIntrospection: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
@@ -145,14 +109,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemLoginDefaultOrgEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, false,
@@ -161,10 +117,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemResetEventType,
)),
eventFromEventPusher(feature_v2.NewSetEvent(
context.Background(), aggregate,
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
)),
),
),
want: &SystemFeatures{
@@ -175,14 +127,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
Level: feature.LevelUnspecified,
Value: false,
},
TriggerIntrospectionProjections: FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
LegacyIntrospection: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,

View File

@@ -697,6 +697,35 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn
return isUnique, err
}
//go:embed user_claimed_user_ids.sql
var userClaimedUserIDOfOrgDomain string
func (q *Queries) SearchClaimedUserIDsOfOrgDomain(ctx context.Context, domain, orgID string) (userIDs []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
err = q.client.QueryContext(ctx,
func(rows *sql.Rows) error {
userIDs = make([]string, 0)
for rows.Next() {
var userID string
err := rows.Scan(&userID)
if err != nil {
return err
}
userIDs = append(userIDs, userID)
}
return nil
},
userClaimedUserIDOfOrgDomain,
authz.GetInstance(ctx).InstanceID(),
"%@"+domain,
orgID,
)
return userIDs, err
}
func (q *UserSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = q.SearchRequest.toQuery(query)
for _, q := range q.Queries {

View File

@@ -0,0 +1,13 @@
SELECT u.id
FROM projections.login_names3_users u
LEFT JOIN projections.login_names3_policies p_custom
ON u.instance_id = p_custom.instance_id
AND p_custom.instance_id = $1
AND p_custom.resource_owner = u.resource_owner
JOIN projections.login_names3_policies p_default
ON u.instance_id = p_default.instance_id
AND p_default.instance_id = $1 AND p_default.is_default IS TRUE
WHERE u.instance_id = $1
AND COALESCE(p_custom.must_be_domain, p_default.must_be_domain) = false
AND u.user_name_lower like $2
AND u.resource_owner <> $3;

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"slices"
"time"
sq "github.com/Masterminds/squirrel"
@@ -44,8 +45,9 @@ type UserGrant struct {
OrgName string `json:"org_name,omitempty"`
OrgPrimaryDomain string `json:"org_primary_domain,omitempty"`
ProjectID string `json:"project_id,omitempty"`
ProjectName string `json:"project_name,omitempty"`
ProjectResourceOwner string `json:"project_resource_owner,omitempty"`
ProjectID string `json:"project_id,omitempty"`
ProjectName string `json:"project_name,omitempty"`
GrantedOrgID string `json:"granted_org_id,omitempty"`
GrantedOrgName string `json:"granted_org_name,omitempty"`
@@ -57,6 +59,27 @@ type UserGrants struct {
UserGrants []*UserGrant
}
func userGrantsCheckPermission(ctx context.Context, grants *UserGrants, permissionCheck domain.PermissionCheck) {
grants.UserGrants = slices.DeleteFunc(grants.UserGrants,
func(grant *UserGrant) bool {
return userGrantCheckPermission(ctx, grant.ResourceOwner, grant.ProjectID, grant.GrantID, grant.UserID, permissionCheck) != nil
},
)
}
func userGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, userID string, permissionCheck domain.PermissionCheck) error {
// you should always be able to read your own permissions
if authz.GetCtxData(ctx).UserID == userID {
return nil
}
// check permission on the project grant
if grantID != "" {
return permissionCheck(ctx, domain.PermissionUserGrantRead, resourceOwner, grantID)
}
// check on project
return permissionCheck(ctx, domain.PermissionUserGrantRead, resourceOwner, projectID)
}
type UserGrantsQueries struct {
SearchRequest
Queries []SearchQuery
@@ -70,6 +93,21 @@ func (q *UserGrantsQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query
}
func userGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserGrantsQueries) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
UserGrantResourceOwner,
domain.PermissionUserGrantRead,
SingleOrgPermissionOption(queries.Queries),
WithProjectsPermissionOption(UserGrantProjectID),
OwnedRowsPermissionOption(UserGrantUserID),
)
return query.JoinClause(join, args...)
}
func NewUserGrantUserIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(UserGrantUserID, id, TextEquals)
}
@@ -78,14 +116,6 @@ func NewUserGrantProjectIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(UserGrantProjectID, id, TextEquals)
}
func NewUserGrantProjectIDsSearchQuery(ids []string) (SearchQuery, error) {
list := make([]interface{}, len(ids))
for i, value := range ids {
list[i] = value
}
return NewListQuery(UserGrantProjectID, list, ListIn)
}
func NewUserGrantProjectOwnerSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(ProjectColumnResourceOwner, id, TextEquals)
}
@@ -94,6 +124,10 @@ func NewUserGrantResourceOwnerSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(UserGrantResourceOwner, id, TextEquals)
}
func NewUserGrantUserResourceOwnerSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(UserResourceOwnerCol, id, TextEquals)
}
func NewUserGrantGrantIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(UserGrantGrantID, id, TextEquals)
}
@@ -102,6 +136,14 @@ func NewUserGrantIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(UserGrantID, id, TextEquals)
}
func NewUserGrantInIDsSearchQuery(ids []string) (SearchQuery, error) {
list := make([]interface{}, len(ids))
for i, value := range ids {
list[i] = value
}
return NewListQuery(UserGrantID, list, ListIn)
}
func NewUserGrantUserTypeQuery(typ domain.UserType) (SearchQuery, error) {
return NewNumberQuery(UserTypeCol, typ, NumberEquals)
}
@@ -262,7 +304,19 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, queries
return grant, err
}
func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool) (grants *UserGrants, err error) {
func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool, permissionCheck domain.PermissionCheck) (*UserGrants, error) {
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
grants, err := q.userGrants(ctx, queries, shouldTriggerBulk, permissionCheckV2)
if err != nil {
return nil, err
}
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
userGrantsCheckPermission(ctx, grants, permissionCheck)
}
return grants, nil
}
func (q *Queries) userGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool, permissionCheckV2 bool) (grants *UserGrants, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -274,6 +328,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh
}
query, scan := prepareUserGrantsQuery()
query = userGrantPermissionCheckV2(ctx, query, permissionCheckV2, queries)
eq := sq.Eq{UserGrantInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
@@ -324,6 +379,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro
UserGrantProjectID.identifier(),
ProjectColumnName.identifier(),
ProjectColumnResourceOwner.identifier(),
GrantedOrgColumnId.identifier(),
GrantedOrgColumnName.identifier(),
@@ -334,7 +390,8 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro
LeftJoin(join(HumanUserIDCol, UserGrantUserID)).
LeftJoin(join(OrgColumnID, UserGrantResourceOwner)).
LeftJoin(join(ProjectColumnID, UserGrantProjectID)).
LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)).
LeftJoin(join(ProjectGrantColumnGrantID, UserGrantGrantID) + " AND " + ProjectGrantColumnProjectID.identifier() + " = " + UserGrantProjectID.identifier()).
LeftJoin(join(GrantedOrgColumnId, ProjectGrantColumnGrantedOrgID)).
LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)).
Where(
sq.Eq{LoginNameIsPrimaryCol.identifier(): true},
@@ -356,7 +413,8 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro
orgName sql.NullString
orgDomain sql.NullString
projectName sql.NullString
projectName sql.NullString
projectResourceOwner sql.NullString
grantedOrgID sql.NullString
grantedOrgName sql.NullString
@@ -389,6 +447,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro
&g.ProjectID,
&projectName,
&projectResourceOwner,
&grantedOrgID,
&grantedOrgName,
@@ -413,6 +472,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro
g.OrgName = orgName.String
g.OrgPrimaryDomain = orgDomain.String
g.ProjectName = projectName.String
g.ProjectResourceOwner = projectResourceOwner.String
g.GrantedOrgID = grantedOrgID.String
g.GrantedOrgName = grantedOrgName.String
g.GrantedOrgDomain = grantedOrgDomain.String
@@ -447,6 +507,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e
UserGrantProjectID.identifier(),
ProjectColumnName.identifier(),
ProjectColumnResourceOwner.identifier(),
GrantedOrgColumnId.identifier(),
GrantedOrgColumnName.identifier(),
@@ -459,7 +520,8 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e
LeftJoin(join(HumanUserIDCol, UserGrantUserID)).
LeftJoin(join(OrgColumnID, UserGrantResourceOwner)).
LeftJoin(join(ProjectColumnID, UserGrantProjectID)).
LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)).
LeftJoin(join(ProjectGrantColumnGrantID, UserGrantGrantID) + " AND " + ProjectGrantColumnProjectID.identifier() + " = " + UserGrantProjectID.identifier()).
LeftJoin(join(GrantedOrgColumnId, ProjectGrantColumnGrantedOrgID)).
LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)).
Where(
sq.Eq{LoginNameIsPrimaryCol.identifier(): true},
@@ -488,7 +550,8 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e
grantedOrgName sql.NullString
grantedOrgDomain sql.NullString
projectName sql.NullString
projectName sql.NullString
projectResourceOwner sql.NullString
)
err := rows.Scan(
@@ -517,6 +580,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e
&g.ProjectID,
&projectName,
&projectResourceOwner,
&grantedOrgID,
&grantedOrgName,
@@ -540,6 +604,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e
g.OrgName = orgName.String
g.OrgPrimaryDomain = orgDomain.String
g.ProjectName = projectName.String
g.ProjectResourceOwner = projectResourceOwner.String
g.GrantedOrgID = grantedOrgID.String
g.GrantedOrgName = grantedOrgName.String
g.GrantedOrgDomain = grantedOrgDomain.String

View File

@@ -37,6 +37,7 @@ var (
", projections.orgs1.primary_domain" +
", projections.user_grants5.project_id" +
", projections.projects4.name" +
", projections.projects4.resource_owner" +
", granted_orgs.id" +
", granted_orgs.name" +
", granted_orgs.primary_domain" +
@@ -45,7 +46,8 @@ var (
" LEFT JOIN projections.users14_humans ON projections.user_grants5.user_id = projections.users14_humans.user_id AND projections.user_grants5.instance_id = projections.users14_humans.instance_id" +
" LEFT JOIN projections.orgs1 ON projections.user_grants5.resource_owner = projections.orgs1.id AND projections.user_grants5.instance_id = projections.orgs1.instance_id" +
" LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" +
" LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" +
" LEFT JOIN projections.project_grants4 ON projections.user_grants5.grant_id = projections.project_grants4.grant_id AND projections.user_grants5.instance_id = projections.project_grants4.instance_id AND projections.project_grants4.project_id = projections.user_grants5.project_id" +
" LEFT JOIN projections.orgs1 AS granted_orgs ON projections.project_grants4.granted_org_id = granted_orgs.id AND projections.project_grants4.instance_id = granted_orgs.instance_id" +
" LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" +
" WHERE projections.login_names3.is_primary = $1")
userGrantCols = []string{
@@ -71,6 +73,7 @@ var (
"primary_domain",
"project_id",
"name", // project name
"resource_owner", // project_grant resource owner
"id", // granted org id
"name", // granted org name
"primary_domain", // granted org domain
@@ -98,6 +101,7 @@ var (
", projections.orgs1.primary_domain" +
", projections.user_grants5.project_id" +
", projections.projects4.name" +
", projections.projects4.resource_owner" +
", granted_orgs.id" +
", granted_orgs.name" +
", granted_orgs.primary_domain" +
@@ -107,7 +111,8 @@ var (
" LEFT JOIN projections.users14_humans ON projections.user_grants5.user_id = projections.users14_humans.user_id AND projections.user_grants5.instance_id = projections.users14_humans.instance_id" +
" LEFT JOIN projections.orgs1 ON projections.user_grants5.resource_owner = projections.orgs1.id AND projections.user_grants5.instance_id = projections.orgs1.instance_id" +
" LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" +
" LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" +
" LEFT JOIN projections.project_grants4 ON projections.user_grants5.grant_id = projections.project_grants4.grant_id AND projections.user_grants5.instance_id = projections.project_grants4.instance_id AND projections.project_grants4.project_id = projections.user_grants5.project_id" +
" LEFT JOIN projections.orgs1 AS granted_orgs ON projections.project_grants4.granted_org_id = granted_orgs.id AND projections.project_grants4.instance_id = granted_orgs.instance_id" +
" LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" +
" WHERE projections.login_names3.is_primary = $1")
userGrantsCols = append(
@@ -175,6 +180,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -182,31 +188,32 @@ func Test_UserGrantPrepares(t *testing.T) {
),
},
object: &UserGrant{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
{
@@ -239,6 +246,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -246,31 +254,32 @@ func Test_UserGrantPrepares(t *testing.T) {
),
},
object: &UserGrant{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeMachine,
UserResourceOwner: "resource-owner",
FirstName: "",
LastName: "",
Email: "",
DisplayName: "",
AvatarURL: "",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeMachine,
UserResourceOwner: "resource-owner",
FirstName: "",
LastName: "",
Email: "",
DisplayName: "",
AvatarURL: "",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
{
@@ -303,6 +312,7 @@ func Test_UserGrantPrepares(t *testing.T) {
nil,
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -310,31 +320,32 @@ func Test_UserGrantPrepares(t *testing.T) {
),
},
object: &UserGrant{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "",
OrgPrimaryDomain: "",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "",
OrgPrimaryDomain: "",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
{
@@ -367,6 +378,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
nil,
nil,
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -374,31 +386,32 @@ func Test_UserGrantPrepares(t *testing.T) {
),
},
object: &UserGrant{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "",
ProjectResourceOwner: "",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
{
@@ -431,6 +444,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -438,31 +452,32 @@ func Test_UserGrantPrepares(t *testing.T) {
),
},
object: &UserGrant{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
{
@@ -525,6 +540,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -538,31 +554,32 @@ func Test_UserGrantPrepares(t *testing.T) {
},
UserGrants: []*UserGrant{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
},
@@ -598,6 +615,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -611,31 +629,32 @@ func Test_UserGrantPrepares(t *testing.T) {
},
UserGrants: []*UserGrant{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeMachine,
UserResourceOwner: "resource-owner",
FirstName: "",
LastName: "",
Email: "",
DisplayName: "",
AvatarURL: "",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeMachine,
UserResourceOwner: "resource-owner",
FirstName: "",
LastName: "",
Email: "",
DisplayName: "",
AvatarURL: "",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
},
@@ -671,6 +690,7 @@ func Test_UserGrantPrepares(t *testing.T) {
nil,
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -684,31 +704,32 @@ func Test_UserGrantPrepares(t *testing.T) {
},
UserGrants: []*UserGrant{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeMachine,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "",
OrgPrimaryDomain: "",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeMachine,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "",
OrgPrimaryDomain: "",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
},
@@ -744,6 +765,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
nil,
nil,
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -757,31 +779,32 @@ func Test_UserGrantPrepares(t *testing.T) {
},
UserGrants: []*UserGrant{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "",
ProjectResourceOwner: "",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
},
@@ -817,6 +840,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -830,31 +854,32 @@ func Test_UserGrantPrepares(t *testing.T) {
},
UserGrants: []*UserGrant{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
},
@@ -890,6 +915,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -917,6 +943,7 @@ func Test_UserGrantPrepares(t *testing.T) {
"primary-domain",
"project-id",
"project-name",
"project-resource-owner",
"granted-org-id",
"granted-org-name",
"granted-org-domain",
@@ -930,58 +957,60 @@ func Test_UserGrantPrepares(t *testing.T) {
},
UserGrants: []*UserGrant{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211111,
Roles: database.TextArray[string]{"role-key"},
GrantID: "grant-id",
State: domain.UserGrantStateActive,
UserID: "user-id",
Username: "username",
UserType: domain.UserTypeHuman,
UserResourceOwner: "resource-owner",
FirstName: "first-name",
LastName: "last-name",
Email: "email",
DisplayName: "display-name",
AvatarURL: "avatar-key",
PreferredLoginName: "login-name",
ResourceOwner: "ro",
OrgName: "org-name",
OrgPrimaryDomain: "primary-domain",
ProjectID: "project-id",
ProjectName: "project-name",
ProjectResourceOwner: "project-resource-owner",
GrantedOrgID: "granted-org-id",
GrantedOrgName: "granted-org-name",
GrantedOrgDomain: "granted-org-domain",
},
},
},

View File

@@ -63,7 +63,15 @@ type MembershipSearchQuery struct {
}
func NewMembershipUserIDQuery(userID string) (SearchQuery, error) {
return NewTextQuery(membershipUserID.setTable(membershipAlias), userID, TextEquals)
return NewTextQuery(MembershipUserID.setTable(membershipAlias), userID, TextEquals)
}
func NewMembershipCreationDateQuery(timestamp time.Time, comparison TimestampComparison) (SearchQuery, error) {
return NewTimestampQuery(MembershipCreationDate.setTable(membershipAlias), timestamp, comparison)
}
func NewMembershipChangeDateQuery(timestamp time.Time, comparison TimestampComparison) (SearchQuery, error) {
return NewTimestampQuery(MembershipChangeDate.setTable(membershipAlias), timestamp, comparison)
}
func NewMembershipOrgIDQuery(value string) (SearchQuery, error) {
@@ -137,7 +145,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer
wg.Wait()
}
query, queryArgs, scan := prepareMembershipsQuery(queries)
query, queryArgs, scan := prepareMembershipsQuery(ctx, queries, false)
eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
@@ -166,7 +174,7 @@ var (
name: "members",
instanceIDCol: projection.MemberInstanceID,
}
membershipUserID = Column{
MembershipUserID = Column{
name: projection.MemberUserIDCol,
table: membershipAlias,
}
@@ -174,11 +182,11 @@ var (
name: projection.MemberRolesCol,
table: membershipAlias,
}
membershipCreationDate = Column{
MembershipCreationDate = Column{
name: projection.MemberCreationDate,
table: membershipAlias,
}
membershipChangeDate = Column{
MembershipChangeDate = Column{
name: projection.MemberChangeDate,
table: membershipAlias,
}
@@ -216,11 +224,11 @@ var (
}
)
func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface{}) {
orgMembers, orgMembersArgs := prepareOrgMember(queries)
iamMembers, iamMembersArgs := prepareIAMMember(queries)
projectMembers, projectMembersArgs := prepareProjectMember(queries)
projectGrantMembers, projectGrantMembersArgs := prepareProjectGrantMember(queries)
func getMembershipFromQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) {
orgMembers, orgMembersArgs := prepareOrgMember(ctx, queries, permissionV2)
iamMembers, iamMembersArgs := prepareIAMMember(ctx, queries, permissionV2)
projectMembers, projectMembersArgs := prepareProjectMember(ctx, queries, permissionV2)
projectGrantMembers, projectGrantMembersArgs := prepareProjectGrantMember(ctx, queries, permissionV2)
args := make([]interface{}, 0)
args = append(append(append(append(args, orgMembersArgs...), iamMembersArgs...), projectMembersArgs...), projectGrantMembersArgs...)
@@ -236,13 +244,13 @@ func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface
args
}
func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) {
query, args := getMembershipFromQuery(queries)
func prepareMembershipsQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) {
query, args := getMembershipFromQuery(ctx, queries, permissionV2)
return sq.Select(
membershipUserID.identifier(),
MembershipUserID.identifier(),
membershipRoles.identifier(),
membershipCreationDate.identifier(),
membershipChangeDate.identifier(),
MembershipCreationDate.identifier(),
MembershipChangeDate.identifier(),
membershipSequence.identifier(),
membershipResourceOwner.identifier(),
membershipOrgID.identifier(),
@@ -257,7 +265,7 @@ func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder,
).From(query).
LeftJoin(join(ProjectColumnID, membershipProjectID)).
LeftJoin(join(OrgColumnID, membershipOrgID)).
LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)).
LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID) + " AND " + membershipProjectID.identifier() + " = " + ProjectGrantColumnProjectID.identifier()).
LeftJoin(join(InstanceColumnID, membershipInstanceID)).
PlaceholderFormat(sq.Dollar),
args,
@@ -340,7 +348,7 @@ func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder,
}
}
func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) {
func prepareOrgMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) {
builder := sq.Select(
OrgMemberUserID.identifier(),
OrgMemberRoles.identifier(),
@@ -354,6 +362,7 @@ func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) {
"NULL::TEXT AS "+membershipProjectID.name,
"NULL::TEXT AS "+membershipGrantID.name,
).From(orgMemberTable.identifier())
builder = administratorOrgPermissionCheckV2(ctx, builder, permissionV2)
for _, q := range query.Queries {
if q.Col().table.name == membershipAlias.name || q.Col().table.name == orgMemberTable.name {
@@ -363,7 +372,7 @@ func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) {
return builder.MustSql()
}
func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) {
func prepareIAMMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) {
builder := sq.Select(
InstanceMemberUserID.identifier(),
InstanceMemberRoles.identifier(),
@@ -377,6 +386,7 @@ func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) {
"NULL::TEXT AS "+membershipProjectID.name,
"NULL::TEXT AS "+membershipGrantID.name,
).From(instanceMemberTable.identifier())
builder = administratorInstancePermissionCheckV2(ctx, builder, permissionV2)
for _, q := range query.Queries {
if q.Col().table.name == membershipAlias.name || q.Col().table.name == instanceMemberTable.name {
@@ -386,7 +396,7 @@ func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) {
return builder.MustSql()
}
func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{}) {
func prepareProjectMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) {
builder := sq.Select(
ProjectMemberUserID.identifier(),
ProjectMemberRoles.identifier(),
@@ -400,6 +410,7 @@ func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{})
ProjectMemberProjectID.identifier(),
"NULL::TEXT AS "+membershipGrantID.name,
).From(projectMemberTable.identifier())
builder = administratorProjectPermissionCheckV2(ctx, builder, permissionV2)
for _, q := range query.Queries {
if q.Col().table.name == membershipAlias.name || q.Col().table.name == projectMemberTable.name {
@@ -410,7 +421,7 @@ func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{})
return builder.MustSql()
}
func prepareProjectGrantMember(query *MembershipSearchQuery) (string, []interface{}) {
func prepareProjectGrantMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) {
builder := sq.Select(
ProjectGrantMemberUserID.identifier(),
ProjectGrantMemberRoles.identifier(),
@@ -424,6 +435,7 @@ func prepareProjectGrantMember(query *MembershipSearchQuery) (string, []interfac
ProjectGrantMemberProjectID.identifier(),
ProjectGrantMemberGrantID.identifier(),
).From(projectGrantMemberTable.identifier())
builder = administratorProjectGrantPermissionCheckV2(ctx, builder, permissionV2)
for _, q := range query.Queries {
if q.Col().table.name == membershipAlias.name || q.Col().table.name == projectMemberTable.name || q.Col().table.name == projectGrantMemberTable.name {

View File

@@ -1,6 +1,7 @@
package query
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
@@ -85,7 +86,7 @@ var (
") AS members" +
" LEFT JOIN projections.projects4 ON members.project_id = projections.projects4.id AND members.instance_id = projections.projects4.instance_id" +
" LEFT JOIN projections.orgs1 ON members.org_id = projections.orgs1.id AND members.instance_id = projections.orgs1.instance_id" +
" LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id" +
" LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id AND members.project_id = projections.project_grants4.project_id" +
" LEFT JOIN projections.instances ON members.instance_id = projections.instances.id")
membershipCols = []string{
"user_id",
@@ -461,7 +462,7 @@ func Test_MembershipPrepares(t *testing.T) {
func prepareMembershipWrapper() func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) {
return func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) {
builder, _, fun := prepareMembershipsQuery(&MembershipSearchQuery{})
builder, _, fun := prepareMembershipsQuery(context.Background(), &MembershipSearchQuery{}, false)
return builder, fun
}
}

View File

@@ -4,12 +4,14 @@ import (
"context"
"database/sql"
"errors"
"slices"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
@@ -36,6 +38,28 @@ type UserMetadataSearchQueries struct {
Queries []SearchQuery
}
func userMetadataCheckPermission(ctx context.Context, userMetadataList *UserMetadataList, permissionCheck domain.PermissionCheck) {
userMetadataList.Metadata = slices.DeleteFunc(userMetadataList.Metadata,
func(userMetadata *UserMetadata) bool {
return userCheckPermission(ctx, userMetadata.ResourceOwner, userMetadata.UserID, permissionCheck) != nil
},
)
}
func userMetadataPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserMetadataSearchQueries) sq.SelectBuilder {
if !enabled {
return query
}
join, args := PermissionClause(
ctx,
UserMetadataResourceOwnerCol,
domain.PermissionUserRead,
SingleOrgPermissionOption(queries.Queries),
OwnedRowsPermissionOption(UserMetadataUserIDCol),
)
return query.JoinClause(join, args...)
}
var (
userMetadataTable = table{
name: projection.UserMetadataProjectionTable,
@@ -139,7 +163,19 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB
return metadata, err
}
func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, withOwnerRemoved bool) (metadata *UserMetadataList, err error) {
func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, permissionCheck domain.PermissionCheck) (metadata *UserMetadataList, err error) {
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
users, err := q.searchUserMetadata(ctx, shouldTriggerBulk, userID, queries, permissionCheckV2)
if err != nil {
return nil, err
}
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
userMetadataCheckPermission(ctx, users, permissionCheck)
}
return users, nil
}
func (q *Queries) searchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, permissionCheckV2 bool) (metadata *UserMetadataList, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -151,6 +187,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool
}
query, scan := prepareUserMetadataListQuery()
query = userMetadataPermissionCheckV2(ctx, query, permissionCheckV2, queries)
eq := sq.Eq{
UserMetadataUserIDCol.identifier(): userID,
UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),

View File

@@ -31,12 +31,6 @@ var oidcUserInfoTriggerHandlers = sync.OnceValue(func() []*handler.Handler {
}
})
// TriggerOIDCUserInfoProjections triggers all projections
// relevant to userinfo queries concurrently.
func TriggerOIDCUserInfoProjections(ctx context.Context) {
triggerBatch(ctx, oidcUserInfoTriggerHandlers()...)
}
var (
//go:embed userinfo_by_id.sql
oidcUserInfoQueryTmpl string

File diff suppressed because it is too large Load Diff