Remove platform tenancy from SXCHNGE customer BFF (#34)
This commit is contained in:
1 parent
7062672a87
commit
981e84a4b4
244 files changed
+651
-17130
No files matched your search
@@ -5,12 +5,10 @@ import (
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/template"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// DB is the interface which must be implemented by all db drivers
|
||||
type DB interface {
|
||||
template.DB[*model.Account]
|
||||
GetByEmail(ctx context.Context, email string) (*model.Account, error)
|
||||
GetAccountsByRefs(ctx context.Context, orgRef bson.ObjectID, refs []bson.ObjectID) ([]model.Account, error)
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
mongoimpl "git.sendico.io/tech/sxchnge/bff/pkg/db/internal/mongo"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type DBDriver string
|
||||
@@ -17,87 +11,3 @@ const (
|
||||
)
|
||||
|
||||
type Config = model.DriverConfig[DBDriver]
|
||||
|
||||
// PermissionsDatabaseConfig is the explicit public contract for services that
|
||||
// read authorization metadata from the central permissions database.
|
||||
type PermissionsDatabaseConfig Config
|
||||
|
||||
// AsDatabaseConfig converts the permissions DB contract to the existing DB
|
||||
// config type without duplicating driver parsing or connection behavior.
|
||||
func (c *PermissionsDatabaseConfig) AsDatabaseConfig() *Config {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
converted := Config(*c)
|
||||
return &converted
|
||||
}
|
||||
|
||||
// ResolvedDatabaseName identifies the database selected by a DB config.
|
||||
type ResolvedDatabaseName struct {
|
||||
Name string
|
||||
Source string
|
||||
}
|
||||
|
||||
// ResolveDatabaseName resolves only the database name portion of a DB config.
|
||||
// It intentionally mirrors the existing env-over-literal behavior and leaves
|
||||
// low-level connection validation to the normal DB connection path.
|
||||
func ResolveDatabaseName(config *Config) ResolvedDatabaseName {
|
||||
if config == nil {
|
||||
return ResolvedDatabaseName{}
|
||||
}
|
||||
return resolveDatabaseName(config)
|
||||
}
|
||||
|
||||
// ResolvePermissionsDatabaseName resolves only the database name portion of a
|
||||
// permissions DB config.
|
||||
func ResolvePermissionsDatabaseName(config *PermissionsDatabaseConfig) ResolvedDatabaseName {
|
||||
return ResolveDatabaseName(config.AsDatabaseConfig())
|
||||
}
|
||||
|
||||
// ValidateSplitDomainPermissionsDatabase verifies the generic split-domain
|
||||
// invariant for services that must keep authorization metadata separate from
|
||||
// domain storage.
|
||||
func ValidateSplitDomainPermissionsDatabase(service string, domain *Config, permissions *PermissionsDatabaseConfig) (ResolvedDatabaseName, ResolvedDatabaseName, error) {
|
||||
domainName := ResolveDatabaseName(domain)
|
||||
permissionsName := ResolvePermissionsDatabaseName(permissions)
|
||||
|
||||
if permissions == nil {
|
||||
return domainName, permissionsName, merrors.InvalidArgument(service+" permissions_database configuration is required", "permissions_database")
|
||||
}
|
||||
if strings.TrimSpace(domainName.Name) == "" {
|
||||
return domainName, permissionsName, merrors.InvalidArgument(service+" domain database name is required for split-domain validation", "database")
|
||||
}
|
||||
if strings.TrimSpace(permissionsName.Name) == "" {
|
||||
return domainName, permissionsName, merrors.InvalidArgument(service+" permissions database name is required for split-domain validation", "permissions_database")
|
||||
}
|
||||
if domainName.Name == permissionsName.Name {
|
||||
return domainName, permissionsName, merrors.InvalidArgument(service+" permissions_database must resolve to a different database than the domain database", "permissions_database")
|
||||
}
|
||||
|
||||
return domainName, permissionsName, nil
|
||||
}
|
||||
|
||||
func resolveDatabaseName(config *Config) ResolvedDatabaseName {
|
||||
if config == nil || config.Driver != Mongo {
|
||||
return ResolvedDatabaseName{}
|
||||
}
|
||||
var mongoConfig mongoimpl.Config
|
||||
if err := mapstructure.Decode(config.Settings, &mongoConfig); err != nil {
|
||||
return ResolvedDatabaseName{}
|
||||
}
|
||||
dbSettings := mongoimpl.Config2DBSettings(zap.NewNop(), &mongoConfig)
|
||||
source := ""
|
||||
if mongoConfig.DatabaseEnv != nil {
|
||||
source = "env:" + *mongoConfig.DatabaseEnv
|
||||
} else if mongoConfig.Database != nil {
|
||||
source = "settings.database"
|
||||
}
|
||||
if source != "" {
|
||||
return ResolvedDatabaseName{
|
||||
Name: dbSettings.Database,
|
||||
Source: source,
|
||||
}
|
||||
}
|
||||
|
||||
return ResolvedDatabaseName{}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/auth"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/account"
|
||||
mongoimpl "git.sendico.io/tech/sxchnge/bff/pkg/db/internal/mongo"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/invitation"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/policy"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/role"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/transaction"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
@@ -16,15 +12,9 @@ import (
|
||||
type Factory interface {
|
||||
NewAccountDB() (account.DB, error)
|
||||
NewAccountDBWithCollection(collection string) (account.DB, error)
|
||||
NewInvitationsDB() (invitation.DB, error)
|
||||
|
||||
NewRolesDB() (role.DB, error)
|
||||
NewPoliciesDB() (policy.DB, error)
|
||||
|
||||
TransactionFactory() transaction.Factory
|
||||
|
||||
Permissions() auth.Provider
|
||||
|
||||
CloseConnection()
|
||||
}
|
||||
|
||||
@@ -38,16 +28,3 @@ func NewConnection(logger mlogger.Logger, config *Config) (Factory, error) {
|
||||
}
|
||||
return nil, merrors.InvalidArgument("unknown database driver: "+string(config.Driver), "config.driver")
|
||||
}
|
||||
|
||||
// NewCentralPermissionsConnection opens the explicitly configured central
|
||||
// permissions database. It intentionally has no fallback to a service domain DB.
|
||||
func NewCentralPermissionsConnection(logger mlogger.Logger, config *PermissionsDatabaseConfig) (Factory, error) {
|
||||
if config == nil {
|
||||
return nil, merrors.InvalidArgument("permissions_database configuration is required", "permissions_database")
|
||||
}
|
||||
conn, err := NewConnection(logger, config.AsDatabaseConfig())
|
||||
if err != nil {
|
||||
return nil, merrors.Wrap(err, "permissions_database")
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
@@ -4,17 +4,9 @@ import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
mutil "git.sendico.io/tech/sxchnge/bff/pkg/mutil/db"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *AccountDB) GetAccountsByRefs(ctx context.Context, orgRef bson.ObjectID, refs []bson.ObjectID) ([]model.Account, error) {
|
||||
filter := repository.Query().Comparison(repository.IDField(), builder.In, refs)
|
||||
return mutil.GetObjects[model.Account](ctx, db.Logger, filter, nil, db.Repository)
|
||||
}
|
||||
|
||||
func (db *AccountDB) GetByEmail(ctx context.Context, email string) (*model.Account, error) {
|
||||
var account model.Account
|
||||
return &account, db.FindOne(ctx, repository.Filter("login", email), &account)
|
||||
|
||||
@@ -7,21 +7,12 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/auth"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/account"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/internal/mongo/accountdb"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/internal/mongo/invitationdb"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/internal/mongo/policiesdb"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/internal/mongo/rolesdb"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/internal/mongo/transactionimp"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/invitation"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/policy"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/role"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/transaction"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
mutil "git.sendico.io/tech/sxchnge/bff/pkg/mutil/config"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
@@ -31,29 +22,28 @@ import (
|
||||
|
||||
// Config represents configuration
|
||||
type Config struct {
|
||||
Port *string `mapstructure:"port"`
|
||||
PortEnv *string `mapstructure:"port_env"`
|
||||
User *string `mapstructure:"user"`
|
||||
UserEnv *string `mapstructure:"user_env"`
|
||||
PasswordEnv string `mapstructure:"password_env"`
|
||||
Database *string `mapstructure:"database"`
|
||||
DatabaseEnv *string `mapstructure:"database_env"`
|
||||
Host *string `mapstructure:"host"`
|
||||
HostEnv *string `mapstructure:"host_env"`
|
||||
Hosts []string `mapstructure:"hosts,omitempty"`
|
||||
HostsEnvPrefix *string `mapstructure:"hosts_env_prefix,omitempty"`
|
||||
HostsEnvPrefixEnv *string `mapstructure:"hosts_env_prefix_env,omitempty"`
|
||||
PortsEnvPrefix *string `mapstructure:"ports_env_prefix,omitempty"`
|
||||
PortsEnvPrefixEnv *string `mapstructure:"ports_env_prefix_env,omitempty"`
|
||||
URI *string `mapstructure:"uri,omitempty"`
|
||||
URIEnv *string `mapstructure:"uri_env,omitempty"`
|
||||
AuthSource *string `mapstructure:"auth_source,omitempty"`
|
||||
AuthSourceEnv *string `mapstructure:"auth_source_env,omitempty"`
|
||||
AuthMechanism *string `mapstructure:"auth_mechanism,omitempty"`
|
||||
AuthMechanismEnv *string `mapstructure:"auth_mechanism_env,omitempty"`
|
||||
ReplicaSet *string `mapstructure:"replica_set,omitempty"`
|
||||
ReplicaSetEnv *string `mapstructure:"replica_set_env,omitempty"`
|
||||
Enforcer *auth.Config `mapstructure:"enforcer"`
|
||||
Port *string `mapstructure:"port"`
|
||||
PortEnv *string `mapstructure:"port_env"`
|
||||
User *string `mapstructure:"user"`
|
||||
UserEnv *string `mapstructure:"user_env"`
|
||||
PasswordEnv string `mapstructure:"password_env"`
|
||||
Database *string `mapstructure:"database"`
|
||||
DatabaseEnv *string `mapstructure:"database_env"`
|
||||
Host *string `mapstructure:"host"`
|
||||
HostEnv *string `mapstructure:"host_env"`
|
||||
Hosts []string `mapstructure:"hosts,omitempty"`
|
||||
HostsEnvPrefix *string `mapstructure:"hosts_env_prefix,omitempty"`
|
||||
HostsEnvPrefixEnv *string `mapstructure:"hosts_env_prefix_env,omitempty"`
|
||||
PortsEnvPrefix *string `mapstructure:"ports_env_prefix,omitempty"`
|
||||
PortsEnvPrefixEnv *string `mapstructure:"ports_env_prefix_env,omitempty"`
|
||||
URI *string `mapstructure:"uri,omitempty"`
|
||||
URIEnv *string `mapstructure:"uri_env,omitempty"`
|
||||
AuthSource *string `mapstructure:"auth_source,omitempty"`
|
||||
AuthSourceEnv *string `mapstructure:"auth_source_env,omitempty"`
|
||||
AuthMechanism *string `mapstructure:"auth_mechanism,omitempty"`
|
||||
AuthMechanismEnv *string `mapstructure:"auth_mechanism_env,omitempty"`
|
||||
ReplicaSet *string `mapstructure:"replica_set,omitempty"`
|
||||
ReplicaSetEnv *string `mapstructure:"replica_set_env,omitempty"`
|
||||
}
|
||||
|
||||
type DBSettings struct {
|
||||
@@ -69,19 +59,6 @@ type DBSettings struct {
|
||||
URI string
|
||||
}
|
||||
|
||||
func newProtectedDB[T any](
|
||||
db *DB,
|
||||
create func(ctx context.Context, logger mlogger.Logger, enforcer auth.Enforcer, pdb policy.DB, client *mongo.Database) (T, error),
|
||||
) (T, error) {
|
||||
pdb, err := db.NewPoliciesDB()
|
||||
if err != nil {
|
||||
db.logger.Warn("Failed to create policies database", zap.Error(err))
|
||||
var zero T
|
||||
return zero, err
|
||||
}
|
||||
return create(context.Background(), db.logger, db.Enforcer(), pdb, db.db())
|
||||
}
|
||||
|
||||
func Config2DBSettings(logger mlogger.Logger, config *Config) *DBSettings {
|
||||
p := new(DBSettings)
|
||||
p.Port = mutil.GetConfigValue(logger, "port", "port_env", config.Port, config.PortEnv)
|
||||
@@ -160,12 +137,9 @@ func ConnectClient(logger mlogger.Logger, settings model.SettingsT) (*mongo.Clie
|
||||
|
||||
// DB represents the structure of the database
|
||||
type DB struct {
|
||||
logger mlogger.Logger
|
||||
config *DBSettings
|
||||
client *mongo.Client
|
||||
enforcer auth.Enforcer
|
||||
manager auth.Manager
|
||||
pdb policy.DB
|
||||
logger mlogger.Logger
|
||||
config *DBSettings
|
||||
client *mongo.Client
|
||||
}
|
||||
|
||||
func (db *DB) db() *mongo.Database {
|
||||
@@ -184,18 +158,6 @@ func (db *DB) NewAccountDBWithCollection(collection string) (account.DB, error)
|
||||
return accountdb.CreateWithCollection(db.logger, db.db(), collection)
|
||||
}
|
||||
|
||||
func (db *DB) NewInvitationsDB() (invitation.DB, error) {
|
||||
return newProtectedDB(db, invitationdb.Create)
|
||||
}
|
||||
|
||||
func (db *DB) NewPoliciesDB() (policy.DB, error) {
|
||||
return db.pdb, nil
|
||||
}
|
||||
|
||||
func (db *DB) NewRolesDB() (role.DB, error) {
|
||||
return rolesdb.Create(db.logger, db.db())
|
||||
}
|
||||
|
||||
func (db *DB) TransactionFactory() transaction.Factory {
|
||||
return transactionimp.CreateFactory(db.client)
|
||||
}
|
||||
@@ -264,23 +226,6 @@ func collectReplicaHosts(configuredHosts []string, replicaSet, defaultPort, host
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (db *DB) Permissions() auth.Provider {
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *DB) Manager() auth.Manager {
|
||||
return db.manager
|
||||
}
|
||||
|
||||
func (db *DB) Enforcer() auth.Enforcer {
|
||||
return db.enforcer
|
||||
}
|
||||
|
||||
func (db *DB) GetPolicyDescription(ctx context.Context, resource mservice.Type) (*model.PolicyDescription, error) {
|
||||
var policyDescription model.PolicyDescription
|
||||
return &policyDescription, db.pdb.FindOne(ctx, repository.Filter("resourceTypes", resource), &policyDescription)
|
||||
}
|
||||
|
||||
func (db *DB) CloseConnection() {
|
||||
if err := db.client.Disconnect(context.Background()); err != nil {
|
||||
db.logger.Warn("Failed to close connection", zap.Error(err))
|
||||
@@ -290,7 +235,7 @@ func (db *DB) CloseConnection() {
|
||||
|
||||
// NewConnection creates a new database connection
|
||||
func NewConnection(logger mlogger.Logger, settings model.SettingsT) (*DB, error) {
|
||||
client, config, dbSettings, err := ConnectClient(logger, settings)
|
||||
client, _, dbSettings, err := ConnectClient(logger, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -301,28 +246,5 @@ func NewConnection(logger mlogger.Logger, settings model.SettingsT) (*DB, error)
|
||||
client: client,
|
||||
}
|
||||
|
||||
cleanup := func(ctx context.Context) {
|
||||
if err := client.Disconnect(ctx); err != nil {
|
||||
logger.Warn("Failed to close MongoDB connection", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
rdb, err := db.NewRolesDB()
|
||||
if err != nil {
|
||||
db.logger.Warn("Failed to create roles database", zap.Error(err))
|
||||
cleanup(context.Background())
|
||||
return nil, err
|
||||
}
|
||||
if db.pdb, err = policiesdb.Create(db.logger, db.db()); err != nil {
|
||||
db.logger.Warn("Failed to create policies database", zap.Error(err))
|
||||
cleanup(context.Background())
|
||||
return nil, err
|
||||
}
|
||||
if db.enforcer, db.manager, err = auth.CreateAuth(logger, db.client, db.db(), db.pdb, rdb, config.Enforcer); err != nil {
|
||||
db.logger.Warn("Failed to create permissions enforcer", zap.Error(err))
|
||||
cleanup(context.Background())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *InvitationDB) Accept(ctx context.Context, invitationRef bson.ObjectID) error {
|
||||
return db.updateStatus(ctx, invitationRef, model.InvitationAccepted)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// SetArchived sets the archived status of an invitation
|
||||
// Invitation supports archiving through PermissionBound embedding ArchivableBase
|
||||
func (db *InvitationDB) SetArchived(ctx context.Context, accountRef, organizationRef, invitationRef bson.ObjectID, archived, cascade bool) error {
|
||||
db.DBImp.Logger.Debug("Setting invitation archived status", mzap.ObjRef("invitation_ref", invitationRef), zap.Bool("archived", archived), zap.Bool("cascade", cascade))
|
||||
res, err := db.Enforcer.Enforce(ctx, db.PermissionRef, accountRef, organizationRef, invitationRef, model.ActionUpdate)
|
||||
if err != nil {
|
||||
db.DBImp.Logger.Warn("Failed to enforce archivation permission", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return err
|
||||
}
|
||||
if !res {
|
||||
db.DBImp.Logger.Debug("Permission denied for archivation", mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return merrors.AccessDenied(db.Collection, string(model.ActionUpdate), invitationRef)
|
||||
}
|
||||
|
||||
// Get the invitation first
|
||||
var invitation model.Invitation
|
||||
if err := db.Get(ctx, accountRef, invitationRef, &invitation); err != nil {
|
||||
db.DBImp.Logger.Warn("Error retrieving invitation for archival", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the invitation's archived status
|
||||
invitation.SetArchived(archived)
|
||||
if err := db.Update(ctx, accountRef, &invitation); err != nil {
|
||||
db.DBImp.Logger.Warn("Error updating invitation archived status", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// Note: Currently no cascade dependencies for invitations
|
||||
// If cascade is enabled, we could add logic here for any future dependencies
|
||||
if cascade {
|
||||
db.DBImp.Logger.Debug("Cascade archiving requested but no dependencies to archive for invitation", mzap.ObjRef("invitation_ref", invitationRef))
|
||||
}
|
||||
|
||||
db.DBImp.Logger.Debug("Successfully set invitation archived status", mzap.ObjRef("invitation_ref", invitationRef), zap.Bool("archived", archived))
|
||||
return nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// DeleteCascade deletes an invitation
|
||||
// Invitations don't have cascade dependencies, so this is a simple deletion
|
||||
func (db *InvitationDB) DeleteCascade(ctx context.Context, accountRef, invitationRef bson.ObjectID) error {
|
||||
db.DBImp.Logger.Debug("Starting invitation cascade deletion", mzap.ObjRef("invitation_ref", invitationRef))
|
||||
|
||||
// Delete the invitation itself (no dependencies to cascade delete)
|
||||
if err := db.Delete(ctx, accountRef, invitationRef); err != nil {
|
||||
db.DBImp.Logger.Error("Error deleting invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return err
|
||||
}
|
||||
|
||||
db.DBImp.Logger.Debug("Successfully deleted invitation", mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return nil
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/auth"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/policy"
|
||||
ri "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/index"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type InvitationDB struct {
|
||||
auth.ProtectedDBImp[*model.Invitation]
|
||||
}
|
||||
|
||||
func Create(
|
||||
ctx context.Context,
|
||||
logger mlogger.Logger,
|
||||
enforcer auth.Enforcer,
|
||||
pdb policy.DB,
|
||||
db *mongo.Database,
|
||||
) (*InvitationDB, error) {
|
||||
p, err := auth.CreateDBImp[*model.Invitation](ctx, logger, pdb, enforcer, mservice.Invitations, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.DBImp.Repository.CreateIndex(pendingInvitationEmailIndexDefinition()); err != nil {
|
||||
p.DBImp.Logger.Error("Failed to create unique pending invitation email index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ttl index
|
||||
ttl := int32(0) // zero ttl means expiration on date preset when inserting data
|
||||
if err := p.DBImp.Repository.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
|
||||
TTL: &ttl,
|
||||
}); err != nil {
|
||||
p.DBImp.Logger.Error("Failed to create ttl index in the invitations", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &InvitationDB{ProtectedDBImp: *p}, nil
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func TestPendingInvitationEmailIndexBehavior(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForListeningPort("27017/tcp").WithStartupTimeout(2*time.Minute)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
termCtx, termCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer termCancel()
|
||||
_ = mongoContainer.Terminate(termCtx)
|
||||
}()
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := mongo.Connect(options.Client().ApplyURI(mongoURI))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = client.Disconnect(context.Background()) }()
|
||||
|
||||
database := client.Database("test_invitation_pending_email_index")
|
||||
defer func() { _ = database.Drop(context.Background()) }()
|
||||
|
||||
repo := repository.CreateMongoRepository(database, "invitations")
|
||||
require.NoError(t, repo.CreateIndex(pendingInvitationEmailIndexDefinition()))
|
||||
assertPendingEmailIndexDefinition(t, ctx, database.Collection("invitations"))
|
||||
|
||||
orgRef := bson.NewObjectID()
|
||||
email := "invitee@example.com"
|
||||
require.NoError(t, repo.Insert(ctx, testInvitation(orgRef, email, model.InvitationCreated), nil))
|
||||
err = repo.Insert(ctx, testInvitation(orgRef, email, model.InvitationSent), nil)
|
||||
require.ErrorIs(t, err, merrors.ErrDataConflict, "duplicate pending invitation must conflict")
|
||||
|
||||
for _, status := range []model.InvitationStatus{model.InvitationRevoked, model.InvitationDeclined, model.InvitationAccepted} {
|
||||
statusEmail := string(status) + "@example.com"
|
||||
require.NoError(t, repo.Insert(ctx, testInvitation(orgRef, statusEmail, status), nil))
|
||||
require.NoError(t, repo.Insert(ctx, testInvitation(orgRef, statusEmail, model.InvitationCreated), nil))
|
||||
}
|
||||
|
||||
otherOrgRef := bson.NewObjectID()
|
||||
require.NoError(t, repo.Insert(ctx, testInvitation(otherOrgRef, email, model.InvitationCreated), nil))
|
||||
}
|
||||
|
||||
func testInvitation(orgRef bson.ObjectID, email string, status model.InvitationStatus) *model.Invitation {
|
||||
inv := &model.Invitation{
|
||||
OrganizationRef: orgRef,
|
||||
Status: status,
|
||||
ExpiresAt: time.Now().UTC().Add(time.Hour),
|
||||
}
|
||||
inv.SetID(bson.NewObjectID())
|
||||
inv.SetOrganizationRef(orgRef)
|
||||
inv.Content.Email = email
|
||||
return inv
|
||||
}
|
||||
|
||||
func assertPendingEmailIndexDefinition(t *testing.T, ctx context.Context, collection *mongo.Collection) {
|
||||
t.Helper()
|
||||
cursor, err := collection.Indexes().List(ctx)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = cursor.Close(ctx) }()
|
||||
|
||||
for cursor.Next(ctx) {
|
||||
var index bson.M
|
||||
require.NoError(t, cursor.Decode(&index))
|
||||
if index["name"] != uniquePendingInvitationEmailIndexName {
|
||||
continue
|
||||
}
|
||||
|
||||
require.Equal(t, true, index["unique"])
|
||||
require.Equal(t, bson.D{
|
||||
{Key: "organizationRef", Value: int32(1)},
|
||||
{Key: "description.email", Value: int32(1)},
|
||||
}, index["key"])
|
||||
require.Equal(t, bson.D{
|
||||
{Key: "status", Value: bson.D{{Key: "$in", Value: bson.A{"created", "sent"}}}},
|
||||
{Key: "description.email", Value: bson.D{{Key: "$type", Value: "string"}}},
|
||||
}, index["partialFilterExpression"])
|
||||
return
|
||||
}
|
||||
require.NoError(t, cursor.Err())
|
||||
t.Fatalf("index %q was not created", uniquePendingInvitationEmailIndexName)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
|
||||
ri "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/index"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestPendingInvitationEmailIndexDefinition(t *testing.T) {
|
||||
def := pendingInvitationEmailIndexDefinition()
|
||||
|
||||
expectedKeys := []ri.Key{
|
||||
{Field: "organizationRef", Sort: ri.Asc},
|
||||
{Field: "description.email", Sort: ri.Asc},
|
||||
}
|
||||
if !reflect.DeepEqual(def.Keys, expectedKeys) {
|
||||
t.Fatalf("keys mismatch: got %#v want %#v", def.Keys, expectedKeys)
|
||||
}
|
||||
if !def.Unique {
|
||||
t.Fatal("index must be unique")
|
||||
}
|
||||
if def.Name != uniquePendingInvitationEmailIndexName {
|
||||
t.Fatalf("index name mismatch: got %q want %q", def.Name, uniquePendingInvitationEmailIndexName)
|
||||
}
|
||||
|
||||
expectedFilter := repository.Query().
|
||||
In(repository.Field("status"), "created", "sent").
|
||||
Comparison(repository.Field("description.email"), builder.Type, "string").
|
||||
BuildQuery()
|
||||
if !reflect.DeepEqual(def.PartialFilter.BuildQuery(), expectedFilter) {
|
||||
t.Fatalf("partial filter mismatch: got %#v want %#v", def.PartialFilter.BuildQuery(), expectedFilter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPendingInvitationEmailIndexUsesModelPendingStatuses(t *testing.T) {
|
||||
def := pendingInvitationEmailIndexDefinition()
|
||||
filter := def.PartialFilter.BuildQuery()
|
||||
statuses := statusInValues(t, filter)
|
||||
|
||||
expected := model.PendingInvitationStatusValues()
|
||||
if !reflect.DeepEqual(statuses, expected) {
|
||||
t.Fatalf("index statuses drifted from model: got %#v want %#v", statuses, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func statusInValues(t *testing.T, filter bson.D) []string {
|
||||
t.Helper()
|
||||
for _, item := range filter {
|
||||
if item.Key != "status" {
|
||||
continue
|
||||
}
|
||||
comparison, ok := item.Value.(bson.M)
|
||||
if !ok {
|
||||
t.Fatalf("status filter has unexpected type %T", item.Value)
|
||||
}
|
||||
rawValues, ok := comparison["$in"]
|
||||
if !ok {
|
||||
t.Fatal("status filter must use $in")
|
||||
}
|
||||
values, ok := rawValues.([]any)
|
||||
if !ok {
|
||||
t.Fatalf("status $in has unexpected type %T", rawValues)
|
||||
}
|
||||
statuses := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
status, ok := value.(string)
|
||||
if !ok {
|
||||
t.Fatalf("status value has unexpected type %T", value)
|
||||
}
|
||||
statuses = append(statuses, status)
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
t.Fatal("status filter not found")
|
||||
return nil
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *InvitationDB) Decline(ctx context.Context, invitationRef bson.ObjectID) error {
|
||||
return db.updateStatus(ctx, invitationRef, model.InvitationDeclined)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (db *InvitationDB) GetPublic(ctx context.Context, invitationRef bson.ObjectID) (*model.PublicInvitation, error) {
|
||||
roleField := repository.Field("role")
|
||||
orgField := repository.Field("organization")
|
||||
accField := repository.Field("account")
|
||||
empField := repository.Field("employee")
|
||||
regField := repository.Field("registrationAcc")
|
||||
descEmailField := repository.Field("description").Dot("email")
|
||||
pipeline := repository.Pipeline().
|
||||
// 0) Filter to exactly the invitation(s) you want
|
||||
Match(repository.IDFilter(invitationRef).And(repository.Filter("status", model.InvitationCreated))).
|
||||
// 1) Lookup the role document
|
||||
Lookup(
|
||||
mservice.Roles,
|
||||
repository.Field("roleRef"),
|
||||
repository.IDField(),
|
||||
roleField,
|
||||
).
|
||||
Unwind(repository.Ref(roleField)).
|
||||
// 2) Lookup the organization document
|
||||
Lookup(
|
||||
mservice.Organizations,
|
||||
repository.Field("organizationRef"),
|
||||
repository.IDField(),
|
||||
orgField,
|
||||
).
|
||||
Unwind(repository.Ref(orgField)).
|
||||
// 3) Lookup the account document
|
||||
Lookup(
|
||||
mservice.Accounts,
|
||||
repository.Field("inviterRef"),
|
||||
repository.IDField(),
|
||||
accField,
|
||||
).
|
||||
Unwind(repository.Ref(accField)).
|
||||
/* 4) do we already have an account whose login == invitation.description ? */
|
||||
Lookup(
|
||||
mservice.Accounts,
|
||||
descEmailField, // local field (invitation.description.email)
|
||||
repository.Field("login"), // foreign field (account.login)
|
||||
regField, // array: 0-length or ≥1
|
||||
).
|
||||
// 5) Projection
|
||||
Project(
|
||||
repository.SimpleAlias(
|
||||
empField.Dot("description"),
|
||||
repository.Ref(accField),
|
||||
),
|
||||
repository.SimpleAlias(
|
||||
empField.Dot("avatarUrl"),
|
||||
repository.Ref(accField.Dot("avatarUrl")),
|
||||
),
|
||||
repository.SimpleAlias(
|
||||
orgField.Dot("description"),
|
||||
repository.Ref(orgField),
|
||||
),
|
||||
repository.SimpleAlias(
|
||||
orgField.Dot("logoUrl"),
|
||||
repository.Ref(orgField.Dot("logoUrl")),
|
||||
),
|
||||
repository.SimpleAlias(
|
||||
roleField,
|
||||
repository.Ref(roleField),
|
||||
),
|
||||
repository.SimpleAlias(
|
||||
repository.Field("invitation"), // ← left-hand side
|
||||
repository.Ref(repository.Field("description")), // ← right-hand side (“$description”)
|
||||
),
|
||||
repository.SimpleAlias(
|
||||
repository.Field("storable"), // ← left-hand side
|
||||
repository.RootRef(), // ← right-hand side (“$description”)
|
||||
),
|
||||
repository.ProjectionExpr(
|
||||
repository.Field("registrationRequired"),
|
||||
repository.Eq(
|
||||
repository.Size(repository.Value(repository.Ref(regField).Build())),
|
||||
repository.Literal(0),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
var res model.PublicInvitation
|
||||
haveResult := false
|
||||
decoder := func(cur *mongo.Cursor) error {
|
||||
if haveResult {
|
||||
// should never get here
|
||||
db.DBImp.Logger.Warn("Unexpected extra invitation", mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return merrors.Internal("Unexpected extra invitation found by reference")
|
||||
}
|
||||
if e := cur.Decode(&res); e != nil {
|
||||
db.DBImp.Logger.Warn("Failed to decode entity", zap.Error(e), zap.Any("data", cur.Current.String()))
|
||||
return e
|
||||
}
|
||||
haveResult = true
|
||||
return nil
|
||||
}
|
||||
if err := db.DBImp.Repository.Aggregate(ctx, pipeline, decoder); err != nil {
|
||||
db.DBImp.Logger.Warn("Failed to execute aggregation pipeline", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return nil, err
|
||||
}
|
||||
if !haveResult {
|
||||
db.DBImp.Logger.Warn("No results fetched", mzap.ObjRef("invitation_ref", invitationRef))
|
||||
return nil, merrors.NoData(fmt.Sprintf("Invitation %s not found", invitationRef.Hex()))
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
|
||||
ri "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/index"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
)
|
||||
|
||||
const uniquePendingInvitationEmailIndexName = "unique_pending_invitation_email_per_org"
|
||||
|
||||
func pendingInvitationEmailIndexDefinition() *ri.Definition {
|
||||
statusValues := model.PendingInvitationStatusValues()
|
||||
statuses := make([]any, 0, len(statusValues))
|
||||
for _, status := range statusValues {
|
||||
statuses = append(statuses, status)
|
||||
}
|
||||
|
||||
return &ri.Definition{
|
||||
Keys: []ri.Key{
|
||||
{Field: repository.OrgField().Build(), Sort: ri.Asc},
|
||||
{Field: "description.email", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
Name: uniquePendingInvitationEmailIndexName,
|
||||
PartialFilter: repository.Query().
|
||||
In(repository.Field("status"), statuses...).
|
||||
Comparison(repository.Field("description.email"), builder.Type, "string"),
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
mauth "git.sendico.io/tech/sxchnge/bff/pkg/mutil/db/auth"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *InvitationDB) List(ctx context.Context, accountRef, organizationRef, _ bson.ObjectID, cursor *model.ViewCursor) ([]model.Invitation, error) {
|
||||
res, err := mauth.GetProtectedObjects[model.Invitation](
|
||||
ctx,
|
||||
db.DBImp.Logger,
|
||||
accountRef, organizationRef, model.ActionRead,
|
||||
repository.OrgFilter(organizationRef),
|
||||
cursor,
|
||||
db.Enforcer,
|
||||
db.DBImp.Repository,
|
||||
)
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return []model.Invitation{}, nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package invitationdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (db *InvitationDB) updateStatus(ctx context.Context, invitationRef bson.ObjectID, newStatus model.InvitationStatus) error {
|
||||
// db.DBImp.Up
|
||||
var inv model.Invitation
|
||||
if err := db.DBImp.FindOne(ctx, repository.IDFilter(invitationRef), &inv); err != nil {
|
||||
db.DBImp.Logger.Warn("Failed to fetch invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef), zap.String("new_status", string(newStatus)))
|
||||
return err
|
||||
}
|
||||
inv.Status = newStatus
|
||||
if err := db.DBImp.Update(ctx, &inv); err != nil {
|
||||
db.DBImp.Logger.Warn("Failed to update invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef), zap.String("new_status", string(newStatus)))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package policiesdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
mutil "git.sendico.io/tech/sxchnge/bff/pkg/mutil/db"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *PoliciesDB) All(ctx context.Context, organizationRef bson.ObjectID) ([]model.PolicyDescription, error) {
|
||||
// all documents
|
||||
filter := repository.Query().Or(
|
||||
repository.Filter(storable.OrganizationRefField, nil),
|
||||
repository.OrgFilter(organizationRef),
|
||||
)
|
||||
return mutil.GetObjects[model.PolicyDescription](ctx, db.Logger, filter, nil, db.Repository)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package policiesdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
)
|
||||
|
||||
func (db *PoliciesDB) GetBuiltInPolicy(ctx context.Context, resourceType mservice.Type, policy *model.PolicyDescription) error {
|
||||
return db.FindOne(ctx, repository.Filter("resourceTypes", resourceType), policy)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package policiesdb
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/template"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type PoliciesDB struct {
|
||||
template.DBImp[*model.PolicyDescription]
|
||||
}
|
||||
|
||||
func Create(logger mlogger.Logger, db *mongo.Database) (*PoliciesDB, error) {
|
||||
p := &PoliciesDB{
|
||||
DBImp: *template.Create[*model.PolicyDescription](logger, mservice.Policies, db),
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package policiesdb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
// Your internal packages
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/internal/mongo/policiesdb"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
|
||||
// Model package (contains PolicyDescription + Describable)
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
// Testcontainers
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Helper to terminate container
|
||||
func terminate(t *testing.T, ctx context.Context, container *mongodb.MongoDBContainer) {
|
||||
err := container.Terminate(ctx)
|
||||
require.NoError(t, err, "failed to terminate MongoDB container")
|
||||
}
|
||||
|
||||
// Helper to disconnect client
|
||||
func disconnect(t *testing.T, ctx context.Context, client *mongo.Client) {
|
||||
err := client.Disconnect(context.Background())
|
||||
require.NoError(t, err, "failed to disconnect from MongoDB")
|
||||
}
|
||||
|
||||
// Helper to drop the Policies collection
|
||||
func cleanupCollection(t *testing.T, ctx context.Context, db *mongo.Database) {
|
||||
// The actual collection name is typically the value returned by
|
||||
// (&model.PolicyDescription{}).Collection(), or something similar.
|
||||
// Make sure it matches what your code uses (often "policies" or "policyDescription").
|
||||
err := db.Collection((&model.PolicyDescription{}).Collection()).Drop(ctx)
|
||||
require.NoError(t, err, "failed to drop collection between sub-tests")
|
||||
}
|
||||
|
||||
func TestPoliciesDB(t *testing.T) {
|
||||
// Create context with reasonable timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Start MongoDB test container
|
||||
mongoC, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(t, ctx, mongoC)
|
||||
|
||||
// Get connection URI
|
||||
mongoURI, err := mongoC.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get connection string")
|
||||
|
||||
// Connect client
|
||||
clientOpts := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(ctx, clientOpts)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(t, ctx, client)
|
||||
|
||||
// Create test DB
|
||||
db := client.Database("testdb")
|
||||
|
||||
// Use a no-op logger (or real logger if you prefer)
|
||||
logger := zap.NewNop()
|
||||
|
||||
// Create an instance of PoliciesDB
|
||||
pdb, err := policiesdb.Create(logger, db)
|
||||
require.NoError(t, err, "unexpected error creating PoliciesDB")
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Each sub-test below starts by dropping the collection.
|
||||
// ---------------------------------------------------------
|
||||
|
||||
t.Run("CreateAndGet", func(t *testing.T) {
|
||||
cleanupCollection(t, ctx, db) // ensure no leftover data
|
||||
|
||||
desc := "Test policy description"
|
||||
policy := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "TestPolicy",
|
||||
Description: &desc,
|
||||
},
|
||||
}
|
||||
require.NoError(t, pdb.Create(ctx, policy))
|
||||
|
||||
result := &model.PolicyDescription{}
|
||||
err := pdb.Get(ctx, policy.ID, result)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, policy.ID, result.ID)
|
||||
assert.Equal(t, "TestPolicy", result.Name)
|
||||
assert.NotNil(t, result.Description)
|
||||
assert.Equal(t, "Test policy description", *result.Description)
|
||||
})
|
||||
|
||||
t.Run("Get_NotFound", func(t *testing.T) {
|
||||
cleanupCollection(t, ctx, db)
|
||||
|
||||
// Attempt to get a non-existent ID
|
||||
nonExistentID := bson.NewObjectID()
|
||||
result := &model.PolicyDescription{}
|
||||
err := pdb.Get(ctx, nonExistentID, result)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, merrors.ErrNoData))
|
||||
})
|
||||
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
cleanupCollection(t, ctx, db)
|
||||
|
||||
originalDesc := "Original description"
|
||||
policy := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "OriginalName",
|
||||
Description: &originalDesc,
|
||||
},
|
||||
}
|
||||
require.NoError(t, pdb.Create(ctx, policy))
|
||||
|
||||
newDesc := "Updated description"
|
||||
policy.Name = "UpdatedName"
|
||||
policy.Description = &newDesc
|
||||
|
||||
err := pdb.Update(ctx, policy)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated := &model.PolicyDescription{}
|
||||
err = pdb.Get(ctx, policy.ID, updated)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "UpdatedName", updated.Name)
|
||||
assert.NotNil(t, updated.Description)
|
||||
assert.Equal(t, "Updated description", *updated.Description)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
cleanupCollection(t, ctx, db)
|
||||
|
||||
desc := "To be deleted"
|
||||
policy := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "WillDelete",
|
||||
Description: &desc,
|
||||
},
|
||||
}
|
||||
require.NoError(t, pdb.Create(ctx, policy))
|
||||
|
||||
err := pdb.Delete(ctx, policy.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
deleted := &model.PolicyDescription{}
|
||||
err = pdb.Get(ctx, policy.ID, deleted)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, merrors.ErrNoData))
|
||||
})
|
||||
|
||||
t.Run("DeleteMany", func(t *testing.T) {
|
||||
cleanupCollection(t, ctx, db)
|
||||
|
||||
desc1 := "Will be deleted 1"
|
||||
desc2 := "Will be deleted 2"
|
||||
pol1 := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "BatchDelete1",
|
||||
Description: &desc1,
|
||||
},
|
||||
}
|
||||
pol2 := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "BatchDelete2",
|
||||
Description: &desc2,
|
||||
},
|
||||
}
|
||||
require.NoError(t, pdb.Create(ctx, pol1))
|
||||
require.NoError(t, pdb.Create(ctx, pol2))
|
||||
|
||||
q := repository.Query().RegEx(repository.Field("description"), "^Will be deleted", "")
|
||||
err := pdb.DeleteMany(ctx, q)
|
||||
require.NoError(t, err)
|
||||
|
||||
res1 := &model.PolicyDescription{}
|
||||
err1 := pdb.Get(ctx, pol1.ID, res1)
|
||||
assert.Error(t, err1)
|
||||
assert.True(t, errors.Is(err1, merrors.ErrNoData))
|
||||
|
||||
res2 := &model.PolicyDescription{}
|
||||
err2 := pdb.Get(ctx, pol2.ID, res2)
|
||||
assert.Error(t, err2)
|
||||
assert.True(t, errors.Is(err2, merrors.ErrNoData))
|
||||
})
|
||||
|
||||
t.Run("FindOne", func(t *testing.T) {
|
||||
cleanupCollection(t, ctx, db)
|
||||
|
||||
desc := "Unique find test"
|
||||
policy := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "FindOneTest",
|
||||
Description: &desc,
|
||||
},
|
||||
}
|
||||
require.NoError(t, pdb.Create(ctx, policy))
|
||||
|
||||
// Match by name == "FindOneTest"
|
||||
q := repository.Query().Comparison(repository.Field("name"), builder.Eq, "FindOneTest")
|
||||
|
||||
found := &model.PolicyDescription{}
|
||||
err := pdb.FindOne(ctx, q, found)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, policy.ID, found.ID)
|
||||
assert.Equal(t, "FindOneTest", found.Name)
|
||||
assert.NotNil(t, found.Description)
|
||||
assert.Equal(t, "Unique find test", *found.Description)
|
||||
})
|
||||
|
||||
t.Run("All", func(t *testing.T) {
|
||||
cleanupCollection(t, ctx, db)
|
||||
|
||||
// Insert some policies (orgA, orgB, nil org)
|
||||
orgA := bson.NewObjectID()
|
||||
orgB := bson.NewObjectID()
|
||||
|
||||
descA := "Org A policy"
|
||||
policyA := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "PolicyA",
|
||||
Description: &descA,
|
||||
},
|
||||
OrganizationRef: &orgA, // belongs to orgA
|
||||
}
|
||||
descB := "Org B policy"
|
||||
policyB := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "PolicyB",
|
||||
Description: &descB,
|
||||
},
|
||||
OrganizationRef: &orgB, // belongs to orgB
|
||||
}
|
||||
descNil := "No org policy"
|
||||
policyNil := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "PolicyNil",
|
||||
Description: &descNil,
|
||||
},
|
||||
// nil => built-in
|
||||
}
|
||||
require.NoError(t, pdb.Create(ctx, policyA))
|
||||
require.NoError(t, pdb.Create(ctx, policyB))
|
||||
require.NoError(t, pdb.Create(ctx, policyNil))
|
||||
|
||||
// Suppose the requirement is: "All" returns
|
||||
// - policies for the requested org
|
||||
// - plus built-in (nil) ones
|
||||
resultsA, err := pdb.All(ctx, orgA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resultsA, 2) // orgA + built-in
|
||||
|
||||
var idsA []bson.ObjectID
|
||||
for _, r := range resultsA {
|
||||
idsA = append(idsA, r.ID)
|
||||
}
|
||||
assert.Contains(t, idsA, policyA.ID)
|
||||
assert.Contains(t, idsA, policyNil.ID)
|
||||
assert.NotContains(t, idsA, policyB.ID)
|
||||
|
||||
resultsB, err := pdb.All(ctx, orgB)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resultsB, 2) // orgB + built-in
|
||||
|
||||
var idsB []bson.ObjectID
|
||||
for _, r := range resultsB {
|
||||
idsB = append(idsB, r.ID)
|
||||
}
|
||||
assert.Contains(t, idsB, policyB.ID)
|
||||
assert.Contains(t, idsB, policyNil.ID)
|
||||
assert.NotContains(t, idsB, policyA.ID)
|
||||
})
|
||||
|
||||
t.Run("Policies", func(t *testing.T) {
|
||||
cleanupCollection(t, ctx, db)
|
||||
|
||||
desc1 := "PolicyOne"
|
||||
pol1 := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "PolicyOne",
|
||||
Description: &desc1,
|
||||
},
|
||||
}
|
||||
desc2 := "PolicyTwo"
|
||||
pol2 := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "PolicyTwo",
|
||||
Description: &desc2,
|
||||
},
|
||||
}
|
||||
desc3 := "PolicyThree"
|
||||
pol3 := &model.PolicyDescription{
|
||||
Describable: model.Describable{
|
||||
Name: "PolicyThree",
|
||||
Description: &desc3,
|
||||
},
|
||||
}
|
||||
require.NoError(t, pdb.Create(ctx, pol1))
|
||||
require.NoError(t, pdb.Create(ctx, pol2))
|
||||
require.NoError(t, pdb.Create(ctx, pol3))
|
||||
|
||||
// 1) Request pol1, pol2
|
||||
results12, err := pdb.Policies(ctx, []bson.ObjectID{pol1.ID, pol2.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results12, 2)
|
||||
// IDs might be out of order, so we do a set-like check
|
||||
var set12 []bson.ObjectID
|
||||
for _, r := range results12 {
|
||||
set12 = append(set12, r.ID)
|
||||
}
|
||||
assert.Contains(t, set12, pol1.ID)
|
||||
assert.Contains(t, set12, pol2.ID)
|
||||
|
||||
// 2) Request pol1, pol3, plus a random ID
|
||||
fakeID := bson.NewObjectID()
|
||||
results13Fake, err := pdb.Policies(ctx, []bson.ObjectID{pol1.ID, pol3.ID, fakeID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results13Fake, 2) // pol1 + pol3 only
|
||||
var set13Fake []bson.ObjectID
|
||||
for _, r := range results13Fake {
|
||||
set13Fake = append(set13Fake, r.ID)
|
||||
}
|
||||
assert.Contains(t, set13Fake, pol1.ID)
|
||||
assert.Contains(t, set13Fake, pol3.ID)
|
||||
|
||||
// 3) Request with empty slice => expect no results
|
||||
resultsEmpty, err := pdb.Policies(ctx, []bson.ObjectID{})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resultsEmpty, 0)
|
||||
})
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package policiesdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
mutil "git.sendico.io/tech/sxchnge/bff/pkg/mutil/db"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *PoliciesDB) Policies(ctx context.Context, refs []bson.ObjectID) ([]model.PolicyDescription, error) {
|
||||
if len(refs) == 0 {
|
||||
return []model.PolicyDescription{}, nil
|
||||
}
|
||||
filter := repository.Query().In(repository.IDField(), refs)
|
||||
return mutil.GetObjects[model.PolicyDescription](ctx, db.Logger, filter, nil, db.Repository)
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/transaction"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
@@ -227,57 +226,6 @@ func (r *MongoRepository) ListIDs(ctx context.Context, query builder.Query) ([]b
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (r *MongoRepository) ListPermissionBound(ctx context.Context, query builder.Query) ([]model.PermissionBoundStorable, error) {
|
||||
return listProjectedStorables(ctx, r.collection, query, bson.M{
|
||||
storable.IDField: 1,
|
||||
storable.PermissionRefField: 1,
|
||||
storable.OrganizationRefField: 1,
|
||||
}, func(doc *model.PermissionBound) model.PermissionBoundStorable {
|
||||
return doc
|
||||
})
|
||||
}
|
||||
|
||||
func (r *MongoRepository) ListAccountBound(ctx context.Context, query builder.Query) ([]model.AccountBoundStorable, error) {
|
||||
return listProjectedStorables(ctx, r.collection, query, bson.M{
|
||||
storable.IDField: 1,
|
||||
model.AccountRefField: 1,
|
||||
model.OrganizationRefField: 1,
|
||||
}, func(doc *model.AccountBoundBase) model.AccountBoundStorable {
|
||||
return doc
|
||||
})
|
||||
}
|
||||
|
||||
func listProjectedStorables[T any, R any](
|
||||
ctx context.Context,
|
||||
collection *mongo.Collection,
|
||||
query builder.Query,
|
||||
projection bson.M,
|
||||
wrap func(*T) R,
|
||||
) ([]R, error) {
|
||||
filter := query.BuildQuery()
|
||||
cursor, err := collection.Find(ctx, filter, options.Find().SetProjection(projection))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = cursor.Close(ctx)
|
||||
}()
|
||||
|
||||
result := make([]R, 0)
|
||||
for cursor.Next(ctx) {
|
||||
doc := new(T)
|
||||
if err := cursor.Decode(doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, wrap(doc))
|
||||
}
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Delete(ctx context.Context, id bson.ObjectID) error {
|
||||
_, err := r.collection.DeleteOne(ctx, idFilter(id))
|
||||
return err
|
||||
|
||||
@@ -446,72 +446,6 @@ func TestMongoRepository_Name(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_ListPermissionBound(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("ListPermissionBound_WithData", func(t *testing.T) {
|
||||
// Insert test objects with permission bound data
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
// Insert documents directly with permission bound fields
|
||||
_, err := db.Collection("testcollection").InsertMany(ctx, []interface{}{
|
||||
bson.M{
|
||||
"_id": bson.NewObjectID(),
|
||||
"organizationRef": orgID,
|
||||
"permissionRef": bson.NewObjectID(),
|
||||
},
|
||||
bson.M{
|
||||
"_id": bson.NewObjectID(),
|
||||
"organizationRef": orgID,
|
||||
"permissionRef": bson.NewObjectID(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Query for permission bound objects
|
||||
query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("organizationRef"), builder.Eq, orgID)
|
||||
|
||||
results, err := repository.ListPermissionBound(ctx, query)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
|
||||
for _, result := range results {
|
||||
assert.Equal(t, orgID, result.GetOrganizationRef())
|
||||
assert.NotNil(t, result.GetPermissionRef())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListPermissionBound_EmptyResult", func(t *testing.T) {
|
||||
nonExistentOrgID := bson.NewObjectID()
|
||||
query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("organizationRef"), builder.Eq, nonExistentOrgID)
|
||||
|
||||
results, err := repository.ListPermissionBound(ctx, query)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_UpdateTimestamp(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package rolesdb
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/template"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type RolesDB struct {
|
||||
template.DBImp[*model.RoleDescription]
|
||||
}
|
||||
|
||||
func Create(logger mlogger.Logger, db *mongo.Database) (*RolesDB, error) {
|
||||
p := &RolesDB{
|
||||
DBImp: *template.Create[*model.RoleDescription](logger, mservice.Roles, db),
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package rolesdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
mutil "git.sendico.io/tech/sxchnge/bff/pkg/mutil/db"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *RolesDB) List(ctx context.Context, organizationRef bson.ObjectID, cursor *model.ViewCursor) ([]model.RoleDescription, error) {
|
||||
filter := repository.OrgFilter(organizationRef)
|
||||
return mutil.GetObjects[model.RoleDescription](ctx, db.Logger, filter, cursor, db.Repository)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package rolesdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
mutil "git.sendico.io/tech/sxchnge/bff/pkg/mutil/db"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *RolesDB) Roles(ctx context.Context, refs []bson.ObjectID) ([]model.RoleDescription, error) {
|
||||
filter := repository.Query().In(repository.IDField(), refs)
|
||||
return mutil.GetObjects[model.RoleDescription](ctx, db.Logger, filter, nil, db.Repository)
|
||||
}
|
||||
@@ -12,6 +12,10 @@ type MongoTransaction struct {
|
||||
}
|
||||
|
||||
func (mt *MongoTransaction) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||
// Reuse an outer customer workflow transaction when a token store joins it.
|
||||
if transaction.ActiveMongoTransaction(ctx) {
|
||||
return cb(ctx)
|
||||
}
|
||||
session, err := mt.client.StartSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package invitation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/auth"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type DB interface {
|
||||
auth.ProtectedDB[*model.Invitation]
|
||||
GetPublic(ctx context.Context, invitationRef bson.ObjectID) (*model.PublicInvitation, error)
|
||||
Accept(ctx context.Context, invitationRef bson.ObjectID) error
|
||||
Decline(ctx context.Context, invitationRef bson.ObjectID) error
|
||||
List(ctx context.Context, accountRef, organizationRef, _ bson.ObjectID, cursor *model.ViewCursor) ([]model.Invitation, error)
|
||||
DeleteCascade(ctx context.Context, accountRef, statusRef bson.ObjectID) error
|
||||
SetArchived(ctx context.Context, accountRef, organizationRef, invitationRef bson.ObjectID, archived, cascade bool) error
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestPermissionsDatabaseConfigAsDatabaseConfig(t *testing.T) {
|
||||
cfg := &PermissionsDatabaseConfig{
|
||||
Driver: Mongo,
|
||||
Settings: model.SettingsT{
|
||||
"database_env": "PERMISSIONS_MONGO_DATABASE",
|
||||
},
|
||||
}
|
||||
|
||||
converted := cfg.AsDatabaseConfig()
|
||||
if converted == nil {
|
||||
t.Fatal("expected converted config")
|
||||
}
|
||||
if converted.Driver != Mongo {
|
||||
t.Fatalf("expected driver %q, got %q", Mongo, converted.Driver)
|
||||
}
|
||||
if got := converted.Settings["database_env"]; got != "PERMISSIONS_MONGO_DATABASE" {
|
||||
t.Fatalf("expected database_env to be preserved, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCentralPermissionsConnectionRequiresConfig(t *testing.T) {
|
||||
_, err := NewCentralPermissionsConnection(zap.NewNop(), nil)
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConnectionRequiresConfig(t *testing.T) {
|
||||
_, err := NewConnection(zap.NewNop(), nil)
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCentralPermissionsConnectionDelegatesToDBConnection(t *testing.T) {
|
||||
_, err := NewCentralPermissionsConnection(zap.NewNop(), &PermissionsDatabaseConfig{
|
||||
Driver: DBDriver("bogus"),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "permissions_database") {
|
||||
t.Fatalf("expected permissions_database context, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown database driver") {
|
||||
t.Fatalf("expected delegated database driver error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSplitDomainPermissionsDatabase(t *testing.T) {
|
||||
t.Setenv("DOMAIN_DB", "domain")
|
||||
t.Setenv("PERMISSIONS_DB", "sendico")
|
||||
|
||||
domain, permissions, err := ValidateSplitDomainPermissionsDatabase(
|
||||
"test service",
|
||||
&Config{
|
||||
Driver: Mongo,
|
||||
Settings: model.SettingsT{
|
||||
"database_env": "DOMAIN_DB",
|
||||
},
|
||||
},
|
||||
&PermissionsDatabaseConfig{
|
||||
Driver: Mongo,
|
||||
Settings: model.SettingsT{
|
||||
"database_env": "PERMISSIONS_DB",
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected valid split-domain config, got %v", err)
|
||||
}
|
||||
if domain.Name != "domain" || permissions.Name != "sendico" {
|
||||
t.Fatalf("unexpected resolved names: domain=%+v permissions=%+v", domain, permissions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSplitDomainPermissionsDatabaseRejectsMissingPermissionsConfig(t *testing.T) {
|
||||
t.Setenv("DOMAIN_DB", "domain")
|
||||
|
||||
_, _, err := ValidateSplitDomainPermissionsDatabase(
|
||||
"test service",
|
||||
&Config{
|
||||
Driver: Mongo,
|
||||
Settings: model.SettingsT{
|
||||
"database_env": "DOMAIN_DB",
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSplitDomainPermissionsDatabaseRejectsSameResolvedDatabase(t *testing.T) {
|
||||
t.Setenv("DOMAIN_DB", "domain")
|
||||
t.Setenv("PERMISSIONS_DB", "domain")
|
||||
|
||||
_, _, err := ValidateSplitDomainPermissionsDatabase(
|
||||
"test service",
|
||||
&Config{
|
||||
Driver: Mongo,
|
||||
Settings: model.SettingsT{
|
||||
"database_env": "DOMAIN_DB",
|
||||
},
|
||||
},
|
||||
&PermissionsDatabaseConfig{
|
||||
Driver: Mongo,
|
||||
Settings: model.SettingsT{
|
||||
"database_env": "PERMISSIONS_DB",
|
||||
},
|
||||
},
|
||||
)
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/template"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type DB interface {
|
||||
template.DB[*model.PolicyDescription]
|
||||
All(ctx context.Context, organizationRef bson.ObjectID) ([]model.PolicyDescription, error)
|
||||
Policies(ctx context.Context, refs []bson.ObjectID) ([]model.PolicyDescription, error)
|
||||
GetBuiltInPolicy(ctx context.Context, resourceType mservice.Type, policy *model.PolicyDescription) error
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// AccountBoundFilter provides factory methods for creating account-bound filters
|
||||
type AccountBoundFilter struct{}
|
||||
|
||||
// NewAccountBoundFilter creates a new AccountBoundFilter instance
|
||||
func NewAccountBoundFilter() *AccountBoundFilter {
|
||||
return &AccountBoundFilter{}
|
||||
}
|
||||
|
||||
// WithoutOrg creates a filter for account-bound objects without organization filter
|
||||
// This filter finds objects where:
|
||||
// - accountRef matches the provided accountRef, OR
|
||||
// - accountRef is nil/null, OR
|
||||
// - accountRef field doesn't exist
|
||||
func (f *AccountBoundFilter) WithoutOrg(accountRef bson.ObjectID) builder.Query {
|
||||
return Query().Or(
|
||||
AccountFilter(accountRef),
|
||||
Filter(model.AccountRefField, nil),
|
||||
Exists(AccountField(), false),
|
||||
)
|
||||
}
|
||||
|
||||
// WithOrg creates a filter for account-bound objects with organization filter
|
||||
// This filter finds objects where:
|
||||
// - accountRef matches the provided accountRef, OR
|
||||
// - accountRef is nil/null, OR
|
||||
// - accountRef field doesn't exist
|
||||
// AND combines with organization filter
|
||||
func (f *AccountBoundFilter) WithOrg(accountRef, organizationRef bson.ObjectID) builder.Query {
|
||||
return Query().And(
|
||||
OrgFilter(organizationRef),
|
||||
f.WithoutOrg(accountRef),
|
||||
)
|
||||
}
|
||||
|
||||
// WithQuery creates a filter for account-bound objects with additional query and organization filter
|
||||
func (f *AccountBoundFilter) WithQuery(accountRef, organizationRef bson.ObjectID, additionalQuery builder.Query) builder.Query {
|
||||
accountQuery := f.WithOrg(accountRef, organizationRef)
|
||||
return additionalQuery.And(accountQuery)
|
||||
}
|
||||
|
||||
// WithQueryNoOrg creates a filter for account-bound objects with additional query but no org filter
|
||||
func (f *AccountBoundFilter) WithQueryNoOrg(accountRef bson.ObjectID, additionalQuery builder.Query) builder.Query {
|
||||
accountQuery := f.WithoutOrg(accountRef)
|
||||
return additionalQuery.And(accountQuery)
|
||||
}
|
||||
|
||||
// Global instance for convenience
|
||||
var DefaultAccountBoundFilter = NewAccountBoundFilter()
|
||||
|
||||
// Convenience functions that use the global factory instance
|
||||
|
||||
// WithOrg is a convenience function that uses the default factory
|
||||
func WithOrg(accountRef, organizationRef bson.ObjectID) builder.Query {
|
||||
return DefaultAccountBoundFilter.WithOrg(accountRef, organizationRef)
|
||||
}
|
||||
|
||||
// WithoutOrg is a convenience function that uses the default factory
|
||||
func WithoutOrg(accountRef bson.ObjectID) builder.Query {
|
||||
return DefaultAccountBoundFilter.WithoutOrg(accountRef)
|
||||
}
|
||||
|
||||
// WithQuery is a convenience function that uses the default factory
|
||||
func WithQuery(accountRef, organizationRef bson.ObjectID, additionalQuery builder.Query) builder.Query {
|
||||
return DefaultAccountBoundFilter.WithQuery(accountRef, organizationRef, additionalQuery)
|
||||
}
|
||||
|
||||
// WithQueryNoOrg is a convenience function that uses the default factory
|
||||
func WithQueryNoOrg(accountRef bson.ObjectID, additionalQuery builder.Query) builder.Query {
|
||||
return DefaultAccountBoundFilter.WithQueryNoOrg(accountRef, additionalQuery)
|
||||
}
|
||||
@@ -68,22 +68,6 @@ func IsArchivedFilter(isArchived bool) builder.Query {
|
||||
return Query().Filter(IsArchivedField(), isArchived)
|
||||
}
|
||||
|
||||
func OrgField() builder.Field {
|
||||
return Field(storable.OrganizationRefField)
|
||||
}
|
||||
|
||||
func OrgFilter(ref bson.ObjectID) builder.Query {
|
||||
return Query().Filter(OrgField(), ref)
|
||||
}
|
||||
|
||||
func ProjectField() builder.Field {
|
||||
return Field("projectRef")
|
||||
}
|
||||
|
||||
func ProjectFilter(ref bson.ObjectID) builder.Query {
|
||||
return Query().Filter(ProjectField(), ref)
|
||||
}
|
||||
|
||||
func AccountField() builder.Field {
|
||||
return Field(model.AccountRefField)
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestAccountBoundFilter_WithOrg(t *testing.T) {
|
||||
factory := NewAccountBoundFilter()
|
||||
accountRef := bson.NewObjectID()
|
||||
orgRef := bson.NewObjectID()
|
||||
|
||||
query := factory.WithOrg(accountRef, orgRef)
|
||||
|
||||
// Test that the query is not nil
|
||||
assert.NotNil(t, query)
|
||||
}
|
||||
|
||||
func TestAccountBoundFilter_WithoutOrg(t *testing.T) {
|
||||
factory := NewAccountBoundFilter()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
query := factory.WithoutOrg(accountRef)
|
||||
|
||||
// Test that the query is not nil
|
||||
assert.NotNil(t, query)
|
||||
}
|
||||
|
||||
func TestAccountBoundFilter_WithQuery(t *testing.T) {
|
||||
factory := NewAccountBoundFilter()
|
||||
accountRef := bson.NewObjectID()
|
||||
orgRef := bson.NewObjectID()
|
||||
additionalQuery := Query().Filter(Field("status"), "active")
|
||||
|
||||
query := factory.WithQuery(accountRef, orgRef, additionalQuery)
|
||||
|
||||
// Test that the query is not nil
|
||||
assert.NotNil(t, query)
|
||||
}
|
||||
|
||||
func TestAccountBoundFilter_WithQueryNoOrg(t *testing.T) {
|
||||
factory := NewAccountBoundFilter()
|
||||
accountRef := bson.NewObjectID()
|
||||
additionalQuery := Query().Filter(Field("status"), "active")
|
||||
|
||||
query := factory.WithQueryNoOrg(accountRef, additionalQuery)
|
||||
|
||||
// Test that the query is not nil
|
||||
assert.NotNil(t, query)
|
||||
}
|
||||
|
||||
func TestDefaultAccountBoundFilter(t *testing.T) {
|
||||
// Test that the default factory is not nil
|
||||
assert.NotNil(t, DefaultAccountBoundFilter)
|
||||
|
||||
// Test that it's the correct type
|
||||
assert.IsType(t, &AccountBoundFilter{}, DefaultAccountBoundFilter)
|
||||
}
|
||||
|
||||
func TestConvenienceFunctions(t *testing.T) {
|
||||
accountRef := bson.NewObjectID()
|
||||
orgRef := bson.NewObjectID()
|
||||
additionalQuery := Query().Filter(Field("status"), "active")
|
||||
|
||||
// Test convenience functions
|
||||
query1 := WithOrg(accountRef, orgRef)
|
||||
assert.NotNil(t, query1)
|
||||
|
||||
query2 := WithoutOrg(accountRef)
|
||||
assert.NotNil(t, query2)
|
||||
|
||||
query3 := WithQuery(accountRef, orgRef, additionalQuery)
|
||||
assert.NotNil(t, query3)
|
||||
|
||||
query4 := WithQueryNoOrg(accountRef, additionalQuery)
|
||||
assert.NotNil(t, query4)
|
||||
}
|
||||
|
||||
func TestFilterFactoryConsistency(t *testing.T) {
|
||||
factory := NewAccountBoundFilter()
|
||||
accountRef := bson.NewObjectID()
|
||||
orgRef := bson.NewObjectID()
|
||||
|
||||
// Test that factory methods and convenience functions produce the same result
|
||||
query1 := factory.WithOrg(accountRef, orgRef)
|
||||
query2 := WithOrg(accountRef, orgRef)
|
||||
|
||||
// Both should be valid queries
|
||||
assert.NotNil(t, query1)
|
||||
assert.NotNil(t, query2)
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
ri "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/index"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/transaction"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
@@ -50,8 +49,6 @@ type Repository interface {
|
||||
DropIndex(ctx context.Context, name string) error
|
||||
ListIndexDefinitions(ctx context.Context) ([]*ri.Definition, error)
|
||||
ListIDs(ctx context.Context, query builder.Query) ([]bson.ObjectID, error)
|
||||
ListPermissionBound(ctx context.Context, query builder.Query) ([]model.PermissionBoundStorable, error)
|
||||
ListAccountBound(ctx context.Context, query builder.Query) ([]model.AccountBoundStorable, error)
|
||||
WithTransaction(ctx context.Context, cb transaction.Callback) (any, error)
|
||||
Collection() string
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/template"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type DB interface {
|
||||
template.DB[*model.RoleDescription]
|
||||
Roles(ctx context.Context, refs []bson.ObjectID) ([]model.RoleDescription, error)
|
||||
List(ctx context.Context, organizationRef bson.ObjectID, cursor *model.ViewCursor) ([]model.RoleDescription, error)
|
||||
}
|
||||
@@ -7,11 +7,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
IDField = "_id"
|
||||
PermissionRefField = "permissionRef"
|
||||
OrganizationRefField = "organizationRef"
|
||||
IsArchivedField = "isArchived"
|
||||
UpdatedAtField = "updatedAt"
|
||||
IDField = "_id"
|
||||
IsArchivedField = "isArchived"
|
||||
UpdatedAtField = "updatedAt"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -72,10 +71,6 @@ func (db *DBImp[T]) ListIDs(ctx context.Context, query builder.Query) ([]bson.Ob
|
||||
return db.Repository.ListIDs(ctx, query)
|
||||
}
|
||||
|
||||
func (db *DBImp[T]) ListPermissionBound(ctx context.Context, query builder.Query) ([]model.PermissionBoundStorable, error) {
|
||||
return db.Repository.ListPermissionBound(ctx, query)
|
||||
}
|
||||
|
||||
func (db *DBImp[T]) SetDeleter(deleter CascadeDeleterT) {
|
||||
db.Logger.Debug("Custom cascade deletion method installed")
|
||||
db.cdeleter = deleter
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
tspoint "git.sendico.io/tech/sxchnge/bff/pkg/db/tseries/point"
|
||||
)
|
||||
|
||||
type TimeSeries[T tspoint.TimePoint] interface {
|
||||
// Insert adds a single point into the series.
|
||||
Insert(ctx context.Context, point T) error
|
||||
// InsertMany adds multiple points in one bulk operation.
|
||||
InsertMany(ctx context.Context, points []T) error
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package tspoint
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimePoint interface {
|
||||
GetTimeStamp() time.Time
|
||||
}
|
||||
|
||||
type TimePointWithMeta[T any] interface {
|
||||
GetTimeStamp() time.Time
|
||||
GetMeta() T
|
||||
}
|
||||
Reference in new issue
Block a user