feat(auth): add readonly review account
This commit is contained in:
parent
e4db51ed67
commit
d500976236
@ -105,17 +105,29 @@ func (h *handler) requireAdminPermission(c *gin.Context, permission string) (*st
|
||||
return user, true
|
||||
}
|
||||
|
||||
if !store.IsOperatorRole(user.Role) {
|
||||
respondError(c, http.StatusForbidden, "forbidden", "insufficient permissions")
|
||||
return nil, false
|
||||
if store.IsOperatorRole(user.Role) {
|
||||
if permission != "" && !h.operatorPermissionAllowed(c, permission) {
|
||||
respondError(c, http.StatusForbidden, "forbidden", "operator permission denied")
|
||||
return nil, false
|
||||
}
|
||||
return user, true
|
||||
}
|
||||
|
||||
if permission != "" && !h.operatorPermissionAllowed(c, permission) {
|
||||
respondError(c, http.StatusForbidden, "forbidden", "operator permission denied")
|
||||
return nil, false
|
||||
if strings.EqualFold(strings.TrimSpace(user.Role), store.RoleReadOnly) {
|
||||
method := c.Request.Method
|
||||
if method != http.MethodGet && method != http.MethodHead {
|
||||
respondError(c, http.StatusForbidden, "read_only_account", "demo account is read-only")
|
||||
return nil, false
|
||||
}
|
||||
if permission == "" || !hasPermission(user.Permissions, permission) {
|
||||
respondError(c, http.StatusForbidden, "forbidden", "readonly permission denied")
|
||||
return nil, false
|
||||
}
|
||||
return user, true
|
||||
}
|
||||
|
||||
return user, true
|
||||
respondError(c, http.StatusForbidden, "forbidden", "insufficient permissions")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (h *handler) requireAdminOrOperator(c *gin.Context) (*store.User, bool) {
|
||||
@ -141,6 +153,20 @@ func (h *handler) operatorPermissionAllowed(c *gin.Context, permission string) b
|
||||
return allowed
|
||||
}
|
||||
|
||||
func hasPermission(permissions []string, target string) bool {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return false
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
normalized := strings.TrimSpace(permission)
|
||||
if normalized == "*" || strings.EqualFold(normalized, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *handler) resolveSessionToken(c *gin.Context) string {
|
||||
token := extractToken(c.GetHeader("Authorization"))
|
||||
if token == "" {
|
||||
|
||||
@ -44,6 +44,8 @@ var (
|
||||
const (
|
||||
// SandboxEmail is the canonical email for the sandbox account.
|
||||
SandboxEmail = "sandbox@svc.plus"
|
||||
// ReviewEmail is the canonical email for the readonly App Review account.
|
||||
ReviewEmail = "review@svc.plus"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -51,6 +53,103 @@ const (
|
||||
rootBootstrapPasswordEnv = "ROOT_BOOTSTRAP_PASSWORD"
|
||||
)
|
||||
|
||||
var defaultReviewPermissions = []string{
|
||||
"admin.settings.read",
|
||||
"admin.users.metrics.read",
|
||||
"admin.users.list.read",
|
||||
"admin.agents.status.read",
|
||||
"admin.blacklist.read",
|
||||
}
|
||||
|
||||
func ensureReviewUser(ctx context.Context, st store.Store, cfg config.ReviewAccount, logger *slog.Logger) error {
|
||||
email := strings.ToLower(strings.TrimSpace(cfg.Email))
|
||||
if email == "" {
|
||||
email = ReviewEmail
|
||||
}
|
||||
name := strings.TrimSpace(cfg.Name)
|
||||
if name == "" {
|
||||
name = "Review"
|
||||
}
|
||||
groups := cfg.Groups
|
||||
if len(groups) == 0 {
|
||||
groups = []string{"User", "Beta", "Review", "ReadOnly Role"}
|
||||
}
|
||||
permissions := cfg.Permissions
|
||||
if len(permissions) == 0 {
|
||||
permissions = defaultReviewPermissions
|
||||
}
|
||||
|
||||
reviewUser, err := st.GetUserByEmail(ctx, email)
|
||||
if err != nil && !errors.Is(err, store.ErrUserNotFound) {
|
||||
return fmt.Errorf("lookup review user: %w", err)
|
||||
}
|
||||
|
||||
if !cfg.Enabled {
|
||||
if reviewUser != nil && reviewUser.Active {
|
||||
reviewUser.Active = false
|
||||
if err := st.UpdateUser(ctx, reviewUser); err != nil {
|
||||
return fmt.Errorf("disable review user: %w", err)
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Info("review account disabled", "email", email)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
password := strings.TrimSpace(cfg.Password)
|
||||
if password == "" {
|
||||
return fmt.Errorf("review account %q enabled without password", email)
|
||||
}
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash review password: %w", err)
|
||||
}
|
||||
|
||||
if reviewUser == nil {
|
||||
user := &store.User{
|
||||
Name: name,
|
||||
Email: email,
|
||||
EmailVerified: true,
|
||||
PasswordHash: string(hashed),
|
||||
Level: store.LevelUser,
|
||||
Role: store.RoleReadOnly,
|
||||
Groups: groups,
|
||||
Permissions: permissions,
|
||||
Active: true,
|
||||
}
|
||||
if err := st.CreateUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("create review user: %w", err)
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Info("review account ensured", "email", email, "created", true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
reviewUser.Name = name
|
||||
reviewUser.Email = email
|
||||
reviewUser.EmailVerified = true
|
||||
reviewUser.PasswordHash = string(hashed)
|
||||
reviewUser.Role = store.RoleReadOnly
|
||||
reviewUser.Level = store.LevelUser
|
||||
reviewUser.Groups = groups
|
||||
reviewUser.Permissions = permissions
|
||||
reviewUser.Active = true
|
||||
reviewUser.MFATOTPSecret = ""
|
||||
reviewUser.MFAEnabled = false
|
||||
reviewUser.MFASecretIssuedAt = time.Time{}
|
||||
reviewUser.MFAConfirmedAt = time.Time{}
|
||||
if err := st.UpdateUser(ctx, reviewUser); err != nil {
|
||||
return fmt.Errorf("update review user: %w", err)
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Info("review account ensured", "email", email, "created", false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type mailerAdapter struct {
|
||||
sender mailer.Sender
|
||||
}
|
||||
@ -596,6 +695,9 @@ func runServer(ctx context.Context, cfg *config.Config, logger *slog.Logger) err
|
||||
logger.Warn("failed to ensure sandbox user", "err", err)
|
||||
}
|
||||
startSandboxUUIDRotator(ctx, st, logger)
|
||||
if err := ensureReviewUser(ctx, st, cfg.ReviewAccount, logger); err != nil {
|
||||
logger.Warn("failed to ensure review user", "err", err)
|
||||
}
|
||||
|
||||
var emailSender api.EmailSender
|
||||
emailVerificationEnabled := true
|
||||
|
||||
@ -84,3 +84,20 @@ agents:
|
||||
token: "replace-with-agent-token"
|
||||
groups:
|
||||
- "default"
|
||||
|
||||
reviewAccount:
|
||||
enabled: true
|
||||
email: "review@svc.plus"
|
||||
name: "Review"
|
||||
password: "Review123!"
|
||||
groups:
|
||||
- "User"
|
||||
- "Beta"
|
||||
- "Review"
|
||||
- "ReadOnly Role"
|
||||
permissions:
|
||||
- "admin.settings.read"
|
||||
- "admin.users.metrics.read"
|
||||
- "admin.users.list.read"
|
||||
- "admin.agents.status.read"
|
||||
- "admin.blacklist.read"
|
||||
|
||||
@ -67,6 +67,23 @@ xray:
|
||||
- "restart"
|
||||
- "xray.service"
|
||||
|
||||
reviewAccount:
|
||||
enabled: true
|
||||
email: "review@svc.plus"
|
||||
name: "Review"
|
||||
password: "Review123!"
|
||||
groups:
|
||||
- "User"
|
||||
- "Beta"
|
||||
- "Review"
|
||||
- "ReadOnly Role"
|
||||
permissions:
|
||||
- "admin.settings.read"
|
||||
- "admin.users.metrics.read"
|
||||
- "admin.users.list.read"
|
||||
- "admin.agents.status.read"
|
||||
- "admin.blacklist.read"
|
||||
|
||||
agent:
|
||||
id: "account-primary"
|
||||
controllerUrl: "http://127.0.0.1:8080"
|
||||
|
||||
@ -107,3 +107,20 @@ agents:
|
||||
token: "replace-with-agent-token"
|
||||
groups:
|
||||
- "default"
|
||||
|
||||
reviewAccount:
|
||||
enabled: true
|
||||
email: "review@svc.plus"
|
||||
name: "Review"
|
||||
password: "Review123!"
|
||||
groups:
|
||||
- "User"
|
||||
- "Beta"
|
||||
- "Review"
|
||||
- "ReadOnly Role"
|
||||
permissions:
|
||||
- "admin.settings.read"
|
||||
- "admin.users.metrics.read"
|
||||
- "admin.users.list.read"
|
||||
- "admin.agents.status.read"
|
||||
- "admin.blacklist.read"
|
||||
|
||||
@ -19,16 +19,17 @@ type Log struct {
|
||||
|
||||
// Config holds configuration for the account service.
|
||||
type Config struct {
|
||||
Mode string `yaml:"mode"`
|
||||
Log Log `yaml:"log"`
|
||||
Server Server `yaml:"server"`
|
||||
Store Store `yaml:"store"`
|
||||
Session Session `yaml:"session"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
SMTP SMTP `yaml:"smtp"`
|
||||
Xray Xray `yaml:"xray"`
|
||||
Agent Agent `yaml:"agent"`
|
||||
Agents Agents `yaml:"agents"`
|
||||
Mode string `yaml:"mode"`
|
||||
Log Log `yaml:"log"`
|
||||
Server Server `yaml:"server"`
|
||||
Store Store `yaml:"store"`
|
||||
Session Session `yaml:"session"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
SMTP SMTP `yaml:"smtp"`
|
||||
Xray Xray `yaml:"xray"`
|
||||
Agent Agent `yaml:"agent"`
|
||||
Agents Agents `yaml:"agents"`
|
||||
ReviewAccount ReviewAccount `yaml:"reviewAccount"`
|
||||
}
|
||||
|
||||
// Server defines HTTP server configuration.
|
||||
@ -163,6 +164,17 @@ type Agents struct {
|
||||
Credentials []AgentCredential `yaml:"credentials"`
|
||||
}
|
||||
|
||||
// ReviewAccount controls the built-in readonly review user intended for App
|
||||
// Review and feature validation.
|
||||
type ReviewAccount struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Email string `yaml:"email"`
|
||||
Name string `yaml:"name"`
|
||||
Password string `yaml:"password"`
|
||||
Groups []string `yaml:"groups"`
|
||||
Permissions []string `yaml:"permissions"`
|
||||
}
|
||||
|
||||
// AgentCredential represents a single agent identity authorised to call the
|
||||
// controller API.
|
||||
type AgentCredential struct {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user