2
0
Files
logikonline f97e0dce4d refactor: update imports to use server/v3 module path
Updates all imports and go.mod to use the new /v3 suffixed module path
for proper Go semantic versioning compliance.

Also updates CI workflows to use version tags (v3.x.x) instead of
pseudo-versions now that the server module has the proper /v3 suffix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 17:59:28 -05:00

283 lines
7.7 KiB
Go

// Copyright 2026 MarketAlly. All rights reserved.
// Proprietary and confidential.
package models
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/modules/timeutil"
"xorm.io/builder"
)
// TokenScope represents a token's permission scope
// Grammar:
//
// scope = permission ":" target ("," target)*
// | "admin"
// permission = "read" | "write"
// target = "*" // all secrets
// | prefix "*" // prefix match (prod.*)
// | secret-name // exact match
//
// Examples:
//
// read:* - read all secrets
// write:* - write all secrets
// read:prod.* - read secrets starting with "prod."
// read:prod.*,staging.* - read prod and staging prefixes
// write:db.credentials - write only db.credentials
// admin - manage tokens, rotate repo key, view audit
type TokenScope string
// VaultToken represents a scoped access token for CI/CD
type VaultToken struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
TokenHash string `xorm:"VARCHAR(64) UNIQUE NOT NULL"` // SHA-256(token)
Scope TokenScope `xorm:"VARCHAR(255) NOT NULL"`
Description string `xorm:"VARCHAR(255)"`
ExpiresUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
CreatedBy int64 `xorm:"NOT NULL"`
UsedCount int64 `xorm:"NOT NULL DEFAULT 0"`
LastUsedUnix timeutil.TimeStamp
RevokedUnix timeutil.TimeStamp // 0 = active, >0 = revoked
}
// TableName returns the table name for xorm
func (VaultToken) TableName() string {
return "vault_token"
}
// IsActive returns true if the token is not revoked and not expired
func (t *VaultToken) IsActive() bool {
if t.RevokedUnix > 0 {
return false
}
if t.ExpiresUnix > 0 && t.ExpiresUnix < timeutil.TimeStampNow() {
return false
}
return true
}
// IsExpired returns true if the token is expired
func (t *VaultToken) IsExpired() bool {
return t.ExpiresUnix > 0 && t.ExpiresUnix < timeutil.TimeStampNow()
}
// IsRevoked returns true if the token is revoked
func (t *VaultToken) IsRevoked() bool {
return t.RevokedUnix > 0
}
// Allows checks if the scope allows the given action on the secret
func (scope TokenScope) Allows(action string, secretName string) bool {
scopeStr := string(scope)
if scopeStr == "admin" {
return true
}
parts := strings.SplitN(scopeStr, ":", 2)
if len(parts) != 2 {
return false
}
permission, targets := parts[0], parts[1]
if permission != action {
return false
}
for _, target := range strings.Split(targets, ",") {
target = strings.TrimSpace(target)
if target == "*" {
return true
}
if strings.HasSuffix(target, "*") {
prefix := strings.TrimSuffix(target, "*")
if strings.HasPrefix(secretName, prefix) {
return true
}
} else if target == secretName {
return true
}
}
return false
}
// CanRead checks if the scope allows reading the secret
func (scope TokenScope) CanRead(secretName string) bool {
return scope.Allows("read", secretName) || scope.Allows("write", secretName)
}
// CanWrite checks if the scope allows writing the secret
func (scope TokenScope) CanWrite(secretName string) bool {
return scope.Allows("write", secretName)
}
// IsAdmin checks if the scope has admin privileges
func (scope TokenScope) IsAdmin() bool {
return string(scope) == "admin"
}
// GenerateToken generates a new random token and its hash
func GenerateToken() (plaintext string, hash string) {
token := make([]byte, 32) // 256 bits entropy
_, _ = rand.Read(token) // crypto/rand.Read always fills the buffer
plaintext = base64.URLEncoding.EncodeToString(token)
hashBytes := sha256.Sum256([]byte(plaintext))
hash = fmt.Sprintf("%x", hashBytes)
return plaintext, hash
}
// HashToken returns the SHA-256 hash of a token
func HashToken(plaintext string) string {
hashBytes := sha256.Sum256([]byte(plaintext))
return fmt.Sprintf("%x", hashBytes)
}
// FindVaultTokenOptions contains options for finding tokens
type FindVaultTokenOptions struct {
db.ListOptions
RepoID int64
TokenHash string
IncludeRevoked bool
IncludeExpired bool
}
// ToConds converts options to xorm conditions
func (opts FindVaultTokenOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
if opts.TokenHash != "" {
cond = cond.And(builder.Eq{"token_hash": opts.TokenHash})
}
if !opts.IncludeRevoked {
cond = cond.And(builder.Eq{"revoked_unix": 0})
}
if !opts.IncludeExpired {
cond = cond.And(builder.Or(
builder.Eq{"expires_unix": 0},
builder.Gte{"expires_unix": timeutil.TimeStampNow()},
))
}
return cond
}
// CreateVaultToken creates a new vault token
func CreateVaultToken(ctx context.Context, token *VaultToken) error {
_, err := db.GetEngine(ctx).Insert(token)
return err
}
// GetVaultTokenByHash returns a token by its hash
func GetVaultTokenByHash(ctx context.Context, hash string) (*VaultToken, error) {
token := &VaultToken{}
has, err := db.GetEngine(ctx).Where("token_hash = ?", hash).Get(token)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return token, nil
}
// GetVaultTokenByID returns a token by ID
func GetVaultTokenByID(ctx context.Context, id int64) (*VaultToken, error) {
token := &VaultToken{}
has, err := db.GetEngine(ctx).ID(id).Get(token)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return token, nil
}
// GetVaultTokensByRepo returns all tokens for a repository
func GetVaultTokensByRepo(ctx context.Context, repoID int64, includeRevoked bool) ([]*VaultToken, error) {
tokens := make([]*VaultToken, 0)
sess := db.GetEngine(ctx).Where("repo_id = ?", repoID)
if !includeRevoked {
sess = sess.And("revoked_unix = 0")
}
err := sess.OrderBy("created_unix DESC").Find(&tokens)
return tokens, err
}
// RevokeVaultToken revokes a token
func RevokeVaultToken(ctx context.Context, token *VaultToken) error {
token.RevokedUnix = timeutil.TimeStampNow()
_, err := db.GetEngine(ctx).ID(token.ID).Cols("revoked_unix").Update(token)
return err
}
// RecordTokenUse records that a token was used
func RecordTokenUse(ctx context.Context, token *VaultToken) error {
token.UsedCount++
token.LastUsedUnix = timeutil.TimeStampNow()
_, err := db.GetEngine(ctx).ID(token.ID).Cols("used_count", "last_used_unix").Update(token)
return err
}
// ValidateAndUseToken validates a plaintext token and records its usage
func ValidateAndUseToken(ctx context.Context, plaintext string, repoID int64) (*VaultToken, error) {
hash := HashToken(plaintext)
token, err := GetVaultTokenByHash(ctx, hash)
if err != nil {
return nil, err
}
if token == nil {
return nil, fmt.Errorf("token not found")
}
// Verify repo
if token.RepoID != repoID {
return nil, fmt.Errorf("token not valid for this repository")
}
// Check if active
if !token.IsActive() {
if token.IsRevoked() {
return nil, fmt.Errorf("token revoked")
}
if token.IsExpired() {
return nil, fmt.Errorf("token expired")
}
}
// Record usage
if err := RecordTokenUse(ctx, token); err != nil {
return nil, err
}
return token, nil
}
// DeleteExpiredTokens deletes tokens that have been expired for more than retentionDays
func DeleteExpiredTokens(ctx context.Context, retentionDays int) (int64, error) {
cutoff := timeutil.TimeStampNow() - timeutil.TimeStamp(retentionDays*24*60*60)
result, err := db.GetEngine(ctx).Where("expires_unix > 0 AND expires_unix < ?", cutoff).Delete(&VaultToken{})
if err != nil {
return 0, err
}
return result, nil
}