mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-10 00:04:27 +00:00
feat(ban): runtime enforcement for ban-scope bitmask
The bitmask landed in 000045 with schema + UI; this commit wires the
three gates the bits describe.
- BanScopeLogin → authService.LoginConfirm checks the scope after
password verification and refuses the session
with "this account has been suspended"
- BanScopeOrgCreate → organizationService.Create checks the scope
before any other validation and refuses with
"this account cannot create new workspaces"
- BanScopeSend → emailSendService.SendEmail checks the scope
before validating the email account and refuses
with "this account cannot send email"
Adds UserRepository.GetBanState(ctx, userID) → uint32 — a single-column
read so the hot paths don't have to fetch the full user row just to
check a flag. Returns 0 when no ban (the column defaults to 0); the
caller treats 0 as "allow."
Threads userRepo into emailSendService — the only constructor change
in this commit. cmd/backend/main.go updated accordingly.
This commit is contained in:
+1
-1
@@ -636,7 +636,7 @@ func main() {
|
||||
// Template & email send services
|
||||
templateService = template.NewService(templateRepository)
|
||||
schedulerService := scheduler.NewSchedulerService(taskRepository, warmupRepository, campaignProgressRepository, emailRepostory, campaignRepostory)
|
||||
emailSendService = emailsend.NewService(taskRepository, emailRepostory, schedulerService, tasksClient, featureGateService)
|
||||
emailSendService = emailsend.NewService(taskRepository, emailRepostory, userRepostory, schedulerService, tasksClient, featureGateService)
|
||||
advancedService = advanced.NewService(
|
||||
advancedRepository,
|
||||
campaignRepostory,
|
||||
|
||||
@@ -114,6 +114,16 @@ func (s *authService) LoginConfirm(ctx context.Context, data *ConfirmData, sessi
|
||||
return nil, errx.ErrCode
|
||||
}
|
||||
|
||||
// Ban-scope enforcement (migration 000045). The runtime treats
|
||||
// BanScopeLogin as "this account cannot authenticate" — the row's
|
||||
// banned_at is set in tandem so legacy callers still see the user
|
||||
// as banned, but the bit makes the rule auditable.
|
||||
if scope, scopeErr := s.userRepository.GetBanState(ctx, atoken.UserID); scopeErr == nil {
|
||||
if models.BanScope(scope).Has(models.BanScopeLogin) {
|
||||
return nil, errx.New(errx.Forbidden, "this account has been suspended")
|
||||
}
|
||||
}
|
||||
|
||||
newToken, err := s.tokenService.GenerateSession(ctx, atoken.UserID, "", ipaddr, userAgent, token.AuthProviderEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/warmbly/warmbly/internal/app/feature"
|
||||
"github.com/warmbly/warmbly/internal/errx"
|
||||
"github.com/warmbly/warmbly/internal/infrastructure/gtasks"
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
"github.com/warmbly/warmbly/internal/repository"
|
||||
"github.com/warmbly/warmbly/internal/scheduler"
|
||||
"github.com/warmbly/warmbly/internal/tasks/proto"
|
||||
@@ -38,6 +39,7 @@ type EmailSendService interface {
|
||||
type emailSendService struct {
|
||||
taskRepo repository.TaskRepository
|
||||
emailRepo repository.EmailRepository
|
||||
userRepo repository.UserRepository
|
||||
scheduler scheduler.SchedulerService
|
||||
tasksClient *gtasks.Client
|
||||
featureGate feature.FeatureGateService
|
||||
@@ -46,6 +48,7 @@ type emailSendService struct {
|
||||
func NewService(
|
||||
taskRepo repository.TaskRepository,
|
||||
emailRepo repository.EmailRepository,
|
||||
userRepo repository.UserRepository,
|
||||
scheduler scheduler.SchedulerService,
|
||||
tasksClient *gtasks.Client,
|
||||
featureGate feature.FeatureGateService,
|
||||
@@ -53,6 +56,7 @@ func NewService(
|
||||
return &emailSendService{
|
||||
taskRepo: taskRepo,
|
||||
emailRepo: emailRepo,
|
||||
userRepo: userRepo,
|
||||
scheduler: scheduler,
|
||||
tasksClient: tasksClient,
|
||||
featureGate: featureGate,
|
||||
@@ -60,6 +64,17 @@ func NewService(
|
||||
}
|
||||
|
||||
func (s *emailSendService) SendEmail(ctx context.Context, userID, orgID, accountID uuid.UUID, req *SendEmailRequest) (*SendEmailResponse, *errx.Error) {
|
||||
// Ban-scope enforcement (migration 000045). Block outbound send
|
||||
// when the admin set BanScopeSend, even if the user can otherwise
|
||||
// log in and inspect their account.
|
||||
if s.userRepo != nil {
|
||||
if scope, scopeErr := s.userRepo.GetBanState(ctx, userID); scopeErr == nil {
|
||||
if models.BanScope(scope).Has(models.BanScopeSend) {
|
||||
return nil, errx.New(errx.Forbidden, "this account cannot send email")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email account exists and belongs to user/org
|
||||
_, xerr := s.emailRepo.GetByID(ctx, accountID)
|
||||
if xerr != nil {
|
||||
|
||||
@@ -122,6 +122,15 @@ func NewService(
|
||||
|
||||
// Create creates a new organization and adds the user as owner
|
||||
func (s *organizationService) Create(ctx context.Context, userID uuid.UUID, name string) (*models.Organization, *errx.Error) {
|
||||
// Ban-scope enforcement (migration 000045). Block new workspace
|
||||
// creation when the admin's set the BanScopeOrgCreate bit, even
|
||||
// if the user can otherwise log in.
|
||||
if scope, scopeErr := s.userRepo.GetBanState(ctx, userID); scopeErr == nil {
|
||||
if models.BanScope(scope).Has(models.BanScopeOrgCreate) {
|
||||
return nil, errx.New(errx.Forbidden, "this account cannot create new workspaces")
|
||||
}
|
||||
}
|
||||
|
||||
// Check organization limit
|
||||
user, userErr := s.userRepo.GetUser(ctx, userID)
|
||||
if userErr != nil {
|
||||
|
||||
@@ -23,6 +23,11 @@ type UserRepository interface {
|
||||
SetFreeTrialUsed(ctx context.Context, userID uuid.UUID) error
|
||||
UpdateOnboarding(ctx context.Context, userID uuid.UUID, firstName, lastName, referralSource string) error
|
||||
UpdateAvatar(ctx context.Context, userID uuid.UUID, avatarURL *string) error
|
||||
|
||||
// GetBanState returns the user's ban_scope bitmask (0 = not
|
||||
// banned). Used by middleware to enforce BanScopeLogin etc.
|
||||
// without re-fetching the full user row.
|
||||
GetBanState(ctx context.Context, userID uuid.UUID) (scope uint32, err error)
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
@@ -156,3 +161,14 @@ func (r *userRepository) UpdateAvatar(ctx context.Context, userID uuid.UUID, ava
|
||||
_, err := r.DB.Exec(ctx, q, userID, avatarURL)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetBanState reads only ban_scope — banned_at is implied by
|
||||
// scope > 0 since unban sets both back to zero. Returns 0 for unbanned
|
||||
// users and for users that don't exist (the latter is fine because
|
||||
// those callers fail elsewhere on the auth check).
|
||||
func (r *userRepository) GetBanState(ctx context.Context, userID uuid.UUID) (uint32, error) {
|
||||
const q = `SELECT ban_scope FROM users WHERE id = $1`
|
||||
var scope uint32
|
||||
err := r.DB.QueryRow(ctx, q, userID).Scan(&scope)
|
||||
return scope, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user