mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-05 00:26:39 +00:00
[management] Enable PAT creation during setup (#6003)
* enable pat creation on setup * remove logic from handler towards setup service * fix lint issue * fix rollback on account id returning empty * fix coderabbit comments * fix setup PAT rollback behavior
This commit is contained in:
216
management/server/instance/setup_service.go
Normal file
216
management/server/instance/setup_service.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/shared/auth"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
const (
|
||||
setupPATTokenName = "setup-token"
|
||||
|
||||
// SetupPATEnabledEnvKey enables setup-time Personal Access Token creation.
|
||||
SetupPATEnabledEnvKey = "NB_SETUP_PAT_ENABLED"
|
||||
|
||||
setupPATDefaultExpireDays = 1
|
||||
)
|
||||
|
||||
// SetupOptions controls optional work performed during initial instance setup.
|
||||
type SetupOptions struct {
|
||||
// CreatePAT requests creation of a setup Personal Access Token. It is honored
|
||||
// only when SetupPATEnabledEnvKey is set to "true".
|
||||
CreatePAT bool
|
||||
// PATExpireInDays defaults to 1 day when CreatePAT is requested and setup PAT
|
||||
// creation is enabled.
|
||||
PATExpireInDays *int
|
||||
}
|
||||
|
||||
// SetupResult contains resources created during initial instance setup.
|
||||
type SetupResult struct {
|
||||
User *idp.UserData
|
||||
PATPlainToken string
|
||||
}
|
||||
|
||||
// SetupService orchestrates the initial setup use case across the instance and
|
||||
// account bounded contexts and owns the compensation logic when a later step
|
||||
// fails.
|
||||
type SetupService struct {
|
||||
instanceManager Manager
|
||||
accountManager account.Manager
|
||||
setupPATEnabled bool
|
||||
}
|
||||
|
||||
// NewSetupService creates a setup use-case service.
|
||||
func NewSetupService(instanceManager Manager, accountManager account.Manager) *SetupService {
|
||||
return &SetupService{
|
||||
instanceManager: instanceManager,
|
||||
accountManager: accountManager,
|
||||
setupPATEnabled: os.Getenv(SetupPATEnabledEnvKey) == "true",
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSetupOptions(opts SetupOptions, setupPATEnabled bool) (SetupOptions, error) {
|
||||
if !opts.CreatePAT {
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
if !setupPATEnabled {
|
||||
opts.CreatePAT = false
|
||||
opts.PATExpireInDays = nil
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
if opts.PATExpireInDays == nil {
|
||||
defaultExpireInDays := setupPATDefaultExpireDays
|
||||
opts.PATExpireInDays = &defaultExpireInDays
|
||||
}
|
||||
|
||||
if *opts.PATExpireInDays < account.PATMinExpireDays || *opts.PATExpireInDays > account.PATMaxExpireDays {
|
||||
return opts, status.Errorf(status.InvalidArgument, "pat_expire_in must be between %d and %d", account.PATMinExpireDays, account.PATMaxExpireDays)
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// SetupOwner creates the initial owner user and, when requested and enabled by
|
||||
// SetupPATEnabledEnvKey, provisions the account and a setup Personal Access
|
||||
// Token. If account or PAT provisioning fails, created resources are rolled
|
||||
// back so setup can be retried. If account rollback fails, user rollback is
|
||||
// skipped to avoid leaving an account without its owner user.
|
||||
func (m *SetupService) SetupOwner(ctx context.Context, email, password, name string, opts SetupOptions) (*SetupResult, error) {
|
||||
opts, err := normalizeSetupOptions(opts, m.setupPATEnabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.CreatePAT && m.accountManager == nil {
|
||||
return nil, fmt.Errorf("account manager is required to create setup PAT")
|
||||
}
|
||||
|
||||
userData, err := m.instanceManager.CreateOwnerUser(ctx, email, password, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &SetupResult{User: userData}
|
||||
if !opts.CreatePAT {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
userAuth := auth.UserAuth{
|
||||
UserId: userData.ID,
|
||||
Email: userData.Email,
|
||||
Name: userData.Name,
|
||||
}
|
||||
|
||||
accountID, err := m.accountManager.GetAccountIDByUserID(ctx, userAuth)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("create account for setup user: %w", err)
|
||||
if rollbackErr := m.rollbackSetup(ctx, userData.ID, "account provisioning failed", err, ""); rollbackErr != nil {
|
||||
return nil, fmt.Errorf("%w; failed to roll back setup resources: %v", err, rollbackErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pat, err := m.accountManager.CreatePAT(ctx, accountID, userData.ID, userData.ID, setupPATTokenName, *opts.PATExpireInDays)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("create setup PAT: %w", err)
|
||||
if rollbackErr := m.rollbackSetup(ctx, userData.ID, "setup PAT provisioning failed", err, accountID); rollbackErr != nil {
|
||||
return nil, fmt.Errorf("%w; failed to roll back setup resources: %v", err, rollbackErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.PATPlainToken = pat.PlainToken
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *SetupService) rollbackSetup(ctx context.Context, userID, reason string, origErr error, accountID string) error {
|
||||
if accountID == "" {
|
||||
resolvedAccountID, err := m.lookupSetupAccountIDForRollback(ctx, userID)
|
||||
if err != nil {
|
||||
rollbackErr := fmt.Errorf("resolve setup account for rollback: %w", err)
|
||||
log.WithContext(ctx).Errorf("failed to resolve setup account for user %s after %s: original error: %v, rollback error: %v", userID, reason, origErr, rollbackErr)
|
||||
return rollbackErr
|
||||
}
|
||||
accountID = resolvedAccountID
|
||||
}
|
||||
|
||||
if accountID != "" {
|
||||
if err := m.rollbackSetupAccount(ctx, accountID); err != nil {
|
||||
rollbackErr := fmt.Errorf("roll back setup account %s: %w", accountID, err)
|
||||
log.WithContext(ctx).Errorf("failed to roll back setup account %s for user %s after %s: original error: %v, rollback error: %v", accountID, userID, reason, origErr, rollbackErr)
|
||||
return rollbackErr
|
||||
}
|
||||
log.WithContext(ctx).Warnf("rolled back setup account %s for user %s after %s: %v", accountID, userID, reason, origErr)
|
||||
}
|
||||
|
||||
if err := m.instanceManager.RollbackSetup(ctx, userID); err != nil {
|
||||
rollbackErr := fmt.Errorf("roll back setup user %s: %w", userID, err)
|
||||
log.WithContext(ctx).Errorf("failed to roll back setup user %s after %s: original error: %v, rollback error: %v", userID, reason, origErr, rollbackErr)
|
||||
return rollbackErr
|
||||
}
|
||||
log.WithContext(ctx).Warnf("rolled back setup user %s after %s: %v", userID, reason, origErr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SetupService) lookupSetupAccountIDForRollback(ctx context.Context, userID string) (string, error) {
|
||||
if m.accountManager == nil {
|
||||
return "", fmt.Errorf("account manager is required to resolve setup account")
|
||||
}
|
||||
|
||||
accountStore := m.accountManager.GetStore()
|
||||
if accountStore == nil {
|
||||
return "", fmt.Errorf("account store is unavailable")
|
||||
}
|
||||
|
||||
accountID, err := accountStore.GetAccountIDByUserID(ctx, store.LockingStrengthNone, userID)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("get setup account ID for rollback: %w", err)
|
||||
}
|
||||
|
||||
return accountID, nil
|
||||
}
|
||||
|
||||
// rollbackSetupAccount removes only the setup-created account data from the
|
||||
// store. It intentionally avoids accountManager.DeleteAccount because the normal
|
||||
// account deletion path also deletes users from the IdP; embedded IdP cleanup is
|
||||
// owned by instanceManager.RollbackSetup.
|
||||
func (m *SetupService) rollbackSetupAccount(ctx context.Context, accountID string) error {
|
||||
if m.accountManager == nil {
|
||||
return fmt.Errorf("account manager is required to roll back setup account")
|
||||
}
|
||||
|
||||
accountStore := m.accountManager.GetStore()
|
||||
if accountStore == nil {
|
||||
return fmt.Errorf("account store is unavailable")
|
||||
}
|
||||
|
||||
account, err := accountStore.GetAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("get setup account for rollback: %w", err)
|
||||
}
|
||||
|
||||
if err := accountStore.DeleteAccount(ctx, account); err != nil {
|
||||
if isNotFoundError(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("delete setup account for rollback: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user