2
0
Files
gitcaddy-server/services/vault/vault.go
logikonline 5c9385f4a2 feat(vault): add user-facing warnings for key configuration issues
Adds dedicated error page and warnings for vault encryption key problems including missing configuration, fallback key usage, and decryption failures. Displays context-specific messages to help users understand and fix key configuration issues. Includes detection of crypto errors in vault operations and graceful error handling throughout the UI.
2026-02-04 13:54:54 -05:00

559 lines
15 KiB
Go

// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package vault
import (
"context"
"errors"
"code.gitcaddy.com/server/v3/modules/plugins"
)
var (
ErrVaultNotAvailable = errors.New("vault plugin not available")
ErrVaultNotLicensed = errors.New("vault plugin not licensed")
ErrVaultNotConfigured = errors.New("vault master key not configured")
ErrSecretNotFound = errors.New("secret not found")
ErrSecretExists = errors.New("secret already exists")
ErrSecretLimitReached = errors.New("secret limit reached for current license tier")
ErrFeatureNotInTier = errors.New("feature not available in current license tier")
ErrTokenNotFound = errors.New("token not found")
ErrTokenExpired = errors.New("token expired")
ErrTokenLimitReached = errors.New("token limit reached for current license tier")
ErrTokenTTLExceeded = errors.New("token TTL exceeds maximum for current license tier")
ErrInvalidToken = errors.New("invalid token")
ErrInvalidScope = errors.New("invalid token scope")
ErrAccessDenied = errors.New("access denied")
)
// Plugin defines the interface that vault plugins must implement
type Plugin interface {
plugins.Plugin
plugins.LicensedPlugin
// Secret operations
ListSecrets(ctx context.Context, repoID int64, includeDeleted bool) ([]Secret, error)
GetSecret(ctx context.Context, repoID int64, name string) (*Secret, error)
GetSecretValue(ctx context.Context, repoID int64, name string, version int) (string, error)
CreateSecret(ctx context.Context, repoID int64, opts CreateSecretOptions) (*Secret, error)
UpdateSecret(ctx context.Context, repoID int64, name string, opts UpdateSecretOptions) (*Secret, error)
DeleteSecret(ctx context.Context, repoID int64, name string, userID int64) error
RestoreSecret(ctx context.Context, repoID int64, name string) error
RollbackSecret(ctx context.Context, repoID int64, name string, version int, userID int64) error
// Version operations
ListVersions(ctx context.Context, repoID int64, name string) ([]SecretVersion, error)
// Token operations
ListTokens(ctx context.Context, repoID int64) ([]Token, error)
CreateToken(ctx context.Context, repoID int64, opts CreateTokenOptions) (*Token, string, error) // returns token and raw value
RevokeToken(ctx context.Context, repoID, tokenID int64) error
ValidateToken(ctx context.Context, rawToken, action, secretName string) (*Token, error)
// Audit operations
ListAuditEntries(ctx context.Context, repoID int64, page, pageSize int) ([]AuditEntry, int64, error)
}
// ConfigurablePlugin is an optional interface that vault plugins can implement
// to report their configuration status
type ConfigurablePlugin interface {
// IsConfigured returns true if the plugin is properly configured (e.g., has master key)
IsConfigured() bool
// ConfigurationError returns the configuration error message, if any
ConfigurationError() string
// IsUsingFallbackKey returns true if the vault is using Gitea's SECRET_KEY as
// the encryption key instead of an explicit vault-specific key.
IsUsingFallbackKey() bool
// KeySource returns a human-readable description of where the master key was loaded from.
KeySource() string
}
// Secret represents a vault secret
type Secret struct {
ID int64
RepoID int64
Name string
Description string
Type string
CurrentVersion int
CreatedUnix int64
UpdatedUnix int64
DeletedUnix int64
}
// IsDeleted returns true if the secret is soft deleted
func (s Secret) IsDeleted() bool {
return s.DeletedUnix > 0
}
// SecretVersion represents a version of a secret
type SecretVersion struct {
ID int64
SecretID int64
Version int
CreatorID int64
CreatorName string
Comment string
CreatedUnix int64
}
// Token represents a CI/CD token
type Token struct {
ID int64
RepoID int64
Description string
Scope string
CreatedUnix int64
ExpiresUnix int64
LastUsedUnix int64
UsedCount int64
IsRevoked bool
IsExpired bool
}
// AuditEntry represents an audit log entry
type AuditEntry struct {
ID int64
RepoID int64
SecretName string
Action string
UserID int64
UserName string
IPAddress string
Success bool
FailReason string
Timestamp int64
}
// CreateSecretOptions contains options for creating a secret
type CreateSecretOptions struct {
Name string
Description string
Type string
Value string
CreatorID int64
}
// UpdateSecretOptions contains options for updating a secret
type UpdateSecretOptions struct {
Type string
Value string
Comment string
UpdaterID int64
}
// CreateTokenOptions contains options for creating a token
type CreateTokenOptions struct {
Description string
Scope string
TTL string // e.g., "1h", "24h", "168h"
CreatorID int64
}
// GetPlugin returns the registered vault plugin or nil
func GetPlugin() Plugin {
p := plugins.Get("vault")
if p == nil {
return nil
}
vp, ok := p.(Plugin)
if !ok {
return nil
}
return vp
}
// IsAvailable returns true if the vault plugin is registered
func IsAvailable() bool {
return GetPlugin() != nil
}
// IsLicensed returns true if the vault plugin is licensed
// Note: This always returns true if the plugin is available because
// we default to Solo tier (free) when no license is present
func IsLicensed() bool {
return plugins.IsLicensed("vault")
}
// IsConfigured returns true if the vault plugin is properly configured
// (e.g., has a master key set). Returns true if plugin doesn't implement
// ConfigurablePlugin interface (assumes configured).
func IsConfigured() bool {
vp := GetPlugin()
if vp == nil {
return false
}
if cp, ok := vp.(ConfigurablePlugin); ok {
return cp.IsConfigured()
}
return true // Assume configured if plugin doesn't implement interface
}
// GetConfigurationError returns the configuration error message if the
// vault plugin is not properly configured. Returns empty string if configured
// or if the plugin doesn't implement ConfigurablePlugin interface.
func GetConfigurationError() string {
vp := GetPlugin()
if vp == nil {
return "vault plugin not available"
}
if cp, ok := vp.(ConfigurablePlugin); ok {
return cp.ConfigurationError()
}
return ""
}
// IsUsingFallbackKey returns true if the vault is using Gitea's SECRET_KEY
// as the encryption key instead of an explicit vault-specific key.
// Returns false if the plugin doesn't implement the ConfigurablePlugin interface.
func IsUsingFallbackKey() bool {
vp := GetPlugin()
if vp == nil {
return false
}
if cp, ok := vp.(ConfigurablePlugin); ok {
return cp.IsUsingFallbackKey()
}
return false
}
// KeySource returns a human-readable description of where the master key was loaded from.
// Returns empty string if the plugin doesn't implement the ConfigurablePlugin interface.
func KeySource() string {
vp := GetPlugin()
if vp == nil {
return ""
}
if cp, ok := vp.(ConfigurablePlugin); ok {
return cp.KeySource()
}
return ""
}
// GetLicenseInfo returns the license info for the vault plugin
// Returns default Solo license if no license file is present
func GetLicenseInfo() *plugins.LicenseInfo {
return plugins.GetLicenseInfo("vault")
}
// GetLimits returns the license limits for the vault plugin
func GetLimits() *plugins.LicenseLimits {
return plugins.GetLicenseLimits("vault")
}
// GetTier returns the current license tier (defaults to "solo")
func GetTier() string {
info := GetLicenseInfo()
if info == nil {
return plugins.TierSolo
}
return info.Tier
}
// CanUseVersioning returns true if the current tier supports versioning
func CanUseVersioning() bool {
limits := GetLimits()
return limits != nil && limits.VersioningEnabled
}
// CanUseCICDTokens returns true if the current tier supports CI/CD tokens
func CanUseCICDTokens() bool {
limits := GetLimits()
return limits != nil && limits.CICDTokensEnabled
}
// CanUseSSO returns true if the current tier supports SSO
func CanUseSSO() bool {
limits := GetLimits()
return limits != nil && limits.SSOEnabled
}
// GetMaxSecretsPerRepo returns the max secrets allowed per repo (-1 = unlimited)
func GetMaxSecretsPerRepo() int {
limits := GetLimits()
if limits == nil {
return 5 // Solo default
}
return limits.SecretsPerRepo
}
// GetAuditRetentionDays returns the audit log retention days
func GetAuditRetentionDays() int {
limits := GetLimits()
if limits == nil {
return 7 // Solo default
}
return limits.AuditRetentionDays
}
// GetMaxVersions returns the max versions to keep per secret (-1 = unlimited)
func GetMaxVersions() int {
limits := GetLimits()
if limits == nil {
return 2 // Solo default
}
return limits.MaxVersions
}
// GetMaxTokens returns the max tokens allowed per repo (-1 = unlimited)
func GetMaxTokens() int {
limits := GetLimits()
if limits == nil {
return 1 // Solo default
}
return limits.MaxTokens
}
// GetMaxTokenTTLHours returns the max token TTL in hours (-1 = unlimited)
func GetMaxTokenTTLHours() int {
limits := GetLimits()
if limits == nil {
return 24 // Solo default
}
return limits.MaxTokenTTLHours
}
// AreTokensReadOnly returns true if tokens are read-only (Solo tier restriction)
func AreTokensReadOnly() bool {
limits := GetLimits()
if limits == nil {
return true // Solo default
}
return limits.TokensReadOnly
}
// HasUnlimitedVersions returns true if the tier has unlimited version history
func HasUnlimitedVersions() bool {
limits := GetLimits()
return limits != nil && limits.MaxVersions == -1
}
// HasUnlimitedTokens returns true if the tier has unlimited tokens
func HasUnlimitedTokens() bool {
limits := GetLimits()
return limits != nil && limits.MaxTokens == -1
}
// CheckTokenLimit checks if adding a new token would exceed the tier limit
func CheckTokenLimit(ctx context.Context, repoID int64) error {
maxTokens := GetMaxTokens()
if maxTokens == -1 {
return nil // Unlimited
}
vp := GetPlugin()
if vp == nil {
return ErrVaultNotAvailable
}
tokens, err := vp.ListTokens(ctx, repoID)
if err != nil {
return err
}
// Count active (non-revoked, non-expired) tokens
activeCount := 0
for _, t := range tokens {
if !t.IsRevoked && !t.IsExpired {
activeCount++
}
}
if activeCount >= maxTokens {
return ErrTokenLimitReached
}
return nil
}
// CheckSecretLimit checks if adding a new secret would exceed the tier limit
func CheckSecretLimit(ctx context.Context, repoID int64) error {
maxSecrets := GetMaxSecretsPerRepo()
if maxSecrets == -1 {
return nil // Unlimited
}
vp := GetPlugin()
if vp == nil {
return ErrVaultNotAvailable
}
secrets, err := vp.ListSecrets(ctx, repoID, false)
if err != nil {
return err
}
if len(secrets) >= maxSecrets {
return ErrSecretLimitReached
}
return nil
}
// ListSecrets lists all secrets for a repository
func ListSecrets(ctx context.Context, repoID int64, includeDeleted bool) ([]Secret, error) {
vp := GetPlugin()
if vp == nil {
return nil, ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, ErrVaultNotLicensed
}
return vp.ListSecrets(ctx, repoID, includeDeleted)
}
// GetSecret gets a secret by name
func GetSecret(ctx context.Context, repoID int64, name string) (*Secret, error) {
vp := GetPlugin()
if vp == nil {
return nil, ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, ErrVaultNotLicensed
}
return vp.GetSecret(ctx, repoID, name)
}
// GetSecretValue gets the decrypted value of a secret
func GetSecretValue(ctx context.Context, repoID int64, name string, version int) (string, error) {
vp := GetPlugin()
if vp == nil {
return "", ErrVaultNotAvailable
}
if !IsLicensed() {
return "", ErrVaultNotLicensed
}
return vp.GetSecretValue(ctx, repoID, name, version)
}
// CreateSecret creates a new secret
func CreateSecret(ctx context.Context, repoID int64, opts CreateSecretOptions) (*Secret, error) {
vp := GetPlugin()
if vp == nil {
return nil, ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, ErrVaultNotLicensed
}
return vp.CreateSecret(ctx, repoID, opts)
}
// UpdateSecret updates an existing secret
func UpdateSecret(ctx context.Context, repoID int64, name string, opts UpdateSecretOptions) (*Secret, error) {
vp := GetPlugin()
if vp == nil {
return nil, ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, ErrVaultNotLicensed
}
return vp.UpdateSecret(ctx, repoID, name, opts)
}
// DeleteSecret soft-deletes a secret
func DeleteSecret(ctx context.Context, repoID int64, name string, userID int64) error {
vp := GetPlugin()
if vp == nil {
return ErrVaultNotAvailable
}
if !IsLicensed() {
return ErrVaultNotLicensed
}
return vp.DeleteSecret(ctx, repoID, name, userID)
}
// RestoreSecret restores a soft-deleted secret
func RestoreSecret(ctx context.Context, repoID int64, name string) error {
vp := GetPlugin()
if vp == nil {
return ErrVaultNotAvailable
}
if !IsLicensed() {
return ErrVaultNotLicensed
}
return vp.RestoreSecret(ctx, repoID, name)
}
// RollbackSecret rolls back a secret to a previous version
func RollbackSecret(ctx context.Context, repoID int64, name string, version int, userID int64) error {
vp := GetPlugin()
if vp == nil {
return ErrVaultNotAvailable
}
if !IsLicensed() {
return ErrVaultNotLicensed
}
return vp.RollbackSecret(ctx, repoID, name, version, userID)
}
// ListVersions lists all versions of a secret
func ListVersions(ctx context.Context, repoID int64, name string) ([]SecretVersion, error) {
vp := GetPlugin()
if vp == nil {
return nil, ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, ErrVaultNotLicensed
}
return vp.ListVersions(ctx, repoID, name)
}
// ListTokens lists all tokens for a repository
func ListTokens(ctx context.Context, repoID int64) ([]Token, error) {
vp := GetPlugin()
if vp == nil {
return nil, ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, ErrVaultNotLicensed
}
return vp.ListTokens(ctx, repoID)
}
// CreateToken creates a new CI/CD token
func CreateToken(ctx context.Context, repoID int64, opts CreateTokenOptions) (*Token, string, error) {
vp := GetPlugin()
if vp == nil {
return nil, "", ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, "", ErrVaultNotLicensed
}
return vp.CreateToken(ctx, repoID, opts)
}
// RevokeToken revokes a token
func RevokeToken(ctx context.Context, repoID, tokenID int64) error {
vp := GetPlugin()
if vp == nil {
return ErrVaultNotAvailable
}
if !IsLicensed() {
return ErrVaultNotLicensed
}
return vp.RevokeToken(ctx, repoID, tokenID)
}
// ValidateToken validates a token for a specific action
func ValidateToken(ctx context.Context, rawToken, action, secretName string) (*Token, error) {
vp := GetPlugin()
if vp == nil {
return nil, ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, ErrVaultNotLicensed
}
return vp.ValidateToken(ctx, rawToken, action, secretName)
}
// ListAuditEntries lists audit entries for a repository
func ListAuditEntries(ctx context.Context, repoID int64, page, pageSize int) ([]AuditEntry, int64, error) {
vp := GetPlugin()
if vp == nil {
return nil, 0, ErrVaultNotAvailable
}
if !IsLicensed() {
return nil, 0, ErrVaultNotLicensed
}
return vp.ListAuditEntries(ctx, repoID, page, pageSize)
}