refactor(crypt): complete AX v0.8.0 polish pass
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
f46cd04e2f
commit
53d7d59a9d
42 changed files with 527 additions and 354 deletions
34
auth/auth.go
34
auth/auth.go
|
|
@ -40,11 +40,14 @@ import (
|
|||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// Default durations for challenge and session lifetimes.
|
||||
const (
|
||||
// DefaultChallengeTTL is the default lifetime for a generated challenge.
|
||||
// Usage: pass DefaultChallengeTTL into WithChallengeTTL(...) to keep the package default.
|
||||
DefaultChallengeTTL = 5 * time.Minute
|
||||
DefaultSessionTTL = 24 * time.Hour
|
||||
nonceBytes = 32
|
||||
// DefaultSessionTTL is the default lifetime for an authenticated session.
|
||||
// Usage: pass DefaultSessionTTL into WithSessionTTL(...) to keep the package default.
|
||||
DefaultSessionTTL = 24 * time.Hour
|
||||
nonceBytes = 32
|
||||
)
|
||||
|
||||
// protectedUsers lists usernames that cannot be deleted.
|
||||
|
|
@ -55,6 +58,7 @@ var protectedUsers = map[string]bool{
|
|||
}
|
||||
|
||||
// User represents a registered user with PGP credentials.
|
||||
// Usage: use User with the other exported helpers in this package.
|
||||
type User struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
KeyID string `json:"key_id"`
|
||||
|
|
@ -65,6 +69,7 @@ type User struct {
|
|||
}
|
||||
|
||||
// Challenge is a PGP-encrypted nonce sent to a client during authentication.
|
||||
// Usage: use Challenge with the other exported helpers in this package.
|
||||
type Challenge struct {
|
||||
Nonce []byte `json:"nonce"`
|
||||
Encrypted string `json:"encrypted"` // PGP-encrypted nonce (armored)
|
||||
|
|
@ -72,6 +77,7 @@ type Challenge struct {
|
|||
}
|
||||
|
||||
// Session represents an authenticated session.
|
||||
// Usage: use Session with the other exported helpers in this package.
|
||||
type Session struct {
|
||||
Token string `json:"token"`
|
||||
UserID string `json:"user_id"`
|
||||
|
|
@ -80,6 +86,7 @@ type Session struct {
|
|||
|
||||
// Revocation records the details of a revoked user key.
|
||||
// Stored as JSON in the user's .rev file, replacing the legacy placeholder.
|
||||
// Usage: use Revocation with the other exported helpers in this package.
|
||||
type Revocation struct {
|
||||
UserID string `json:"user_id"`
|
||||
Reason string `json:"reason"`
|
||||
|
|
@ -87,9 +94,11 @@ type Revocation struct {
|
|||
}
|
||||
|
||||
// Option configures an Authenticator.
|
||||
// Usage: use Option with the other exported helpers in this package.
|
||||
type Option func(*Authenticator)
|
||||
|
||||
// WithChallengeTTL sets the lifetime of a challenge before it expires.
|
||||
// Usage: pass WithChallengeTTL(...) into the related constructor to adjust the default behaviour.
|
||||
func WithChallengeTTL(d time.Duration) Option {
|
||||
return func(a *Authenticator) {
|
||||
a.challengeTTL = d
|
||||
|
|
@ -97,6 +106,7 @@ func WithChallengeTTL(d time.Duration) Option {
|
|||
}
|
||||
|
||||
// WithSessionTTL sets the lifetime of a session before it expires.
|
||||
// Usage: pass WithSessionTTL(...) into the related constructor to adjust the default behaviour.
|
||||
func WithSessionTTL(d time.Duration) Option {
|
||||
return func(a *Authenticator) {
|
||||
a.sessionTTL = d
|
||||
|
|
@ -105,6 +115,7 @@ func WithSessionTTL(d time.Duration) Option {
|
|||
|
||||
// WithSessionStore sets the SessionStore implementation.
|
||||
// If not provided, an in-memory store is used (sessions lost on restart).
|
||||
// Usage: pass WithSessionStore(...) into the related constructor to adjust the default behaviour.
|
||||
func WithSessionStore(s SessionStore) Option {
|
||||
return func(a *Authenticator) {
|
||||
a.store = s
|
||||
|
|
@ -120,6 +131,7 @@ func WithSessionStore(s SessionStore) Option {
|
|||
// An optional HardwareKey can be provided via WithHardwareKey for
|
||||
// hardware-backed cryptographic operations (PKCS#11, YubiKey, etc.).
|
||||
// See auth/hardware.go for the interface definition and integration points.
|
||||
// Usage: create an Authenticator with New(...) and then call Register, Login, or CreateChallenge.
|
||||
type Authenticator struct {
|
||||
medium io.Medium
|
||||
store SessionStore
|
||||
|
|
@ -133,6 +145,7 @@ type Authenticator struct {
|
|||
// New creates an Authenticator that persists user data via the given Medium.
|
||||
// By default, sessions are stored in memory. Use WithSessionStore to provide
|
||||
// a persistent implementation (e.g. SQLiteSessionStore).
|
||||
// Usage: call New(...) to create a ready-to-use value.
|
||||
func New(m io.Medium, opts ...Option) *Authenticator {
|
||||
a := &Authenticator{
|
||||
medium: m,
|
||||
|
|
@ -159,6 +172,7 @@ func userPath(userID, ext string) string {
|
|||
// produce a userID, generates a PGP keypair (protected by the given password),
|
||||
// and persists the public key, private key, revocation placeholder, password
|
||||
// hash (Argon2id), and encrypted metadata via the Medium.
|
||||
// Usage: call Register(...) during the package's normal workflow.
|
||||
func (a *Authenticator) Register(username, password string) (*User, error) {
|
||||
const op = "auth.Register"
|
||||
|
||||
|
|
@ -237,6 +251,7 @@ func (a *Authenticator) Register(username, password string) (*User, error) {
|
|||
// CreateChallenge generates a cryptographic challenge for the given user.
|
||||
// A random nonce is created and encrypted with the user's PGP public key.
|
||||
// The client must decrypt the nonce and sign it to prove key ownership.
|
||||
// Usage: call CreateChallenge(...) during the package's normal workflow.
|
||||
func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) {
|
||||
const op = "auth.CreateChallenge"
|
||||
|
||||
|
|
@ -279,6 +294,7 @@ func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) {
|
|||
// ValidateResponse verifies a signed nonce from the client. The client must
|
||||
// have decrypted the challenge nonce and signed it with their private key.
|
||||
// On success, a new session is created and returned.
|
||||
// Usage: call ValidateResponse(...) during the package's normal workflow.
|
||||
func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Session, error) {
|
||||
const op = "auth.ValidateResponse"
|
||||
|
||||
|
|
@ -313,6 +329,7 @@ func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Se
|
|||
}
|
||||
|
||||
// ValidateSession checks whether a token maps to a valid, non-expired session.
|
||||
// Usage: call ValidateSession(...) during the package's normal workflow.
|
||||
func (a *Authenticator) ValidateSession(token string) (*Session, error) {
|
||||
const op = "auth.ValidateSession"
|
||||
|
||||
|
|
@ -330,6 +347,7 @@ func (a *Authenticator) ValidateSession(token string) (*Session, error) {
|
|||
}
|
||||
|
||||
// RefreshSession extends the expiry of an existing valid session.
|
||||
// Usage: call RefreshSession(...) during the package's normal workflow.
|
||||
func (a *Authenticator) RefreshSession(token string) (*Session, error) {
|
||||
const op = "auth.RefreshSession"
|
||||
|
||||
|
|
@ -351,6 +369,7 @@ func (a *Authenticator) RefreshSession(token string) (*Session, error) {
|
|||
}
|
||||
|
||||
// RevokeSession removes a session, invalidating the token immediately.
|
||||
// Usage: call RevokeSession(...) during the package's normal workflow.
|
||||
func (a *Authenticator) RevokeSession(token string) error {
|
||||
const op = "auth.RevokeSession"
|
||||
|
||||
|
|
@ -363,6 +382,7 @@ func (a *Authenticator) RevokeSession(token string) error {
|
|||
// DeleteUser removes a user and all associated keys from storage.
|
||||
// The "server" user is protected and cannot be deleted (mirroring the
|
||||
// original TypeScript implementation's safeguard).
|
||||
// Usage: call DeleteUser(...) during the package's normal workflow.
|
||||
func (a *Authenticator) DeleteUser(userID string) error {
|
||||
const op = "auth.DeleteUser"
|
||||
|
||||
|
|
@ -403,6 +423,8 @@ func (a *Authenticator) DeleteUser(userID string) error {
|
|||
// - Otherwise, falls back to legacy .lthn file with LTHN hash verification.
|
||||
// On successful legacy login, the password is re-hashed with Argon2id and
|
||||
// a .hash file is written (transparent migration).
|
||||
//
|
||||
// Usage: call Login(...) for password-based flows when challenge-response is not required.
|
||||
func (a *Authenticator) Login(userID, password string) (*Session, error) {
|
||||
const op = "auth.Login"
|
||||
|
||||
|
|
@ -455,6 +477,7 @@ func (a *Authenticator) Login(userID, password string) (*Session, error) {
|
|||
// all existing sessions. The caller must provide the current password
|
||||
// (oldPassword) to decrypt existing metadata and the new password (newPassword)
|
||||
// to protect the new keypair.
|
||||
// Usage: call RotateKeyPair(...) during the package's normal workflow.
|
||||
func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) (*User, error) {
|
||||
const op = "auth.RotateKeyPair"
|
||||
|
||||
|
|
@ -539,6 +562,7 @@ func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) (
|
|||
// RevokeKey marks a user's key as revoked. It verifies the password first,
|
||||
// writes a JSON revocation record to the .rev file (replacing the placeholder),
|
||||
// and invalidates all sessions for the user.
|
||||
// Usage: call RevokeKey(...) during the package's normal workflow.
|
||||
func (a *Authenticator) RevokeKey(userID, password, reason string) error {
|
||||
const op = "auth.RevokeKey"
|
||||
|
||||
|
|
@ -576,6 +600,7 @@ func (a *Authenticator) RevokeKey(userID, password, reason string) error {
|
|||
// IsRevoked checks whether a user's key has been revoked by inspecting the
|
||||
// .rev file. Returns true only if the file contains valid revocation JSON
|
||||
// (not the legacy "REVOCATION_PLACEHOLDER" string).
|
||||
// Usage: call IsRevoked(...) during the package's normal workflow.
|
||||
func (a *Authenticator) IsRevoked(userID string) bool {
|
||||
content, err := a.medium.Read(userPath(userID, ".rev"))
|
||||
if err != nil {
|
||||
|
|
@ -601,6 +626,7 @@ func (a *Authenticator) IsRevoked(userID string) bool {
|
|||
// WriteChallengeFile writes an encrypted challenge to a file for air-gapped
|
||||
// (courier) transport. The challenge is created and then its encrypted nonce
|
||||
// is written to the specified path on the Medium.
|
||||
// Usage: call WriteChallengeFile(...) during the package's normal workflow.
|
||||
func (a *Authenticator) WriteChallengeFile(userID, path string) error {
|
||||
const op = "auth.WriteChallengeFile"
|
||||
|
||||
|
|
@ -625,6 +651,7 @@ func (a *Authenticator) WriteChallengeFile(userID, path string) error {
|
|||
// ReadResponseFile reads a signed response from a file and validates it,
|
||||
// completing the air-gapped authentication flow. The file must contain the
|
||||
// raw PGP signature bytes (armored).
|
||||
// Usage: call ReadResponseFile(...) during the package's normal workflow.
|
||||
func (a *Authenticator) ReadResponseFile(userID, path string) (*Session, error) {
|
||||
const op = "auth.ReadResponseFile"
|
||||
|
||||
|
|
@ -698,6 +725,7 @@ func (a *Authenticator) createSession(userID string) (*Session, error) {
|
|||
|
||||
// StartCleanup runs a background goroutine that periodically removes expired
|
||||
// sessions from the store. It stops when the context is cancelled.
|
||||
// Usage: call StartCleanup(...) during the package's normal workflow.
|
||||
func (a *Authenticator) StartCleanup(ctx context.Context, interval time.Duration) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func newTestAuth(opts ...Option) (*Authenticator, *io.MockMedium) {
|
|||
|
||||
// --- Register ---
|
||||
|
||||
func TestRegister_Good(t *testing.T) {
|
||||
func TestAuth_Register_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
user, err := a.Register("alice", "hunter2")
|
||||
|
|
@ -48,7 +48,7 @@ func TestRegister_Good(t *testing.T) {
|
|||
assert.False(t, user.Created.IsZero())
|
||||
}
|
||||
|
||||
func TestRegister_Bad(t *testing.T) {
|
||||
func TestAuth_Register_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Register first time succeeds
|
||||
|
|
@ -61,7 +61,7 @@ func TestRegister_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "user already exists")
|
||||
}
|
||||
|
||||
func TestRegister_Ugly(t *testing.T) {
|
||||
func TestAuth_Register_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Empty username/password should still work (PGP allows it)
|
||||
|
|
@ -72,7 +72,7 @@ func TestRegister_Ugly(t *testing.T) {
|
|||
|
||||
// --- CreateChallenge ---
|
||||
|
||||
func TestCreateChallenge_Good(t *testing.T) {
|
||||
func TestAuth_CreateChallenge_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
user, err := a.Register("charlie", "pass")
|
||||
|
|
@ -87,7 +87,7 @@ func TestCreateChallenge_Good(t *testing.T) {
|
|||
assert.True(t, challenge.ExpiresAt.After(time.Now()))
|
||||
}
|
||||
|
||||
func TestCreateChallenge_Bad(t *testing.T) {
|
||||
func TestAuth_CreateChallenge_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Challenge for non-existent user
|
||||
|
|
@ -96,7 +96,7 @@ func TestCreateChallenge_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "user not found")
|
||||
}
|
||||
|
||||
func TestCreateChallenge_Ugly(t *testing.T) {
|
||||
func TestAuth_CreateChallenge_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Empty userID
|
||||
|
|
@ -106,7 +106,7 @@ func TestCreateChallenge_Ugly(t *testing.T) {
|
|||
|
||||
// --- ValidateResponse (full challenge-response flow) ---
|
||||
|
||||
func TestValidateResponse_Good(t *testing.T) {
|
||||
func TestAuth_ValidateResponse_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
// Register user
|
||||
|
|
@ -140,7 +140,7 @@ func TestValidateResponse_Good(t *testing.T) {
|
|||
assert.True(t, session.ExpiresAt.After(time.Now()))
|
||||
}
|
||||
|
||||
func TestValidateResponse_Bad(t *testing.T) {
|
||||
func TestAuth_ValidateResponse_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("eve", "pass")
|
||||
|
|
@ -153,7 +153,7 @@ func TestValidateResponse_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "no pending challenge")
|
||||
}
|
||||
|
||||
func TestValidateResponse_Ugly(t *testing.T) {
|
||||
func TestAuth_ValidateResponse_Ugly(t *testing.T) {
|
||||
a, m := newTestAuth(WithChallengeTTL(1 * time.Millisecond))
|
||||
|
||||
_, err := a.Register("frank", "pass")
|
||||
|
|
@ -180,7 +180,7 @@ func TestValidateResponse_Ugly(t *testing.T) {
|
|||
|
||||
// --- ValidateSession ---
|
||||
|
||||
func TestValidateSession_Good(t *testing.T) {
|
||||
func TestAuth_ValidateSession_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("grace", "pass")
|
||||
|
|
@ -196,7 +196,7 @@ func TestValidateSession_Good(t *testing.T) {
|
|||
assert.Equal(t, userID, validated.UserID)
|
||||
}
|
||||
|
||||
func TestValidateSession_Bad(t *testing.T) {
|
||||
func TestAuth_ValidateSession_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.ValidateSession("nonexistent-token")
|
||||
|
|
@ -204,7 +204,7 @@ func TestValidateSession_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "session not found")
|
||||
}
|
||||
|
||||
func TestValidateSession_Ugly(t *testing.T) {
|
||||
func TestAuth_ValidateSession_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond))
|
||||
|
||||
_, err := a.Register("heidi", "pass")
|
||||
|
|
@ -223,7 +223,7 @@ func TestValidateSession_Ugly(t *testing.T) {
|
|||
|
||||
// --- RefreshSession ---
|
||||
|
||||
func TestRefreshSession_Good(t *testing.T) {
|
||||
func TestAuth_RefreshSession_Good(t *testing.T) {
|
||||
a, _ := newTestAuth(WithSessionTTL(1 * time.Hour))
|
||||
|
||||
_, err := a.Register("ivan", "pass")
|
||||
|
|
@ -243,7 +243,7 @@ func TestRefreshSession_Good(t *testing.T) {
|
|||
assert.True(t, refreshed.ExpiresAt.After(originalExpiry))
|
||||
}
|
||||
|
||||
func TestRefreshSession_Bad(t *testing.T) {
|
||||
func TestAuth_RefreshSession_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.RefreshSession("nonexistent-token")
|
||||
|
|
@ -251,7 +251,7 @@ func TestRefreshSession_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "session not found")
|
||||
}
|
||||
|
||||
func TestRefreshSession_Ugly(t *testing.T) {
|
||||
func TestAuth_RefreshSession_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond))
|
||||
|
||||
_, err := a.Register("judy", "pass")
|
||||
|
|
@ -270,7 +270,7 @@ func TestRefreshSession_Ugly(t *testing.T) {
|
|||
|
||||
// --- RevokeSession ---
|
||||
|
||||
func TestRevokeSession_Good(t *testing.T) {
|
||||
func TestAuth_RevokeSession_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("karl", "pass")
|
||||
|
|
@ -288,7 +288,7 @@ func TestRevokeSession_Good(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRevokeSession_Bad(t *testing.T) {
|
||||
func TestAuth_RevokeSession_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
err := a.RevokeSession("nonexistent-token")
|
||||
|
|
@ -296,7 +296,7 @@ func TestRevokeSession_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "session not found")
|
||||
}
|
||||
|
||||
func TestRevokeSession_Ugly(t *testing.T) {
|
||||
func TestAuth_RevokeSession_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Revoke empty token
|
||||
|
|
@ -306,7 +306,7 @@ func TestRevokeSession_Ugly(t *testing.T) {
|
|||
|
||||
// --- DeleteUser ---
|
||||
|
||||
func TestDeleteUser_Good(t *testing.T) {
|
||||
func TestAuth_DeleteUser_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
_, err := a.Register("larry", "pass")
|
||||
|
|
@ -334,7 +334,7 @@ func TestDeleteUser_Good(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "session not found")
|
||||
}
|
||||
|
||||
func TestDeleteUser_Bad(t *testing.T) {
|
||||
func TestAuth_DeleteUser_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Protected user "server" cannot be deleted
|
||||
|
|
@ -343,7 +343,7 @@ func TestDeleteUser_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "cannot delete protected user")
|
||||
}
|
||||
|
||||
func TestDeleteUser_Ugly(t *testing.T) {
|
||||
func TestAuth_DeleteUser_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Non-existent user
|
||||
|
|
@ -354,7 +354,7 @@ func TestDeleteUser_Ugly(t *testing.T) {
|
|||
|
||||
// --- Login ---
|
||||
|
||||
func TestLogin_Good(t *testing.T) {
|
||||
func TestAuth_Login_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("mallory", "secret")
|
||||
|
|
@ -370,7 +370,7 @@ func TestLogin_Good(t *testing.T) {
|
|||
assert.True(t, session.ExpiresAt.After(time.Now()))
|
||||
}
|
||||
|
||||
func TestLogin_Bad(t *testing.T) {
|
||||
func TestAuth_Login_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("nancy", "correct-password")
|
||||
|
|
@ -383,7 +383,7 @@ func TestLogin_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "invalid password")
|
||||
}
|
||||
|
||||
func TestLogin_Ugly(t *testing.T) {
|
||||
func TestAuth_Login_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Login for non-existent user
|
||||
|
|
@ -394,7 +394,7 @@ func TestLogin_Ugly(t *testing.T) {
|
|||
|
||||
// --- WriteChallengeFile / ReadResponseFile (Air-Gapped) ---
|
||||
|
||||
func TestAirGappedFlow_Good(t *testing.T) {
|
||||
func TestAuth_AirGappedFlow_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
_, err := a.Register("oscar", "airgap-pass")
|
||||
|
|
@ -439,7 +439,7 @@ func TestAirGappedFlow_Good(t *testing.T) {
|
|||
assert.Equal(t, userID, session.UserID)
|
||||
}
|
||||
|
||||
func TestWriteChallengeFile_Bad(t *testing.T) {
|
||||
func TestAuth_WriteChallengeFile_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Challenge for non-existent user
|
||||
|
|
@ -447,7 +447,7 @@ func TestWriteChallengeFile_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadResponseFile_Bad(t *testing.T) {
|
||||
func TestAuth_ReadResponseFile_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Response file does not exist
|
||||
|
|
@ -455,7 +455,7 @@ func TestReadResponseFile_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadResponseFile_Ugly(t *testing.T) {
|
||||
func TestAuth_ReadResponseFile_Ugly(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
_, err := a.Register("peggy", "pass")
|
||||
|
|
@ -477,13 +477,13 @@ func TestReadResponseFile_Ugly(t *testing.T) {
|
|||
|
||||
// --- Options ---
|
||||
|
||||
func TestWithChallengeTTL_Good(t *testing.T) {
|
||||
func TestAuth_WithChallengeTTL_Good(t *testing.T) {
|
||||
ttl := 30 * time.Second
|
||||
a, _ := newTestAuth(WithChallengeTTL(ttl))
|
||||
assert.Equal(t, ttl, a.challengeTTL)
|
||||
}
|
||||
|
||||
func TestWithSessionTTL_Good(t *testing.T) {
|
||||
func TestAuth_WithSessionTTL_Good(t *testing.T) {
|
||||
ttl := 2 * time.Hour
|
||||
a, _ := newTestAuth(WithSessionTTL(ttl))
|
||||
assert.Equal(t, ttl, a.sessionTTL)
|
||||
|
|
@ -491,7 +491,7 @@ func TestWithSessionTTL_Good(t *testing.T) {
|
|||
|
||||
// --- Full Round-Trip (Online Flow) ---
|
||||
|
||||
func TestFullRoundTrip_Good(t *testing.T) {
|
||||
func TestAuth_FullRoundTrip_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
// 1. Register
|
||||
|
|
@ -541,7 +541,7 @@ func TestFullRoundTrip_Good(t *testing.T) {
|
|||
|
||||
// --- Concurrent Access ---
|
||||
|
||||
func TestConcurrentSessions_Good(t *testing.T) {
|
||||
func TestAuth_ConcurrentSessions_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("ruth", "pass")
|
||||
|
|
@ -579,9 +579,9 @@ func TestConcurrentSessions_Good(t *testing.T) {
|
|||
|
||||
// --- Phase 0 Additions ---
|
||||
|
||||
// TestConcurrentSessionCreation_Good verifies that 10 goroutines creating
|
||||
// TestAuth_ConcurrentSessionCreation_Good verifies that 10 goroutines creating
|
||||
// sessions simultaneously do not produce data races or errors.
|
||||
func TestConcurrentSessionCreation_Good(t *testing.T) {
|
||||
func TestAuth_ConcurrentSessionCreation_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Register 10 distinct users to avoid contention on a single user record
|
||||
|
|
@ -619,9 +619,9 @@ func TestConcurrentSessionCreation_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestSessionTokenUniqueness_Good generates 1000 session tokens and verifies
|
||||
// TestAuth_SessionTokenUniqueness_Good generates 1000 session tokens and verifies
|
||||
// no collisions without paying the full login hash-verification cost each time.
|
||||
func TestSessionTokenUniqueness_Good(t *testing.T) {
|
||||
func TestAuth_SessionTokenUniqueness_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("uniqueness-test", "pass")
|
||||
|
|
@ -645,9 +645,9 @@ func TestSessionTokenUniqueness_Good(t *testing.T) {
|
|||
assert.Len(t, tokens, n, "all 1000 tokens should be unique")
|
||||
}
|
||||
|
||||
// TestChallengeExpiryBoundary_Ugly tests validation right at the 5-minute boundary.
|
||||
// TestAuth_ChallengeExpiryBoundary_Ugly tests validation right at the 5-minute boundary.
|
||||
// The challenge should still be valid just before expiry and rejected after.
|
||||
func TestChallengeExpiryBoundary_Ugly(t *testing.T) {
|
||||
func TestAuth_ChallengeExpiryBoundary_Ugly(t *testing.T) {
|
||||
// Use a very short TTL to test the boundary without sleeping 5 minutes
|
||||
ttl := 50 * time.Millisecond
|
||||
a, m := newTestAuth(WithChallengeTTL(ttl))
|
||||
|
|
@ -691,9 +691,9 @@ func TestChallengeExpiryBoundary_Ugly(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "challenge expired")
|
||||
}
|
||||
|
||||
// TestEmptyPasswordRegistration_Good verifies that empty password registration works.
|
||||
// TestAuth_EmptyPasswordRegistration_Good verifies that empty password registration works.
|
||||
// PGP key is generated unencrypted in this case.
|
||||
func TestEmptyPasswordRegistration_Good(t *testing.T) {
|
||||
func TestAuth_EmptyPasswordRegistration_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
user, err := a.Register("no-password-user", "")
|
||||
|
|
@ -730,8 +730,8 @@ func TestEmptyPasswordRegistration_Good(t *testing.T) {
|
|||
assert.NotNil(t, crSession)
|
||||
}
|
||||
|
||||
// TestVeryLongUsername_Ugly verifies behaviour with a 10K character username.
|
||||
func TestVeryLongUsername_Ugly(t *testing.T) {
|
||||
// TestAuth_VeryLongUsername_Ugly verifies behaviour with a 10K character username.
|
||||
func TestAuth_VeryLongUsername_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
longName := core.NewBuilder()
|
||||
|
|
@ -753,8 +753,8 @@ func TestVeryLongUsername_Ugly(t *testing.T) {
|
|||
assert.NotNil(t, session)
|
||||
}
|
||||
|
||||
// TestUnicodeUsernamePassword_Good verifies registration and login with Unicode characters.
|
||||
func TestUnicodeUsernamePassword_Good(t *testing.T) {
|
||||
// TestAuth_UnicodeUsernamePassword_Good verifies registration and login with Unicode characters.
|
||||
func TestAuth_UnicodeUsernamePassword_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
// Japanese + emoji + Chinese + Arabic
|
||||
|
|
@ -777,9 +777,9 @@ func TestUnicodeUsernamePassword_Good(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestAirGappedRoundTrip_Good tests the full air-gapped flow:
|
||||
// TestAuth_AirGappedRoundTrip_Good tests the full air-gapped flow:
|
||||
// WriteChallengeFile -> client signs offline -> ReadResponseFile
|
||||
func TestAirGappedRoundTrip_Good(t *testing.T) {
|
||||
func TestAuth_AirGappedRoundTrip_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
_, err := a.Register("airgap-roundtrip", "courier-pass")
|
||||
|
|
@ -832,8 +832,8 @@ func TestAirGappedRoundTrip_Good(t *testing.T) {
|
|||
assert.Equal(t, session.Token, validated.Token)
|
||||
}
|
||||
|
||||
// TestRefreshExpiredSession_Bad verifies that refreshing an already-expired session fails.
|
||||
func TestRefreshExpiredSession_Bad(t *testing.T) {
|
||||
// TestAuth_RefreshExpiredSession_Bad verifies that refreshing an already-expired session fails.
|
||||
func TestAuth_RefreshExpiredSession_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond))
|
||||
|
||||
_, err := a.Register("expired-refresh", "pass")
|
||||
|
|
@ -859,8 +859,8 @@ func TestRefreshExpiredSession_Bad(t *testing.T) {
|
|||
|
||||
// --- Phase 2: Password Hash Migration ---
|
||||
|
||||
// TestRegisterArgon2id_Good verifies that new registrations use Argon2id format.
|
||||
func TestRegisterArgon2id_Good(t *testing.T) {
|
||||
// TestAuth_RegisterArgon2id_Good verifies that new registrations use Argon2id format.
|
||||
func TestAuth_RegisterArgon2id_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
user, err := a.Register("argon2-user", "strong-pass")
|
||||
|
|
@ -881,8 +881,8 @@ func TestRegisterArgon2id_Good(t *testing.T) {
|
|||
assert.True(t, core.HasPrefix(user.PasswordHash, "$argon2id$"))
|
||||
}
|
||||
|
||||
// TestLoginArgon2id_Good verifies login works with Argon2id hashed password.
|
||||
func TestLoginArgon2id_Good(t *testing.T) {
|
||||
// TestAuth_LoginArgon2id_Good verifies login works with Argon2id hashed password.
|
||||
func TestAuth_LoginArgon2id_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("login-argon2", "my-password")
|
||||
|
|
@ -895,8 +895,8 @@ func TestLoginArgon2id_Good(t *testing.T) {
|
|||
assert.NotEmpty(t, session.Token)
|
||||
}
|
||||
|
||||
// TestLoginArgon2id_Bad verifies wrong password fails with Argon2id hash.
|
||||
func TestLoginArgon2id_Bad(t *testing.T) {
|
||||
// TestAuth_LoginArgon2id_Bad verifies wrong password fails with Argon2id hash.
|
||||
func TestAuth_LoginArgon2id_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("login-argon2-bad", "correct")
|
||||
|
|
@ -908,9 +908,9 @@ func TestLoginArgon2id_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "invalid password")
|
||||
}
|
||||
|
||||
// TestLegacyLTHNMigration_Good verifies that a user registered with the legacy
|
||||
// TestAuth_LegacyLTHNMigration_Good verifies that a user registered with the legacy
|
||||
// LTHN hash format is transparently migrated to Argon2id on successful login.
|
||||
func TestLegacyLTHNMigration_Good(t *testing.T) {
|
||||
func TestAuth_LegacyLTHNMigration_Good(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
a := New(m)
|
||||
|
||||
|
|
@ -950,8 +950,8 @@ func TestLegacyLTHNMigration_Good(t *testing.T) {
|
|||
assert.NotEmpty(t, session2.Token)
|
||||
}
|
||||
|
||||
// TestLegacyLTHNLogin_Bad verifies wrong password fails for legacy LTHN users.
|
||||
func TestLegacyLTHNLogin_Bad(t *testing.T) {
|
||||
// TestAuth_LegacyLTHNLogin_Bad verifies wrong password fails for legacy LTHN users.
|
||||
func TestAuth_LegacyLTHNLogin_Bad(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
a := New(m)
|
||||
|
||||
|
|
@ -976,9 +976,9 @@ func TestLegacyLTHNLogin_Bad(t *testing.T) {
|
|||
|
||||
// --- Phase 2: Key Rotation ---
|
||||
|
||||
// TestRotateKeyPair_Good verifies the full key rotation flow:
|
||||
// TestAuth_RotateKeyPair_Good verifies the full key rotation flow:
|
||||
// register -> login -> rotate -> verify old key can't decrypt -> verify new key works -> sessions invalidated.
|
||||
func TestRotateKeyPair_Good(t *testing.T) {
|
||||
func TestAuth_RotateKeyPair_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
// Register and login
|
||||
|
|
@ -1032,8 +1032,8 @@ func TestRotateKeyPair_Good(t *testing.T) {
|
|||
assert.True(t, core.HasPrefix(meta.PasswordHash, "$argon2id$"))
|
||||
}
|
||||
|
||||
// TestRotateKeyPair_Bad verifies that rotation fails with wrong old password.
|
||||
func TestRotateKeyPair_Bad(t *testing.T) {
|
||||
// TestAuth_RotateKeyPair_Bad verifies that rotation fails with wrong old password.
|
||||
func TestAuth_RotateKeyPair_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("rotate-bad", "correct-pass")
|
||||
|
|
@ -1046,8 +1046,8 @@ func TestRotateKeyPair_Bad(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "failed to decrypt metadata")
|
||||
}
|
||||
|
||||
// TestRotateKeyPair_Ugly verifies rotation for non-existent user.
|
||||
func TestRotateKeyPair_Ugly(t *testing.T) {
|
||||
// TestAuth_RotateKeyPair_Ugly verifies rotation for non-existent user.
|
||||
func TestAuth_RotateKeyPair_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.RotateKeyPair("nonexistent-user-id", "old", "new")
|
||||
|
|
@ -1055,9 +1055,9 @@ func TestRotateKeyPair_Ugly(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "user not found")
|
||||
}
|
||||
|
||||
// TestRotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key
|
||||
// TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key
|
||||
// cannot decrypt metadata after rotation.
|
||||
func TestRotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) {
|
||||
func TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
_, err := a.Register("rotate-crypto", "pass-a")
|
||||
|
|
@ -1081,9 +1081,9 @@ func TestRotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) {
|
|||
|
||||
// --- Phase 2: Key Revocation ---
|
||||
|
||||
// TestRevokeKey_Good verifies the full revocation flow:
|
||||
// TestAuth_RevokeKey_Good verifies the full revocation flow:
|
||||
// register -> login -> revoke -> login fails -> challenge fails -> sessions invalidated.
|
||||
func TestRevokeKey_Good(t *testing.T) {
|
||||
func TestAuth_RevokeKey_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
_, err := a.Register("revoke-user", "pass")
|
||||
|
|
@ -1131,8 +1131,8 @@ func TestRevokeKey_Good(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestRevokeKey_Bad verifies revocation fails with wrong password.
|
||||
func TestRevokeKey_Bad(t *testing.T) {
|
||||
// TestAuth_RevokeKey_Bad verifies revocation fails with wrong password.
|
||||
func TestAuth_RevokeKey_Bad(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
_, err := a.Register("revoke-bad", "correct")
|
||||
|
|
@ -1147,8 +1147,8 @@ func TestRevokeKey_Bad(t *testing.T) {
|
|||
assert.False(t, a.IsRevoked(userID))
|
||||
}
|
||||
|
||||
// TestRevokeKey_Ugly verifies revocation for non-existent user.
|
||||
func TestRevokeKey_Ugly(t *testing.T) {
|
||||
// TestAuth_RevokeKey_Ugly verifies revocation for non-existent user.
|
||||
func TestAuth_RevokeKey_Ugly(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
err := a.RevokeKey("nonexistent-user-id", "pass", "reason")
|
||||
|
|
@ -1156,9 +1156,9 @@ func TestRevokeKey_Ugly(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "user not found")
|
||||
}
|
||||
|
||||
// TestIsRevoked_Placeholder_Good verifies that the legacy placeholder is not
|
||||
// TestAuth_IsRevoked_Placeholder_Good verifies that the legacy placeholder is not
|
||||
// treated as a valid revocation.
|
||||
func TestIsRevoked_Placeholder_Good(t *testing.T) {
|
||||
func TestAuth_IsRevoked_Placeholder_Good(t *testing.T) {
|
||||
a, m := newTestAuth()
|
||||
|
||||
_, err := a.Register("placeholder-user", "pass")
|
||||
|
|
@ -1174,16 +1174,16 @@ func TestIsRevoked_Placeholder_Good(t *testing.T) {
|
|||
assert.False(t, a.IsRevoked(userID))
|
||||
}
|
||||
|
||||
// TestIsRevoked_NoRevFile_Good verifies that a missing .rev file returns false.
|
||||
func TestIsRevoked_NoRevFile_Good(t *testing.T) {
|
||||
// TestAuth_IsRevoked_NoRevFile_Good verifies that a missing .rev file returns false.
|
||||
func TestAuth_IsRevoked_NoRevFile_Good(t *testing.T) {
|
||||
a, _ := newTestAuth()
|
||||
|
||||
assert.False(t, a.IsRevoked("completely-nonexistent"))
|
||||
}
|
||||
|
||||
// TestRevokeKey_LegacyUser_Good verifies revocation works for a legacy user
|
||||
// TestAuth_RevokeKey_LegacyUser_Good verifies revocation works for a legacy user
|
||||
// with only a .lthn hash file (no .hash file).
|
||||
func TestRevokeKey_LegacyUser_Good(t *testing.T) {
|
||||
func TestAuth_RevokeKey_LegacyUser_Good(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
a := New(m)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package auth
|
|||
// similar tamper-resistant devices.
|
||||
//
|
||||
// All methods must be safe for concurrent use.
|
||||
// Usage: implement HardwareKey and pass it to WithHardwareKey(...) to wire hardware-backed auth into New(...).
|
||||
type HardwareKey interface {
|
||||
// Sign produces a cryptographic signature over the given data using the
|
||||
// hardware-stored private key. The signature format depends on the
|
||||
|
|
@ -44,6 +45,7 @@ type HardwareKey interface {
|
|||
//
|
||||
// This is a forward-looking option — integration points are documented in
|
||||
// auth.go but not yet wired up.
|
||||
// Usage: pass WithHardwareKey(...) into New(...) to enable a HardwareKey implementation.
|
||||
func WithHardwareKey(hk HardwareKey) Option {
|
||||
return func(a *Authenticator) {
|
||||
a.hardwareKey = hk
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ import (
|
|||
)
|
||||
|
||||
// ErrSessionNotFound is returned when a session token is not found.
|
||||
// Usage: compare returned errors against ErrSessionNotFound when branching on failures.
|
||||
var ErrSessionNotFound = coreerr.E("auth", "session not found", nil)
|
||||
|
||||
// SessionStore abstracts session persistence.
|
||||
// Usage: use SessionStore with the other exported helpers in this package.
|
||||
type SessionStore interface {
|
||||
Get(token string) (*Session, error)
|
||||
Set(session *Session) error
|
||||
|
|
@ -21,12 +23,14 @@ type SessionStore interface {
|
|||
}
|
||||
|
||||
// MemorySessionStore is an in-memory SessionStore backed by a map.
|
||||
// Usage: use MemorySessionStore with the other exported helpers in this package.
|
||||
type MemorySessionStore struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*Session
|
||||
}
|
||||
|
||||
// NewMemorySessionStore creates a new in-memory session store.
|
||||
// Usage: call NewMemorySessionStore(...) to create a ready-to-use value.
|
||||
func NewMemorySessionStore() *MemorySessionStore {
|
||||
return &MemorySessionStore{
|
||||
sessions: make(map[string]*Session),
|
||||
|
|
@ -34,6 +38,7 @@ func NewMemorySessionStore() *MemorySessionStore {
|
|||
}
|
||||
|
||||
// Get retrieves a session by token.
|
||||
// Usage: call Get(...) during the package's normal workflow.
|
||||
func (m *MemorySessionStore) Get(token string) (*Session, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
|
@ -49,6 +54,7 @@ func (m *MemorySessionStore) Get(token string) (*Session, error) {
|
|||
}
|
||||
|
||||
// Set stores a session, keyed by its token.
|
||||
// Usage: call Set(...) during the package's normal workflow.
|
||||
func (m *MemorySessionStore) Set(session *Session) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
@ -60,6 +66,7 @@ func (m *MemorySessionStore) Set(session *Session) error {
|
|||
}
|
||||
|
||||
// Delete removes a session by token.
|
||||
// Usage: call Delete(...) during the package's normal workflow.
|
||||
func (m *MemorySessionStore) Delete(token string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
@ -73,6 +80,7 @@ func (m *MemorySessionStore) Delete(token string) error {
|
|||
}
|
||||
|
||||
// DeleteByUser removes all sessions belonging to the given user.
|
||||
// Usage: call DeleteByUser(...) during the package's normal workflow.
|
||||
func (m *MemorySessionStore) DeleteByUser(userID string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
@ -84,6 +92,7 @@ func (m *MemorySessionStore) DeleteByUser(userID string) error {
|
|||
}
|
||||
|
||||
// Cleanup removes all expired sessions and returns the count removed.
|
||||
// Usage: call Cleanup(...) during the package's normal workflow.
|
||||
func (m *MemorySessionStore) Cleanup() (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const sessionGroup = "sessions"
|
|||
|
||||
// SQLiteSessionStore is a SessionStore backed by core/store (SQLite KV).
|
||||
// A mutex serialises all operations because SQLite is single-writer.
|
||||
// Usage: use SQLiteSessionStore with the other exported helpers in this package.
|
||||
type SQLiteSessionStore struct {
|
||||
mu sync.Mutex
|
||||
store *store.Store
|
||||
|
|
@ -19,6 +20,7 @@ type SQLiteSessionStore struct {
|
|||
|
||||
// NewSQLiteSessionStore creates a new SQLite-backed session store.
|
||||
// Use ":memory:" for testing or a file path for persistent storage.
|
||||
// Usage: call NewSQLiteSessionStore(...) to create a ready-to-use value.
|
||||
func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) {
|
||||
s, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
|
|
@ -28,6 +30,7 @@ func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) {
|
|||
}
|
||||
|
||||
// Get retrieves a session by token from SQLite.
|
||||
// Usage: call Get(...) during the package's normal workflow.
|
||||
func (s *SQLiteSessionStore) Get(token string) (*Session, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -50,6 +53,7 @@ func (s *SQLiteSessionStore) Get(token string) (*Session, error) {
|
|||
}
|
||||
|
||||
// Set stores a session in SQLite, keyed by its token.
|
||||
// Usage: call Set(...) during the package's normal workflow.
|
||||
func (s *SQLiteSessionStore) Set(session *Session) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -63,6 +67,7 @@ func (s *SQLiteSessionStore) Set(session *Session) error {
|
|||
}
|
||||
|
||||
// Delete removes a session by token from SQLite.
|
||||
// Usage: call Delete(...) during the package's normal workflow.
|
||||
func (s *SQLiteSessionStore) Delete(token string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -79,6 +84,7 @@ func (s *SQLiteSessionStore) Delete(token string) error {
|
|||
}
|
||||
|
||||
// DeleteByUser removes all sessions belonging to the given user.
|
||||
// Usage: call DeleteByUser(...) during the package's normal workflow.
|
||||
func (s *SQLiteSessionStore) DeleteByUser(userID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -104,6 +110,7 @@ func (s *SQLiteSessionStore) DeleteByUser(userID string) error {
|
|||
}
|
||||
|
||||
// Cleanup removes all expired sessions and returns the count removed.
|
||||
// Usage: call Cleanup(...) during the package's normal workflow.
|
||||
func (s *SQLiteSessionStore) Cleanup() (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -132,6 +139,7 @@ func (s *SQLiteSessionStore) Cleanup() (int, error) {
|
|||
}
|
||||
|
||||
// Close closes the underlying SQLite store.
|
||||
// Usage: call Close(...) during the package's normal workflow.
|
||||
func (s *SQLiteSessionStore) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import (
|
|||
|
||||
// --- MemorySessionStore ---
|
||||
|
||||
func TestMemorySessionStore_GetSetDelete_Good(t *testing.T) {
|
||||
func TestSessionStore_MemorySessionStore_GetSetDelete_Good(t *testing.T) {
|
||||
store := NewMemorySessionStore()
|
||||
|
||||
session := &Session{
|
||||
|
|
@ -44,21 +44,21 @@ func TestMemorySessionStore_GetSetDelete_Good(t *testing.T) {
|
|||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||||
}
|
||||
|
||||
func TestMemorySessionStore_GetNotFound_Bad(t *testing.T) {
|
||||
func TestSessionStore_MemorySessionStore_GetNotFound_Bad(t *testing.T) {
|
||||
store := NewMemorySessionStore()
|
||||
|
||||
_, err := store.Get("nonexistent-token")
|
||||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||||
}
|
||||
|
||||
func TestMemorySessionStore_DeleteNotFound_Bad(t *testing.T) {
|
||||
func TestSessionStore_MemorySessionStore_DeleteNotFound_Bad(t *testing.T) {
|
||||
store := NewMemorySessionStore()
|
||||
|
||||
err := store.Delete("nonexistent-token")
|
||||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||||
}
|
||||
|
||||
func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) {
|
||||
func TestSessionStore_MemorySessionStore_DeleteByUser_Good(t *testing.T) {
|
||||
store := NewMemorySessionStore()
|
||||
|
||||
// Create sessions for two users
|
||||
|
|
@ -94,7 +94,7 @@ func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) {
|
|||
assert.Equal(t, "user-b", got.UserID)
|
||||
}
|
||||
|
||||
func TestMemorySessionStore_Cleanup_Good(t *testing.T) {
|
||||
func TestSessionStore_MemorySessionStore_Cleanup_Good(t *testing.T) {
|
||||
store := NewMemorySessionStore()
|
||||
|
||||
// Create expired and valid sessions
|
||||
|
|
@ -134,7 +134,7 @@ func TestMemorySessionStore_Cleanup_Good(t *testing.T) {
|
|||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||||
}
|
||||
|
||||
func TestMemorySessionStore_Concurrent_Good(t *testing.T) {
|
||||
func TestSessionStore_MemorySessionStore_Concurrent_Good(t *testing.T) {
|
||||
store := NewMemorySessionStore()
|
||||
|
||||
const n = 20
|
||||
|
|
@ -164,7 +164,7 @@ func TestMemorySessionStore_Concurrent_Good(t *testing.T) {
|
|||
|
||||
// --- SQLiteSessionStore ---
|
||||
|
||||
func TestSQLiteSessionStore_GetSetDelete_Good(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_GetSetDelete_Good(t *testing.T) {
|
||||
store, err := NewSQLiteSessionStore(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer store.Close()
|
||||
|
|
@ -194,7 +194,7 @@ func TestSQLiteSessionStore_GetSetDelete_Good(t *testing.T) {
|
|||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||||
}
|
||||
|
||||
func TestSQLiteSessionStore_GetNotFound_Bad(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_GetNotFound_Bad(t *testing.T) {
|
||||
store, err := NewSQLiteSessionStore(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer store.Close()
|
||||
|
|
@ -203,7 +203,7 @@ func TestSQLiteSessionStore_GetNotFound_Bad(t *testing.T) {
|
|||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||||
}
|
||||
|
||||
func TestSQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) {
|
||||
store, err := NewSQLiteSessionStore(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer store.Close()
|
||||
|
|
@ -212,7 +212,7 @@ func TestSQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) {
|
|||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||||
}
|
||||
|
||||
func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_DeleteByUser_Good(t *testing.T) {
|
||||
store, err := NewSQLiteSessionStore(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer store.Close()
|
||||
|
|
@ -250,7 +250,7 @@ func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) {
|
|||
assert.Equal(t, "user-b", got.UserID)
|
||||
}
|
||||
|
||||
func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_Cleanup_Good(t *testing.T) {
|
||||
store, err := NewSQLiteSessionStore(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer store.Close()
|
||||
|
|
@ -292,7 +292,7 @@ func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) {
|
|||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||||
}
|
||||
|
||||
func TestSQLiteSessionStore_Persistence_Good(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_Persistence_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dbPath := core.Path(dir, "sessions.db")
|
||||
|
||||
|
|
@ -323,7 +323,7 @@ func TestSQLiteSessionStore_Persistence_Good(t *testing.T) {
|
|||
assert.Equal(t, "persist-token", got.Token)
|
||||
}
|
||||
|
||||
func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_Concurrent_Good(t *testing.T) {
|
||||
// Use a temp file — :memory: SQLite has concurrency limitations
|
||||
dbPath := core.Path(t.TempDir(), "concurrent.db")
|
||||
store, err := NewSQLiteSessionStore(dbPath)
|
||||
|
|
@ -359,7 +359,7 @@ func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) {
|
|||
|
||||
// --- Authenticator with SessionStore ---
|
||||
|
||||
func TestAuthenticator_WithSessionStore_Good(t *testing.T) {
|
||||
func TestSessionStore_Authenticator_WithSessionStore_Good(t *testing.T) {
|
||||
sqliteStore, err := NewSQLiteSessionStore(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer sqliteStore.Close()
|
||||
|
|
@ -398,7 +398,7 @@ func TestAuthenticator_WithSessionStore_Good(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "session not found")
|
||||
}
|
||||
|
||||
func TestAuthenticator_DefaultStore_Good(t *testing.T) {
|
||||
func TestSessionStore_Authenticator_DefaultStore_Good(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
a := New(m)
|
||||
|
||||
|
|
@ -407,7 +407,7 @@ func TestAuthenticator_DefaultStore_Good(t *testing.T) {
|
|||
assert.True(t, ok, "default store should be MemorySessionStore")
|
||||
}
|
||||
|
||||
func TestAuthenticator_StartCleanup_Good(t *testing.T) {
|
||||
func TestSessionStore_Authenticator_StartCleanup_Good(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
a := New(m, WithSessionTTL(1*time.Millisecond))
|
||||
|
||||
|
|
@ -436,7 +436,7 @@ func TestAuthenticator_StartCleanup_Good(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "session not found")
|
||||
}
|
||||
|
||||
func TestAuthenticator_StartCleanup_CancelStops_Good(t *testing.T) {
|
||||
func TestSessionStore_Authenticator_StartCleanup_CancelStops_Good(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
a := New(m)
|
||||
|
||||
|
|
@ -448,7 +448,7 @@ func TestAuthenticator_StartCleanup_CancelStops_Good(t *testing.T) {
|
|||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_UpdateExisting_Good(t *testing.T) {
|
||||
store, err := NewSQLiteSessionStore(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer store.Close()
|
||||
|
|
@ -476,7 +476,7 @@ func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) {
|
|||
"updated session should have later expiry")
|
||||
}
|
||||
|
||||
func TestSQLiteSessionStore_TempFile_Good(t *testing.T) {
|
||||
func TestSessionStore_SQLiteSessionStore_TempFile_Good(t *testing.T) {
|
||||
// Verify we can use a real temp file (not :memory:)
|
||||
tmpFile := core.Path(t.TempDir(), "go-crypt-test-session-store.db")
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ func init() {
|
|||
}
|
||||
|
||||
// AddCryptCommands registers the 'crypt' command group and all subcommands.
|
||||
// Usage: call AddCryptCommands(...) during the package's normal workflow.
|
||||
func AddCryptCommands(root *cli.Command) {
|
||||
cryptCmd := &cli.Command{
|
||||
Use: "crypt",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ func initTestFlags() {
|
|||
}
|
||||
|
||||
// AddTestCommands registers the 'test' command and all subcommands.
|
||||
// Usage: call AddTestCommands(...) during the package's normal workflow.
|
||||
func AddTestCommands(root *cli.Command) {
|
||||
initTestFlags()
|
||||
root.AddCommand(testCmd)
|
||||
|
|
|
|||
|
|
@ -6,19 +6,19 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShortenPackageName_Good(t *testing.T) {
|
||||
func TestOutput_ShortenPackageName_Good(t *testing.T) {
|
||||
assert.Equal(t, "pkg/foo", shortenPackageName("dappco.re/go/core/pkg/foo"))
|
||||
assert.Equal(t, "cli-php", shortenPackageName("example.com/org/cli-php"))
|
||||
assert.Equal(t, "bar", shortenPackageName("github.com/other/bar"))
|
||||
}
|
||||
|
||||
func TestFormatCoverage_Good(t *testing.T) {
|
||||
func TestOutput_FormatCoverage_Good(t *testing.T) {
|
||||
assert.Contains(t, formatCoverage(85.0), "85.0%")
|
||||
assert.Contains(t, formatCoverage(65.0), "65.0%")
|
||||
assert.Contains(t, formatCoverage(25.0), "25.0%")
|
||||
}
|
||||
|
||||
func TestParseTestOutput_Good(t *testing.T) {
|
||||
func TestOutput_ParseTestOutput_Good(t *testing.T) {
|
||||
output := `ok dappco.re/go/core/pkg/foo 0.100s coverage: 50.0% of statements
|
||||
FAIL dappco.re/go/core/pkg/bar
|
||||
? dappco.re/go/core/pkg/baz [no test files]
|
||||
|
|
@ -33,7 +33,7 @@ FAIL dappco.re/go/core/pkg/bar
|
|||
assert.Equal(t, 50.0, results.packages[0].coverage)
|
||||
}
|
||||
|
||||
func TestPrintCoverageSummary_Good_LongPackageNames(t *testing.T) {
|
||||
func TestOutput_PrintCoverageSummary_Good_LongPackageNames(t *testing.T) {
|
||||
// This tests the bug fix for long package names causing negative Repeat count
|
||||
results := testResults{
|
||||
packages: []packageCoverage{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
// Encrypt encrypts data using ChaCha20-Poly1305.
|
||||
// Usage: call Encrypt(...) during the package's normal workflow.
|
||||
func Encrypt(plaintext []byte, key []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
|
|
@ -26,6 +27,7 @@ func Encrypt(plaintext []byte, key []byte) ([]byte, error) {
|
|||
}
|
||||
|
||||
// Decrypt decrypts data using ChaCha20-Poly1305.
|
||||
// Usage: call Decrypt(...) during the package's normal workflow.
|
||||
func Decrypt(ciphertext []byte, key []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) {
|
|||
return 0, core.NewError("read error")
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt_Good(t *testing.T) {
|
||||
func TestChachapoly_EncryptDecrypt_Good(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = 1
|
||||
|
|
@ -31,14 +31,14 @@ func TestEncryptDecrypt_Good(t *testing.T) {
|
|||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestEncrypt_Bad_InvalidKeySize(t *testing.T) {
|
||||
func TestChachapoly_Encrypt_Bad_InvalidKeySize(t *testing.T) {
|
||||
key := make([]byte, 16) // Wrong size
|
||||
plaintext := []byte("test")
|
||||
_, err := Encrypt(plaintext, key)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDecrypt_Bad_WrongKey(t *testing.T) {
|
||||
func TestChachapoly_Decrypt_Bad_WrongKey(t *testing.T) {
|
||||
key1 := make([]byte, 32)
|
||||
key2 := make([]byte, 32)
|
||||
key2[0] = 1 // Different key
|
||||
|
|
@ -51,7 +51,7 @@ func TestDecrypt_Bad_WrongKey(t *testing.T) {
|
|||
assert.Error(t, err) // Should fail authentication
|
||||
}
|
||||
|
||||
func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) {
|
||||
func TestChachapoly_Decrypt_Bad_TamperedCiphertext(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
plaintext := []byte("secret")
|
||||
ciphertext, err := Encrypt(plaintext, key)
|
||||
|
|
@ -64,7 +64,7 @@ func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEncrypt_Good_EmptyPlaintext(t *testing.T) {
|
||||
func TestChachapoly_Encrypt_Good_EmptyPlaintext(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
plaintext := []byte("")
|
||||
ciphertext, err := Encrypt(plaintext, key)
|
||||
|
|
@ -76,7 +76,7 @@ func TestEncrypt_Good_EmptyPlaintext(t *testing.T) {
|
|||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestDecrypt_Bad_ShortCiphertext(t *testing.T) {
|
||||
func TestChachapoly_Decrypt_Bad_ShortCiphertext(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
shortCiphertext := []byte("short")
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ func TestDecrypt_Bad_ShortCiphertext(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "too short")
|
||||
}
|
||||
|
||||
func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) {
|
||||
func TestChachapoly_CiphertextDiffersFromPlaintext_Good(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
plaintext := []byte("Hello, world!")
|
||||
ciphertext, err := Encrypt(plaintext, key)
|
||||
|
|
@ -93,7 +93,7 @@ func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) {
|
|||
assert.NotEqual(t, plaintext, ciphertext)
|
||||
}
|
||||
|
||||
func TestEncrypt_Bad_NonceError(t *testing.T) {
|
||||
func TestChachapoly_Encrypt_Bad_NonceError(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
plaintext := []byte("test")
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ func TestEncrypt_Bad_NonceError(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDecrypt_Bad_InvalidKeySize(t *testing.T) {
|
||||
func TestChachapoly_Decrypt_Bad_InvalidKeySize(t *testing.T) {
|
||||
key := make([]byte, 16) // Wrong size
|
||||
ciphertext := []byte("test")
|
||||
_, err := Decrypt(ciphertext, key)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
// SHA256File computes the SHA-256 checksum of a file and returns it as a hex string.
|
||||
// Usage: call SHA256File(...) during the package's normal workflow.
|
||||
func SHA256File(path string) (string, error) {
|
||||
openResult := (&core.Fs{}).New("/").Open(path)
|
||||
if !openResult.OK {
|
||||
|
|
@ -29,6 +30,7 @@ func SHA256File(path string) (string, error) {
|
|||
}
|
||||
|
||||
// SHA512File computes the SHA-512 checksum of a file and returns it as a hex string.
|
||||
// Usage: call SHA512File(...) during the package's normal workflow.
|
||||
func SHA512File(path string) (string, error) {
|
||||
openResult := (&core.Fs{}).New("/").Open(path)
|
||||
if !openResult.OK {
|
||||
|
|
@ -47,12 +49,14 @@ func SHA512File(path string) (string, error) {
|
|||
}
|
||||
|
||||
// SHA256Sum computes the SHA-256 checksum of data and returns it as a hex string.
|
||||
// Usage: call SHA256Sum(...) during the package's normal workflow.
|
||||
func SHA256Sum(data []byte) string {
|
||||
h := sha256.Sum256(data)
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// SHA512Sum computes the SHA-512 checksum of data and returns it as a hex string.
|
||||
// Usage: call SHA512Sum(...) during the package's normal workflow.
|
||||
func SHA512Sum(data []byte) string {
|
||||
h := sha512.Sum512(data)
|
||||
return hex.EncodeToString(h[:])
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSHA256Sum_Good(t *testing.T) {
|
||||
func TestChecksum_SHA256Sum_Good(t *testing.T) {
|
||||
data := []byte("hello")
|
||||
expected := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ func TestSHA256Sum_Good(t *testing.T) {
|
|||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestSHA512Sum_Good(t *testing.T) {
|
||||
func TestChecksum_SHA512Sum_Good(t *testing.T) {
|
||||
data := []byte("hello")
|
||||
expected := "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
|
||||
|
||||
|
|
@ -26,8 +26,8 @@ func TestSHA512Sum_Good(t *testing.T) {
|
|||
|
||||
// --- Phase 0 Additions ---
|
||||
|
||||
// TestSHA256FileEmpty_Good verifies checksum of an empty file.
|
||||
func TestSHA256FileEmpty_Good(t *testing.T) {
|
||||
// TestChecksum_SHA256FileEmpty_Good verifies checksum of an empty file.
|
||||
func TestChecksum_SHA256FileEmpty_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
emptyFile := core.Path(tmpDir, "empty.bin")
|
||||
writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644)
|
||||
|
|
@ -39,8 +39,8 @@ func TestSHA256FileEmpty_Good(t *testing.T) {
|
|||
assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash)
|
||||
}
|
||||
|
||||
// TestSHA512FileEmpty_Good verifies SHA-512 checksum of an empty file.
|
||||
func TestSHA512FileEmpty_Good(t *testing.T) {
|
||||
// TestChecksum_SHA512FileEmpty_Good verifies SHA-512 checksum of an empty file.
|
||||
func TestChecksum_SHA512FileEmpty_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
emptyFile := core.Path(tmpDir, "empty.bin")
|
||||
writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644)
|
||||
|
|
@ -51,22 +51,22 @@ func TestSHA512FileEmpty_Good(t *testing.T) {
|
|||
assert.Equal(t, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", hash)
|
||||
}
|
||||
|
||||
// TestSHA256FileNonExistent_Bad verifies error on non-existent file.
|
||||
func TestSHA256FileNonExistent_Bad(t *testing.T) {
|
||||
// TestChecksum_SHA256FileNonExistent_Bad verifies error on non-existent file.
|
||||
func TestChecksum_SHA256FileNonExistent_Bad(t *testing.T) {
|
||||
_, err := SHA256File("/nonexistent/path/to/file.bin")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to open file")
|
||||
}
|
||||
|
||||
// TestSHA512FileNonExistent_Bad verifies error on non-existent file.
|
||||
func TestSHA512FileNonExistent_Bad(t *testing.T) {
|
||||
// TestChecksum_SHA512FileNonExistent_Bad verifies error on non-existent file.
|
||||
func TestChecksum_SHA512FileNonExistent_Bad(t *testing.T) {
|
||||
_, err := SHA512File("/nonexistent/path/to/file.bin")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to open file")
|
||||
}
|
||||
|
||||
// TestSHA256FileWithContent_Good verifies checksum of a file with known content.
|
||||
func TestSHA256FileWithContent_Good(t *testing.T) {
|
||||
// TestChecksum_SHA256FileWithContent_Good verifies checksum of a file with known content.
|
||||
func TestChecksum_SHA256FileWithContent_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := core.Path(tmpDir, "test.txt")
|
||||
writeResult := (&core.Fs{}).New("/").WriteMode(testFile, "hello", 0o644)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
// Encrypt encrypts data with a passphrase using ChaCha20-Poly1305.
|
||||
// A random salt is generated and prepended to the output.
|
||||
// Format: salt (16 bytes) + nonce (24 bytes) + ciphertext.
|
||||
// Usage: call Encrypt(...) during the package's normal workflow.
|
||||
func Encrypt(plaintext, passphrase []byte) ([]byte, error) {
|
||||
salt, err := generateSalt(argon2SaltLen)
|
||||
if err != nil {
|
||||
|
|
@ -29,6 +30,7 @@ func Encrypt(plaintext, passphrase []byte) ([]byte, error) {
|
|||
|
||||
// Decrypt decrypts data encrypted with Encrypt.
|
||||
// Expects format: salt (16 bytes) + nonce (24 bytes) + ciphertext.
|
||||
// Usage: call Decrypt(...) during the package's normal workflow.
|
||||
func Decrypt(ciphertext, passphrase []byte) ([]byte, error) {
|
||||
if len(ciphertext) < argon2SaltLen {
|
||||
return nil, coreerr.E("crypt.Decrypt", "ciphertext too short", nil)
|
||||
|
|
@ -50,6 +52,7 @@ func Decrypt(ciphertext, passphrase []byte) ([]byte, error) {
|
|||
// EncryptAES encrypts data using AES-256-GCM with a passphrase.
|
||||
// A random salt is generated and prepended to the output.
|
||||
// Format: salt (16 bytes) + nonce (12 bytes) + ciphertext.
|
||||
// Usage: call EncryptAES(...) during the package's normal workflow.
|
||||
func EncryptAES(plaintext, passphrase []byte) ([]byte, error) {
|
||||
salt, err := generateSalt(argon2SaltLen)
|
||||
if err != nil {
|
||||
|
|
@ -71,6 +74,7 @@ func EncryptAES(plaintext, passphrase []byte) ([]byte, error) {
|
|||
|
||||
// DecryptAES decrypts data encrypted with EncryptAES.
|
||||
// Expects format: salt (16 bytes) + nonce (12 bytes) + ciphertext.
|
||||
// Usage: call DecryptAES(...) during the package's normal workflow.
|
||||
func DecryptAES(ciphertext, passphrase []byte) ([]byte, error) {
|
||||
if len(ciphertext) < argon2SaltLen {
|
||||
return nil, coreerr.E("crypt.DecryptAES", "ciphertext too short", nil)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncryptDecrypt_Good(t *testing.T) {
|
||||
func TestCrypt_EncryptDecrypt_Good(t *testing.T) {
|
||||
plaintext := []byte("hello, world!")
|
||||
passphrase := []byte("correct-horse-battery-staple")
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ func TestEncryptDecrypt_Good(t *testing.T) {
|
|||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt_Bad(t *testing.T) {
|
||||
func TestCrypt_EncryptDecrypt_Bad(t *testing.T) {
|
||||
plaintext := []byte("secret data")
|
||||
passphrase := []byte("correct-passphrase")
|
||||
wrongPassphrase := []byte("wrong-passphrase")
|
||||
|
|
@ -33,7 +33,7 @@ func TestEncryptDecrypt_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEncryptDecryptAES_Good(t *testing.T) {
|
||||
func TestCrypt_EncryptDecryptAES_Good(t *testing.T) {
|
||||
plaintext := []byte("hello, AES world!")
|
||||
passphrase := []byte("my-secure-passphrase")
|
||||
|
||||
|
|
@ -48,8 +48,8 @@ func TestEncryptDecryptAES_Good(t *testing.T) {
|
|||
|
||||
// --- Phase 0 Additions ---
|
||||
|
||||
// TestWrongPassphraseDecrypt_Bad verifies wrong passphrase returns error, not corrupt data.
|
||||
func TestWrongPassphraseDecrypt_Bad(t *testing.T) {
|
||||
// TestCrypt_WrongPassphraseDecrypt_Bad verifies wrong passphrase returns error, not corrupt data.
|
||||
func TestCrypt_WrongPassphraseDecrypt_Bad(t *testing.T) {
|
||||
plaintext := []byte("sensitive payload")
|
||||
passphrase := []byte("correct-passphrase")
|
||||
wrongPassphrase := []byte("wrong-passphrase")
|
||||
|
|
@ -70,8 +70,8 @@ func TestWrongPassphraseDecrypt_Bad(t *testing.T) {
|
|||
assert.Nil(t, decryptedAES, "wrong passphrase must not return partial data (AES)")
|
||||
}
|
||||
|
||||
// TestEmptyPlaintextRoundTrip_Good verifies encrypt/decrypt of empty plaintext.
|
||||
func TestEmptyPlaintextRoundTrip_Good(t *testing.T) {
|
||||
// TestCrypt_EmptyPlaintextRoundTrip_Good verifies encrypt/decrypt of empty plaintext.
|
||||
func TestCrypt_EmptyPlaintextRoundTrip_Good(t *testing.T) {
|
||||
passphrase := []byte("test-passphrase")
|
||||
|
||||
// ChaCha20
|
||||
|
|
@ -93,8 +93,8 @@ func TestEmptyPlaintextRoundTrip_Good(t *testing.T) {
|
|||
assert.Empty(t, decryptedAES)
|
||||
}
|
||||
|
||||
// TestLargePlaintextRoundTrip_Good verifies encrypt/decrypt of a 1MB payload.
|
||||
func TestLargePlaintextRoundTrip_Good(t *testing.T) {
|
||||
// TestCrypt_LargePlaintextRoundTrip_Good verifies encrypt/decrypt of a 1MB payload.
|
||||
func TestCrypt_LargePlaintextRoundTrip_Good(t *testing.T) {
|
||||
passphrase := []byte("large-payload-passphrase")
|
||||
plaintext := bytes.Repeat([]byte("X"), 1024*1024) // 1MB
|
||||
|
||||
|
|
@ -116,8 +116,8 @@ func TestLargePlaintextRoundTrip_Good(t *testing.T) {
|
|||
assert.Equal(t, plaintext, decryptedAES)
|
||||
}
|
||||
|
||||
// TestDecryptCiphertextTooShort_Ugly verifies short ciphertext is rejected.
|
||||
func TestDecryptCiphertextTooShort_Ugly(t *testing.T) {
|
||||
// TestCrypt_DecryptCiphertextTooShort_Ugly verifies short ciphertext is rejected.
|
||||
func TestCrypt_DecryptCiphertextTooShort_Ugly(t *testing.T) {
|
||||
_, err := Decrypt([]byte("short"), []byte("pass"))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "too short")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
// HashPassword hashes a password using Argon2id with default parameters.
|
||||
// Returns a string in the format: $argon2id$v=19$m=65536,t=3,p=4$<base64salt>$<base64hash>
|
||||
// Usage: call HashPassword(...) during the package's normal workflow.
|
||||
func HashPassword(password string) (string, error) {
|
||||
salt, err := generateSalt(argon2SaltLen)
|
||||
if err != nil {
|
||||
|
|
@ -34,6 +35,7 @@ func HashPassword(password string) (string, error) {
|
|||
|
||||
// VerifyPassword verifies a password against an Argon2id hash string.
|
||||
// The hash must be in the format produced by HashPassword.
|
||||
// Usage: call VerifyPassword(...) during the package's normal workflow.
|
||||
func VerifyPassword(password, hash string) (bool, error) {
|
||||
parts := core.Split(hash, "$")
|
||||
if len(parts) != 6 {
|
||||
|
|
@ -116,6 +118,7 @@ func parsePrefixedUint32(input, prefix string) (uint32, error) {
|
|||
|
||||
// HashBcrypt hashes a password using bcrypt with the given cost.
|
||||
// Cost must be between bcrypt.MinCost and bcrypt.MaxCost.
|
||||
// Usage: call HashBcrypt(...) during the package's normal workflow.
|
||||
func HashBcrypt(password string, cost int) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||
if err != nil {
|
||||
|
|
@ -125,6 +128,7 @@ func HashBcrypt(password string, cost int) (string, error) {
|
|||
}
|
||||
|
||||
// VerifyBcrypt verifies a password against a bcrypt hash.
|
||||
// Usage: call VerifyBcrypt(...) during the package's normal workflow.
|
||||
func VerifyBcrypt(password, hash string) (bool, error) {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestHashPassword_Good(t *testing.T) {
|
||||
func TestHash_HashPassword_Good(t *testing.T) {
|
||||
password := "my-secure-password"
|
||||
|
||||
hash, err := HashPassword(password)
|
||||
|
|
@ -20,7 +20,7 @@ func TestHashPassword_Good(t *testing.T) {
|
|||
assert.True(t, match)
|
||||
}
|
||||
|
||||
func TestVerifyPassword_Bad(t *testing.T) {
|
||||
func TestHash_VerifyPassword_Bad(t *testing.T) {
|
||||
password := "my-secure-password"
|
||||
wrongPassword := "wrong-password"
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ func TestVerifyPassword_Bad(t *testing.T) {
|
|||
assert.False(t, match)
|
||||
}
|
||||
|
||||
func TestHashBcrypt_Good(t *testing.T) {
|
||||
func TestHash_HashBcrypt_Good(t *testing.T) {
|
||||
password := "bcrypt-test-password"
|
||||
|
||||
hash, err := HashBcrypt(password, bcrypt.DefaultCost)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
// HMACSHA256 computes the HMAC-SHA256 of a message using the given key.
|
||||
// Usage: call HMACSHA256(...) during the package's normal workflow.
|
||||
func HMACSHA256(message, key []byte) []byte {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(message)
|
||||
|
|
@ -15,6 +16,7 @@ func HMACSHA256(message, key []byte) []byte {
|
|||
}
|
||||
|
||||
// HMACSHA512 computes the HMAC-SHA512 of a message using the given key.
|
||||
// Usage: call HMACSHA512(...) during the package's normal workflow.
|
||||
func HMACSHA512(message, key []byte) []byte {
|
||||
mac := hmac.New(sha512.New, key)
|
||||
mac.Write(message)
|
||||
|
|
@ -23,6 +25,7 @@ func HMACSHA512(message, key []byte) []byte {
|
|||
|
||||
// VerifyHMAC verifies an HMAC using constant-time comparison.
|
||||
// hashFunc should be sha256.New, sha512.New, etc.
|
||||
// Usage: call VerifyHMAC(...) during the package's normal workflow.
|
||||
func VerifyHMAC(message, key, mac []byte, hashFunc func() hash.Hash) bool {
|
||||
expected := hmac.New(hashFunc, key)
|
||||
expected.Write(message)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHMACSHA256_Good(t *testing.T) {
|
||||
func TestHMAC_HMACSHA256_Good(t *testing.T) {
|
||||
// RFC 4231 Test Case 2
|
||||
key := []byte("Jefe")
|
||||
message := []byte("what do ya want for nothing?")
|
||||
|
|
@ -18,7 +18,7 @@ func TestHMACSHA256_Good(t *testing.T) {
|
|||
assert.Equal(t, expected, hex.EncodeToString(mac))
|
||||
}
|
||||
|
||||
func TestVerifyHMAC_Good(t *testing.T) {
|
||||
func TestHMAC_VerifyHMAC_Good(t *testing.T) {
|
||||
key := []byte("secret-key")
|
||||
message := []byte("test message")
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ func TestVerifyHMAC_Good(t *testing.T) {
|
|||
assert.True(t, valid)
|
||||
}
|
||||
|
||||
func TestVerifyHMAC_Bad(t *testing.T) {
|
||||
func TestHMAC_VerifyHMAC_Bad(t *testing.T) {
|
||||
key := []byte("secret-key")
|
||||
message := []byte("test message")
|
||||
tampered := []byte("tampered message")
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ const (
|
|||
|
||||
// DeriveKey derives a key from a passphrase using Argon2id with default parameters.
|
||||
// The salt must be argon2SaltLen bytes. keyLen specifies the desired key length.
|
||||
// Usage: call DeriveKey(...) during the package's normal workflow.
|
||||
func DeriveKey(passphrase, salt []byte, keyLen uint32) []byte {
|
||||
return argon2.IDKey(passphrase, salt, argon2Time, argon2Memory, argon2Parallelism, keyLen)
|
||||
}
|
||||
|
||||
// DeriveKeyScrypt derives a key from a passphrase using scrypt.
|
||||
// Uses recommended parameters: N=32768, r=8, p=1.
|
||||
// Usage: call DeriveKeyScrypt(...) during the package's normal workflow.
|
||||
func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) {
|
||||
key, err := scrypt.Key(passphrase, salt, 32768, 8, 1, keyLen)
|
||||
if err != nil {
|
||||
|
|
@ -42,6 +44,7 @@ func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) {
|
|||
// HKDF derives a key using HKDF-SHA256.
|
||||
// secret is the input keying material, salt is optional (can be nil),
|
||||
// info is optional context, and keyLen is the desired output length.
|
||||
// Usage: call HKDF(...) during the package's normal workflow.
|
||||
func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error) {
|
||||
reader := hkdf.New(sha256.New, secret, salt, info)
|
||||
key := make([]byte, keyLen)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeriveKey_Good(t *testing.T) {
|
||||
func TestKDF_DeriveKey_Good(t *testing.T) {
|
||||
passphrase := []byte("test-passphrase")
|
||||
salt := []byte("1234567890123456") // 16 bytes
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ func TestDeriveKey_Good(t *testing.T) {
|
|||
assert.NotEqual(t, key1, key3)
|
||||
}
|
||||
|
||||
func TestDeriveKeyScrypt_Good(t *testing.T) {
|
||||
func TestKDF_DeriveKeyScrypt_Good(t *testing.T) {
|
||||
passphrase := []byte("test-passphrase")
|
||||
salt := []byte("1234567890123456")
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ func TestDeriveKeyScrypt_Good(t *testing.T) {
|
|||
assert.Equal(t, key, key2)
|
||||
}
|
||||
|
||||
func TestHKDF_Good(t *testing.T) {
|
||||
func TestKDF_HKDF_Good(t *testing.T) {
|
||||
secret := []byte("input-keying-material")
|
||||
salt := []byte("optional-salt")
|
||||
info := []byte("context-info")
|
||||
|
|
@ -57,8 +57,8 @@ func TestHKDF_Good(t *testing.T) {
|
|||
|
||||
// --- Phase 0 Additions ---
|
||||
|
||||
// TestKeyDerivationDeterminism_Good verifies same passphrase + salt always yields same key.
|
||||
func TestKeyDerivationDeterminism_Good(t *testing.T) {
|
||||
// TestKDF_KeyDerivationDeterminism_Good verifies same passphrase + salt always yields same key.
|
||||
func TestKDF_KeyDerivationDeterminism_Good(t *testing.T) {
|
||||
passphrase := []byte("determinism-test-passphrase")
|
||||
salt := []byte("1234567890123456") // 16 bytes
|
||||
|
||||
|
|
@ -82,8 +82,8 @@ func TestKeyDerivationDeterminism_Good(t *testing.T) {
|
|||
assert.Equal(t, scryptKey1, scryptKey2, "scrypt must also be deterministic")
|
||||
}
|
||||
|
||||
// TestHKDFDifferentInfoStrings_Good verifies different info strings produce different keys.
|
||||
func TestHKDFDifferentInfoStrings_Good(t *testing.T) {
|
||||
// TestKDF_HKDFDifferentInfoStrings_Good verifies different info strings produce different keys.
|
||||
func TestKDF_HKDFDifferentInfoStrings_Good(t *testing.T) {
|
||||
secret := []byte("shared-secret-material")
|
||||
salt := []byte("common-salt")
|
||||
|
||||
|
|
@ -114,8 +114,8 @@ func TestHKDFDifferentInfoStrings_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestHKDFNilSalt_Good verifies HKDF works with nil salt.
|
||||
func TestHKDFNilSalt_Good(t *testing.T) {
|
||||
// TestKDF_HKDFNilSalt_Good verifies HKDF works with nil salt.
|
||||
func TestKDF_HKDFNilSalt_Good(t *testing.T) {
|
||||
secret := []byte("input-keying-material")
|
||||
info := []byte("context")
|
||||
|
||||
|
|
|
|||
|
|
@ -42,11 +42,13 @@ var keyMap = map[rune]rune{
|
|||
// SetKeyMap replaces the default character substitution map.
|
||||
// Use this to customize the quasi-salt derivation for specific applications.
|
||||
// Changes affect all subsequent Hash and Verify calls.
|
||||
// Usage: call SetKeyMap(...) during the package's normal workflow.
|
||||
func SetKeyMap(newKeyMap map[rune]rune) {
|
||||
keyMap = newKeyMap
|
||||
}
|
||||
|
||||
// GetKeyMap returns the current character substitution map.
|
||||
// Usage: call GetKeyMap(...) during the package's normal workflow.
|
||||
func GetKeyMap() map[rune]rune {
|
||||
return keyMap
|
||||
}
|
||||
|
|
@ -61,6 +63,7 @@ func GetKeyMap() map[rune]rune {
|
|||
//
|
||||
// The same input always produces the same hash, enabling verification
|
||||
// without storing a separate salt value.
|
||||
// Usage: call Hash(...) when you need a deterministic content-style digest rather than a password hash.
|
||||
func Hash(input string) string {
|
||||
salt := createSalt(input)
|
||||
hash := sha256.Sum256([]byte(input + salt))
|
||||
|
|
@ -89,6 +92,7 @@ func createSalt(input string) string {
|
|||
// Verify checks if an input string produces the given hash.
|
||||
// Returns true if Hash(input) equals the provided hash value.
|
||||
// Uses constant-time comparison to prevent timing attacks.
|
||||
// Usage: call Verify(...) during the package's normal workflow.
|
||||
func Verify(input string, hash string) bool {
|
||||
computed := Hash(input)
|
||||
return subtle.ConstantTimeCompare([]byte(computed), []byte(hash)) == 1
|
||||
|
|
|
|||
|
|
@ -7,36 +7,36 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHash_Good(t *testing.T) {
|
||||
func TestLTHN_Hash_Good(t *testing.T) {
|
||||
hash := Hash("hello")
|
||||
assert.NotEmpty(t, hash)
|
||||
}
|
||||
|
||||
func TestVerify_Good(t *testing.T) {
|
||||
func TestLTHN_Verify_Good(t *testing.T) {
|
||||
hash := Hash("hello")
|
||||
assert.True(t, Verify("hello", hash))
|
||||
}
|
||||
|
||||
func TestVerify_Bad(t *testing.T) {
|
||||
func TestLTHN_Verify_Bad(t *testing.T) {
|
||||
hash := Hash("hello")
|
||||
assert.False(t, Verify("world", hash))
|
||||
}
|
||||
|
||||
func TestCreateSalt_Good(t *testing.T) {
|
||||
func TestLTHN_CreateSalt_Good(t *testing.T) {
|
||||
// "hello" reversed: "olleh" -> "0113h"
|
||||
expected := "0113h"
|
||||
actual := createSalt("hello")
|
||||
assert.Equal(t, expected, actual, "Salt should be correctly created for 'hello'")
|
||||
}
|
||||
|
||||
func TestCreateSalt_Bad(t *testing.T) {
|
||||
func TestLTHN_CreateSalt_Bad(t *testing.T) {
|
||||
// Test with an empty string
|
||||
expected := ""
|
||||
actual := createSalt("")
|
||||
assert.Equal(t, expected, actual, "Salt for an empty string should be empty")
|
||||
}
|
||||
|
||||
func TestCreateSalt_Ugly(t *testing.T) {
|
||||
func TestLTHN_CreateSalt_Ugly(t *testing.T) {
|
||||
// Test with characters not in the keyMap
|
||||
input := "world123"
|
||||
// "world123" reversed: "321dlrow" -> "e2ld1r0w"
|
||||
|
|
@ -54,7 +54,7 @@ func TestCreateSalt_Ugly(t *testing.T) {
|
|||
|
||||
var testKeyMapMu sync.Mutex
|
||||
|
||||
func TestSetKeyMap_Good(t *testing.T) {
|
||||
func TestLTHN_SetKeyMap_Good(t *testing.T) {
|
||||
testKeyMapMu.Lock()
|
||||
originalKeyMap := GetKeyMap()
|
||||
t.Cleanup(func() {
|
||||
|
|
|
|||
|
|
@ -14,17 +14,20 @@ import (
|
|||
)
|
||||
|
||||
// Service provides OpenPGP cryptographic operations.
|
||||
// Usage: use Service with the other exported helpers in this package.
|
||||
type Service struct {
|
||||
core *framework.Core
|
||||
}
|
||||
|
||||
// New creates a new OpenPGP service instance.
|
||||
// Usage: call New(...) to create a ready-to-use value.
|
||||
func New(c *framework.Core) (any, error) {
|
||||
return &Service{core: c}, nil
|
||||
}
|
||||
|
||||
// CreateKeyPair generates a new RSA-4096 PGP keypair.
|
||||
// Returns the armored private key string.
|
||||
// Usage: call CreateKeyPair(...) during the package's normal workflow.
|
||||
func (s *Service) CreateKeyPair(name, passphrase string) (string, error) {
|
||||
config := &packet.Config{
|
||||
Algorithm: packet.PubKeyAlgoRSA,
|
||||
|
|
@ -100,6 +103,7 @@ func serializeEntity(w goio.Writer, e *openpgp.Entity) error {
|
|||
|
||||
// EncryptPGP encrypts data for a recipient identified by their public key (armored string in recipientPath).
|
||||
// The encrypted data is written to the provided writer and also returned as an armored string.
|
||||
// Usage: call EncryptPGP(...) during the package's normal workflow.
|
||||
func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) {
|
||||
entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(recipientPath))
|
||||
if err != nil {
|
||||
|
|
@ -135,6 +139,7 @@ func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opt
|
|||
}
|
||||
|
||||
// DecryptPGP decrypts a PGP message using the provided armored private key and passphrase.
|
||||
// Usage: call DecryptPGP(...) during the package's normal workflow.
|
||||
func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) {
|
||||
entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(privateKey))
|
||||
if err != nil {
|
||||
|
|
@ -173,6 +178,7 @@ func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any
|
|||
}
|
||||
|
||||
// HandleIPCEvents handles PGP-related IPC messages.
|
||||
// Usage: call HandleIPCEvents(...) during the package's normal workflow.
|
||||
func (s *Service) HandleIPCEvents(c *framework.Core, msg framework.Message) error {
|
||||
switch m := msg.(type) {
|
||||
case map[string]any:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateKeyPair_Good(t *testing.T) {
|
||||
func TestService_CreateKeyPair_Good(t *testing.T) {
|
||||
c := framework.New()
|
||||
s := &Service{core: c}
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ func TestCreateKeyPair_Good(t *testing.T) {
|
|||
assert.Contains(t, privKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----")
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt_Good(t *testing.T) {
|
||||
func TestService_EncryptDecrypt_Good(t *testing.T) {
|
||||
c := framework.New()
|
||||
s := &Service{core: c}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
)
|
||||
|
||||
// KeyPair holds armored PGP public and private keys.
|
||||
// Usage: use KeyPair with the other exported helpers in this package.
|
||||
type KeyPair struct {
|
||||
PublicKey string
|
||||
PrivateKey string
|
||||
|
|
@ -24,6 +25,7 @@ type KeyPair struct {
|
|||
// CreateKeyPair generates a new PGP key pair for the given identity.
|
||||
// If password is non-empty, the private key is encrypted with it.
|
||||
// Returns a KeyPair with armored public and private keys.
|
||||
// Usage: call CreateKeyPair(...) during the package's normal workflow.
|
||||
func CreateKeyPair(name, email, password string) (*KeyPair, error) {
|
||||
const op = "pgp.CreateKeyPair"
|
||||
|
||||
|
|
@ -116,6 +118,7 @@ func serializeEncryptedEntity(w io.Writer, e *openpgp.Entity) error {
|
|||
|
||||
// Encrypt encrypts data for the recipient identified by their armored public key.
|
||||
// Returns the encrypted data as armored PGP output.
|
||||
// Usage: call Encrypt(...) during the package's normal workflow.
|
||||
func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) {
|
||||
const op = "pgp.Encrypt"
|
||||
|
||||
|
|
@ -149,6 +152,7 @@ func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) {
|
|||
|
||||
// Decrypt decrypts armored PGP data using the given armored private key.
|
||||
// If the private key is encrypted, the password is used to decrypt it first.
|
||||
// Usage: call Decrypt(...) during the package's normal workflow.
|
||||
func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) {
|
||||
const op = "pgp.Decrypt"
|
||||
|
||||
|
|
@ -193,6 +197,7 @@ func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) {
|
|||
// Sign creates an armored detached signature for the given data using
|
||||
// the armored private key. If the key is encrypted, the password is used
|
||||
// to decrypt it first.
|
||||
// Usage: call Sign(...) during the package's normal workflow.
|
||||
func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) {
|
||||
const op = "pgp.Sign"
|
||||
|
||||
|
|
@ -224,6 +229,7 @@ func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) {
|
|||
|
||||
// Verify verifies an armored detached signature against the given data
|
||||
// and armored public key. Returns nil if the signature is valid.
|
||||
// Usage: call Verify(...) during the package's normal workflow.
|
||||
func Verify(data, signature []byte, publicKeyArmor string) error {
|
||||
const op = "pgp.Verify"
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateKeyPair_Good(t *testing.T) {
|
||||
func TestPGP_CreateKeyPair_Good(t *testing.T) {
|
||||
kp, err := CreateKeyPair("Test User", "test@example.com", "")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kp)
|
||||
|
|
@ -15,7 +15,7 @@ func TestCreateKeyPair_Good(t *testing.T) {
|
|||
assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----")
|
||||
}
|
||||
|
||||
func TestCreateKeyPair_Bad(t *testing.T) {
|
||||
func TestPGP_CreateKeyPair_Bad(t *testing.T) {
|
||||
// Empty name still works (openpgp allows it), but test with password
|
||||
kp, err := CreateKeyPair("Secure User", "secure@example.com", "strong-password")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -24,14 +24,14 @@ func TestCreateKeyPair_Bad(t *testing.T) {
|
|||
assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----")
|
||||
}
|
||||
|
||||
func TestCreateKeyPair_Ugly(t *testing.T) {
|
||||
func TestPGP_CreateKeyPair_Ugly(t *testing.T) {
|
||||
// Minimal identity
|
||||
kp, err := CreateKeyPair("", "", "")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kp)
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt_Good(t *testing.T) {
|
||||
func TestPGP_EncryptDecrypt_Good(t *testing.T) {
|
||||
kp, err := CreateKeyPair("Test User", "test@example.com", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ func TestEncryptDecrypt_Good(t *testing.T) {
|
|||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt_Bad(t *testing.T) {
|
||||
func TestPGP_EncryptDecrypt_Bad(t *testing.T) {
|
||||
kp1, err := CreateKeyPair("User One", "one@example.com", "")
|
||||
require.NoError(t, err)
|
||||
kp2, err := CreateKeyPair("User Two", "two@example.com", "")
|
||||
|
|
@ -61,7 +61,7 @@ func TestEncryptDecrypt_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt_Ugly(t *testing.T) {
|
||||
func TestPGP_EncryptDecrypt_Ugly(t *testing.T) {
|
||||
// Invalid public key for encryption
|
||||
_, err := Encrypt([]byte("data"), "not-a-pgp-key")
|
||||
assert.Error(t, err)
|
||||
|
|
@ -71,7 +71,7 @@ func TestEncryptDecrypt_Ugly(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEncryptDecryptWithPassword_Good(t *testing.T) {
|
||||
func TestPGP_EncryptDecryptWithPassword_Good(t *testing.T) {
|
||||
password := "my-secret-passphrase"
|
||||
kp, err := CreateKeyPair("Secure User", "secure@example.com", password)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -85,7 +85,7 @@ func TestEncryptDecryptWithPassword_Good(t *testing.T) {
|
|||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestSignVerify_Good(t *testing.T) {
|
||||
func TestPGP_SignVerify_Good(t *testing.T) {
|
||||
kp, err := CreateKeyPair("Signer", "signer@example.com", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func TestSignVerify_Good(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSignVerify_Bad(t *testing.T) {
|
||||
func TestPGP_SignVerify_Bad(t *testing.T) {
|
||||
kp, err := CreateKeyPair("Signer", "signer@example.com", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ func TestSignVerify_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSignVerify_Ugly(t *testing.T) {
|
||||
func TestPGP_SignVerify_Ugly(t *testing.T) {
|
||||
// Invalid key for signing
|
||||
_, err := Sign([]byte("data"), "not-a-key", "")
|
||||
assert.Error(t, err)
|
||||
|
|
@ -129,7 +129,7 @@ func TestSignVerify_Ugly(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSignVerifyWithPassword_Good(t *testing.T) {
|
||||
func TestPGP_SignVerifyWithPassword_Good(t *testing.T) {
|
||||
password := "signing-password"
|
||||
kp, err := CreateKeyPair("Signer", "signer@example.com", password)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -142,7 +142,7 @@ func TestSignVerifyWithPassword_Good(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFullRoundTrip_Good(t *testing.T) {
|
||||
func TestPGP_FullRoundTrip_Good(t *testing.T) {
|
||||
// Generate keys, encrypt, decrypt, sign, and verify - full round trip
|
||||
kp, err := CreateKeyPair("Full Test", "full@example.com", "")
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -12,14 +12,17 @@ import (
|
|||
)
|
||||
|
||||
// Service provides RSA functionality.
|
||||
// Usage: use Service with the other exported helpers in this package.
|
||||
type Service struct{}
|
||||
|
||||
// NewService creates and returns a new Service instance for performing RSA-related operations.
|
||||
// Usage: call NewService(...) to create a ready-to-use value.
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// GenerateKeyPair creates a new RSA key pair.
|
||||
// Usage: call GenerateKeyPair(...) during the package's normal workflow.
|
||||
func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err error) {
|
||||
const op = "rsa.GenerateKeyPair"
|
||||
|
||||
|
|
@ -50,6 +53,7 @@ func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err e
|
|||
}
|
||||
|
||||
// Encrypt encrypts data with a public key.
|
||||
// Usage: call Encrypt(...) during the package's normal workflow.
|
||||
func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) {
|
||||
const op = "rsa.Encrypt"
|
||||
|
||||
|
|
@ -77,6 +81,7 @@ func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) {
|
|||
}
|
||||
|
||||
// Decrypt decrypts data with a private key.
|
||||
// Usage: call Decrypt(...) during the package's normal workflow.
|
||||
func (s *Service) Decrypt(privateKey, ciphertext, label []byte) ([]byte, error) {
|
||||
const op = "rsa.Decrypt"
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) {
|
|||
return 0, core.NewError("read error")
|
||||
}
|
||||
|
||||
func TestRSA_Good(t *testing.T) {
|
||||
func TestRSA_RSA_Good(t *testing.T) {
|
||||
s := NewService()
|
||||
|
||||
// Generate a new key pair
|
||||
|
|
@ -37,7 +37,7 @@ func TestRSA_Good(t *testing.T) {
|
|||
assert.Equal(t, message, plaintext)
|
||||
}
|
||||
|
||||
func TestRSA_Bad(t *testing.T) {
|
||||
func TestRSA_RSA_Bad(t *testing.T) {
|
||||
s := NewService()
|
||||
|
||||
// Decrypt with wrong key
|
||||
|
|
@ -56,7 +56,7 @@ func TestRSA_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRSA_Ugly(t *testing.T) {
|
||||
func TestRSA_RSA_Ugly(t *testing.T) {
|
||||
s := NewService()
|
||||
|
||||
// Malformed keys and messages
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
// ChaCha20Encrypt encrypts plaintext using ChaCha20-Poly1305.
|
||||
// The key must be 32 bytes. The nonce is randomly generated and prepended
|
||||
// to the ciphertext.
|
||||
// Usage: call ChaCha20Encrypt(...) during the package's normal workflow.
|
||||
func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
|
|
@ -30,6 +31,7 @@ func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) {
|
|||
|
||||
// ChaCha20Decrypt decrypts ciphertext encrypted with ChaCha20Encrypt.
|
||||
// The key must be 32 bytes. Expects the nonce prepended to the ciphertext.
|
||||
// Usage: call ChaCha20Decrypt(...) during the package's normal workflow.
|
||||
func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
|
|
@ -53,6 +55,7 @@ func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) {
|
|||
// AESGCMEncrypt encrypts plaintext using AES-256-GCM.
|
||||
// The key must be 32 bytes. The nonce is randomly generated and prepended
|
||||
// to the ciphertext.
|
||||
// Usage: call AESGCMEncrypt(...) during the package's normal workflow.
|
||||
func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
|
|
@ -75,6 +78,7 @@ func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) {
|
|||
|
||||
// AESGCMDecrypt decrypts ciphertext encrypted with AESGCMEncrypt.
|
||||
// The key must be 32 bytes. Expects the nonce prepended to the ciphertext.
|
||||
// Usage: call AESGCMDecrypt(...) during the package's normal workflow.
|
||||
func AESGCMDecrypt(ciphertext, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChaCha20_Good(t *testing.T) {
|
||||
func TestSymmetric_ChaCha20_Good(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -23,7 +23,7 @@ func TestChaCha20_Good(t *testing.T) {
|
|||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestChaCha20_Bad(t *testing.T) {
|
||||
func TestSymmetric_ChaCha20_Bad(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
wrongKey := make([]byte, 32)
|
||||
_, _ = rand.Read(key)
|
||||
|
|
@ -38,7 +38,7 @@ func TestChaCha20_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAESGCM_Good(t *testing.T) {
|
||||
func TestSymmetric_AESGCM_Good(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -56,8 +56,8 @@ func TestAESGCM_Good(t *testing.T) {
|
|||
|
||||
// --- Phase 0 Additions ---
|
||||
|
||||
// TestAESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data.
|
||||
func TestAESGCM_Bad_WrongKey(t *testing.T) {
|
||||
// TestSymmetric_AESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data.
|
||||
func TestSymmetric_AESGCM_Bad_WrongKey(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
wrongKey := make([]byte, 32)
|
||||
_, _ = rand.Read(key)
|
||||
|
|
@ -72,8 +72,8 @@ func TestAESGCM_Bad_WrongKey(t *testing.T) {
|
|||
assert.Nil(t, decrypted, "wrong key must not return partial data")
|
||||
}
|
||||
|
||||
// TestChaCha20EmptyPlaintext_Good verifies empty plaintext round-trip at low level.
|
||||
func TestChaCha20EmptyPlaintext_Good(t *testing.T) {
|
||||
// TestSymmetric_ChaCha20EmptyPlaintext_Good verifies empty plaintext round-trip at low level.
|
||||
func TestSymmetric_ChaCha20EmptyPlaintext_Good(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -87,8 +87,8 @@ func TestChaCha20EmptyPlaintext_Good(t *testing.T) {
|
|||
assert.Empty(t, decrypted)
|
||||
}
|
||||
|
||||
// TestAESGCMEmptyPlaintext_Good verifies empty plaintext round-trip at low level.
|
||||
func TestAESGCMEmptyPlaintext_Good(t *testing.T) {
|
||||
// TestSymmetric_AESGCMEmptyPlaintext_Good verifies empty plaintext round-trip at low level.
|
||||
func TestSymmetric_AESGCMEmptyPlaintext_Good(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -102,8 +102,8 @@ func TestAESGCMEmptyPlaintext_Good(t *testing.T) {
|
|||
assert.Empty(t, decrypted)
|
||||
}
|
||||
|
||||
// TestChaCha20LargePayload_Good verifies 1MB encrypt/decrypt round-trip.
|
||||
func TestChaCha20LargePayload_Good(t *testing.T) {
|
||||
// TestSymmetric_ChaCha20LargePayload_Good verifies 1MB encrypt/decrypt round-trip.
|
||||
func TestSymmetric_ChaCha20LargePayload_Good(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
_, _ = rand.Read(key)
|
||||
|
||||
|
|
@ -120,8 +120,8 @@ func TestChaCha20LargePayload_Good(t *testing.T) {
|
|||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
// TestAESGCMLargePayload_Good verifies 1MB encrypt/decrypt round-trip.
|
||||
func TestAESGCMLargePayload_Good(t *testing.T) {
|
||||
// TestSymmetric_AESGCMLargePayload_Good verifies 1MB encrypt/decrypt round-trip.
|
||||
func TestSymmetric_AESGCMLargePayload_Good(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
_, _ = rand.Read(key)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,18 +10,23 @@ import (
|
|||
)
|
||||
|
||||
// ApprovalStatus represents the state of an approval request.
|
||||
// Usage: use ApprovalStatus with the other exported helpers in this package.
|
||||
type ApprovalStatus int
|
||||
|
||||
const (
|
||||
// ApprovalPending means the request is awaiting review.
|
||||
// Usage: compare or pass ApprovalPending when using the related package APIs.
|
||||
ApprovalPending ApprovalStatus = iota
|
||||
// ApprovalApproved means the request was approved.
|
||||
// Usage: compare or pass ApprovalApproved when using the related package APIs.
|
||||
ApprovalApproved
|
||||
// ApprovalDenied means the request was denied.
|
||||
// Usage: compare or pass ApprovalDenied when using the related package APIs.
|
||||
ApprovalDenied
|
||||
)
|
||||
|
||||
// String returns the human-readable name of the approval status.
|
||||
// Usage: call String(...) during the package's normal workflow.
|
||||
func (s ApprovalStatus) String() string {
|
||||
switch s {
|
||||
case ApprovalPending:
|
||||
|
|
@ -36,6 +41,7 @@ func (s ApprovalStatus) String() string {
|
|||
}
|
||||
|
||||
// ApprovalRequest represents a queued capability approval request.
|
||||
// Usage: use ApprovalRequest with the other exported helpers in this package.
|
||||
type ApprovalRequest struct {
|
||||
// ID is the unique identifier for this request.
|
||||
ID string
|
||||
|
|
@ -58,6 +64,7 @@ type ApprovalRequest struct {
|
|||
}
|
||||
|
||||
// ApprovalQueue manages pending approval requests for NeedsApproval decisions.
|
||||
// Usage: use ApprovalQueue with the other exported helpers in this package.
|
||||
type ApprovalQueue struct {
|
||||
mu sync.RWMutex
|
||||
requests map[string]*ApprovalRequest
|
||||
|
|
@ -65,6 +72,7 @@ type ApprovalQueue struct {
|
|||
}
|
||||
|
||||
// NewApprovalQueue creates an empty approval queue.
|
||||
// Usage: call NewApprovalQueue(...) to create a ready-to-use value.
|
||||
func NewApprovalQueue() *ApprovalQueue {
|
||||
return &ApprovalQueue{
|
||||
requests: make(map[string]*ApprovalRequest),
|
||||
|
|
@ -73,6 +81,7 @@ func NewApprovalQueue() *ApprovalQueue {
|
|||
|
||||
// Submit creates a new approval request and returns its ID.
|
||||
// Returns an error if the agent name or capability is empty.
|
||||
// Usage: call Submit(...) during the package's normal workflow.
|
||||
func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (string, error) {
|
||||
if agent == "" {
|
||||
return "", coreerr.E("trust.ApprovalQueue.Submit", "agent name is required", nil)
|
||||
|
|
@ -101,6 +110,7 @@ func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (strin
|
|||
|
||||
// Approve marks a pending request as approved. Returns an error if the
|
||||
// request is not found or is not in pending status.
|
||||
// Usage: call Approve(...) during the package's normal workflow.
|
||||
func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
|
@ -122,6 +132,7 @@ func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) err
|
|||
|
||||
// Deny marks a pending request as denied. Returns an error if the
|
||||
// request is not found or is not in pending status.
|
||||
// Usage: call Deny(...) during the package's normal workflow.
|
||||
func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
|
@ -142,6 +153,7 @@ func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error
|
|||
}
|
||||
|
||||
// Get returns the approval request with the given ID, or nil if not found.
|
||||
// Usage: call Get(...) during the package's normal workflow.
|
||||
func (q *ApprovalQueue) Get(id string) *ApprovalRequest {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
|
@ -156,6 +168,7 @@ func (q *ApprovalQueue) Get(id string) *ApprovalRequest {
|
|||
}
|
||||
|
||||
// Pending returns all requests with ApprovalPending status.
|
||||
// Usage: call Pending(...) during the package's normal workflow.
|
||||
func (q *ApprovalQueue) Pending() []ApprovalRequest {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
|
@ -170,6 +183,7 @@ func (q *ApprovalQueue) Pending() []ApprovalRequest {
|
|||
}
|
||||
|
||||
// PendingSeq returns an iterator over all requests with ApprovalPending status.
|
||||
// Usage: call PendingSeq(...) during the package's normal workflow.
|
||||
func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] {
|
||||
return func(yield func(ApprovalRequest) bool) {
|
||||
q.mu.RLock()
|
||||
|
|
@ -186,6 +200,7 @@ func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] {
|
|||
}
|
||||
|
||||
// Len returns the total number of requests in the queue.
|
||||
// Usage: call Len(...) during the package's normal workflow.
|
||||
func (q *ApprovalQueue) Len() int {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ import (
|
|||
|
||||
// --- ApprovalStatus ---
|
||||
|
||||
func TestApprovalStatusString_Good(t *testing.T) {
|
||||
func TestApproval_ApprovalStatusString_Good(t *testing.T) {
|
||||
assert.Equal(t, "pending", ApprovalPending.String())
|
||||
assert.Equal(t, "approved", ApprovalApproved.String())
|
||||
assert.Equal(t, "denied", ApprovalDenied.String())
|
||||
}
|
||||
|
||||
func TestApprovalStatusString_Bad_Unknown(t *testing.T) {
|
||||
func TestApproval_ApprovalStatusString_Bad_Unknown(t *testing.T) {
|
||||
assert.Contains(t, ApprovalStatus(99).String(), "unknown")
|
||||
}
|
||||
|
||||
// --- Submit ---
|
||||
|
||||
func TestApprovalSubmit_Good(t *testing.T) {
|
||||
func TestApproval_ApprovalSubmit_Good(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -31,7 +31,7 @@ func TestApprovalSubmit_Good(t *testing.T) {
|
|||
assert.Equal(t, 1, q.Len())
|
||||
}
|
||||
|
||||
func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) {
|
||||
func TestApproval_ApprovalSubmit_Good_MultipleRequests(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id1, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -42,7 +42,7 @@ func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) {
|
|||
assert.Equal(t, 2, q.Len())
|
||||
}
|
||||
|
||||
func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) {
|
||||
func TestApproval_ApprovalSubmit_Good_EmptyRepo(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, err := q.Submit("Clotho", CapMergePR, "")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -53,14 +53,14 @@ func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) {
|
|||
assert.Empty(t, req.Repo)
|
||||
}
|
||||
|
||||
func TestApprovalSubmit_Bad_EmptyAgent(t *testing.T) {
|
||||
func TestApproval_ApprovalSubmit_Bad_EmptyAgent(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
_, err := q.Submit("", CapMergePR, "")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "agent name is required")
|
||||
}
|
||||
|
||||
func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) {
|
||||
func TestApproval_ApprovalSubmit_Bad_EmptyCapability(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
_, err := q.Submit("Clotho", "", "")
|
||||
assert.Error(t, err)
|
||||
|
|
@ -69,7 +69,7 @@ func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) {
|
|||
|
||||
// --- Get ---
|
||||
|
||||
func TestApprovalGet_Good(t *testing.T) {
|
||||
func TestApproval_ApprovalGet_Good(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -85,7 +85,7 @@ func TestApprovalGet_Good(t *testing.T) {
|
|||
assert.True(t, req.ReviewedAt.IsZero())
|
||||
}
|
||||
|
||||
func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) {
|
||||
func TestApproval_ApprovalGet_Good_ReturnsSnapshot(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -99,14 +99,14 @@ func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) {
|
|||
assert.Equal(t, ApprovalPending, original.Status)
|
||||
}
|
||||
|
||||
func TestApprovalGet_Bad_NotFound(t *testing.T) {
|
||||
func TestApproval_ApprovalGet_Bad_NotFound(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
assert.Nil(t, q.Get("nonexistent"))
|
||||
}
|
||||
|
||||
// --- Approve ---
|
||||
|
||||
func TestApprovalApprove_Good(t *testing.T) {
|
||||
func TestApproval_ApprovalApprove_Good(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
|
||||
|
|
@ -121,14 +121,14 @@ func TestApprovalApprove_Good(t *testing.T) {
|
|||
assert.False(t, req.ReviewedAt.IsZero())
|
||||
}
|
||||
|
||||
func TestApprovalApprove_Bad_NotFound(t *testing.T) {
|
||||
func TestApproval_ApprovalApprove_Bad_NotFound(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
err := q.Approve("nonexistent", "admin", "")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) {
|
||||
func TestApproval_ApprovalApprove_Bad_AlreadyApproved(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
require.NoError(t, q.Approve(id, "admin", ""))
|
||||
|
|
@ -138,7 +138,7 @@ func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "already approved")
|
||||
}
|
||||
|
||||
func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) {
|
||||
func TestApproval_ApprovalApprove_Bad_AlreadyDenied(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
require.NoError(t, q.Deny(id, "admin", "nope"))
|
||||
|
|
@ -150,7 +150,7 @@ func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) {
|
|||
|
||||
// --- Deny ---
|
||||
|
||||
func TestApprovalDeny_Good(t *testing.T) {
|
||||
func TestApproval_ApprovalDeny_Good(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
|
||||
|
|
@ -165,14 +165,14 @@ func TestApprovalDeny_Good(t *testing.T) {
|
|||
assert.False(t, req.ReviewedAt.IsZero())
|
||||
}
|
||||
|
||||
func TestApprovalDeny_Bad_NotFound(t *testing.T) {
|
||||
func TestApproval_ApprovalDeny_Bad_NotFound(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
err := q.Deny("nonexistent", "admin", "")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) {
|
||||
func TestApproval_ApprovalDeny_Bad_AlreadyDenied(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
require.NoError(t, q.Deny(id, "admin", ""))
|
||||
|
|
@ -184,7 +184,7 @@ func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) {
|
|||
|
||||
// --- Pending ---
|
||||
|
||||
func TestApprovalPending_Good(t *testing.T) {
|
||||
func TestApproval_ApprovalPending_Good(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
q.Submit("Hypnos", CapMergePR, "host-uk/docs")
|
||||
|
|
@ -196,12 +196,12 @@ func TestApprovalPending_Good(t *testing.T) {
|
|||
assert.Len(t, pending, 2)
|
||||
}
|
||||
|
||||
func TestApprovalPending_Good_Empty(t *testing.T) {
|
||||
func TestApproval_ApprovalPending_Good_Empty(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
assert.Empty(t, q.Pending())
|
||||
}
|
||||
|
||||
func TestApprovalPendingSeq_Good(t *testing.T) {
|
||||
func TestApproval_ApprovalPendingSeq_Good(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
q.Submit("Clotho", CapMergePR, "host-uk/core")
|
||||
q.Submit("Hypnos", CapMergePR, "host-uk/docs")
|
||||
|
|
@ -219,7 +219,7 @@ func TestApprovalPendingSeq_Good(t *testing.T) {
|
|||
|
||||
// --- Concurrent operations ---
|
||||
|
||||
func TestApprovalConcurrent_Good(t *testing.T) {
|
||||
func TestApproval_ApprovalConcurrent_Good(t *testing.T) {
|
||||
q := NewApprovalQueue()
|
||||
|
||||
const n = 10
|
||||
|
|
@ -270,7 +270,7 @@ func TestApprovalConcurrent_Good(t *testing.T) {
|
|||
|
||||
// --- Integration: PolicyEngine + ApprovalQueue ---
|
||||
|
||||
func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) {
|
||||
func TestApproval_ApprovalWorkflow_Good_EndToEnd(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
q := NewApprovalQueue()
|
||||
|
||||
|
|
@ -293,7 +293,7 @@ func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) {
|
|||
assert.Equal(t, "Virgil", req.ReviewedBy)
|
||||
}
|
||||
|
||||
func TestApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) {
|
||||
func TestApproval_ApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
q := NewApprovalQueue()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
// AuditEntry records a single policy evaluation for compliance.
|
||||
// Usage: use AuditEntry with the other exported helpers in this package.
|
||||
type AuditEntry struct {
|
||||
// Timestamp is when the evaluation occurred.
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
|
@ -27,6 +28,7 @@ type AuditEntry struct {
|
|||
}
|
||||
|
||||
// MarshalJSON implements custom JSON encoding for Decision.
|
||||
// Usage: call MarshalJSON(...) during the package's normal workflow.
|
||||
func (d Decision) MarshalJSON() ([]byte, error) {
|
||||
result := core.JSONMarshal(d.String())
|
||||
if !result.OK {
|
||||
|
|
@ -37,6 +39,7 @@ func (d Decision) MarshalJSON() ([]byte, error) {
|
|||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON decoding for Decision.
|
||||
// Usage: call UnmarshalJSON(...) during the package's normal workflow.
|
||||
func (d *Decision) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
result := core.JSONUnmarshal(data, &s)
|
||||
|
|
@ -58,6 +61,7 @@ func (d *Decision) UnmarshalJSON(data []byte) error {
|
|||
}
|
||||
|
||||
// AuditLog is an append-only log of policy evaluations.
|
||||
// Usage: use AuditLog with the other exported helpers in this package.
|
||||
type AuditLog struct {
|
||||
mu sync.Mutex
|
||||
entries []AuditEntry
|
||||
|
|
@ -66,6 +70,7 @@ type AuditLog struct {
|
|||
|
||||
// NewAuditLog creates an in-memory audit log. If a writer is provided,
|
||||
// each entry is also written as a JSON line to that writer (append-only).
|
||||
// Usage: call NewAuditLog(...) to create a ready-to-use value.
|
||||
func NewAuditLog(w io.Writer) *AuditLog {
|
||||
return &AuditLog{
|
||||
writer: w,
|
||||
|
|
@ -73,6 +78,7 @@ func NewAuditLog(w io.Writer) *AuditLog {
|
|||
}
|
||||
|
||||
// Record appends an evaluation result to the audit log.
|
||||
// Usage: call Record(...) during the package's normal workflow.
|
||||
func (l *AuditLog) Record(result EvalResult, repo string) error {
|
||||
entry := AuditEntry{
|
||||
Timestamp: time.Now(),
|
||||
|
|
@ -104,6 +110,7 @@ func (l *AuditLog) Record(result EvalResult, repo string) error {
|
|||
}
|
||||
|
||||
// Entries returns a snapshot of all audit entries.
|
||||
// Usage: call Entries(...) during the package's normal workflow.
|
||||
func (l *AuditLog) Entries() []AuditEntry {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
|
@ -114,6 +121,7 @@ func (l *AuditLog) Entries() []AuditEntry {
|
|||
}
|
||||
|
||||
// EntriesSeq returns an iterator over all audit entries.
|
||||
// Usage: call EntriesSeq(...) during the package's normal workflow.
|
||||
func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] {
|
||||
return func(yield func(AuditEntry) bool) {
|
||||
l.mu.Lock()
|
||||
|
|
@ -128,6 +136,7 @@ func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] {
|
|||
}
|
||||
|
||||
// Len returns the number of entries in the log.
|
||||
// Usage: call Len(...) during the package's normal workflow.
|
||||
func (l *AuditLog) Len() int {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
|
@ -135,6 +144,7 @@ func (l *AuditLog) Len() int {
|
|||
}
|
||||
|
||||
// EntriesFor returns all audit entries for a specific agent.
|
||||
// Usage: call EntriesFor(...) during the package's normal workflow.
|
||||
func (l *AuditLog) EntriesFor(agent string) []AuditEntry {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
|
@ -149,6 +159,7 @@ func (l *AuditLog) EntriesFor(agent string) []AuditEntry {
|
|||
}
|
||||
|
||||
// EntriesForSeq returns an iterator over audit entries for a specific agent.
|
||||
// Usage: call EntriesForSeq(...) during the package's normal workflow.
|
||||
func (l *AuditLog) EntriesForSeq(agent string) iter.Seq[AuditEntry] {
|
||||
return func(yield func(AuditEntry) bool) {
|
||||
l.mu.Lock()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
// --- AuditLog basic ---
|
||||
|
||||
func TestAuditRecord_Good(t *testing.T) {
|
||||
func TestAudit_AuditRecord_Good(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
|
||||
result := EvalResult{
|
||||
|
|
@ -26,7 +26,7 @@ func TestAuditRecord_Good(t *testing.T) {
|
|||
assert.Equal(t, 1, log.Len())
|
||||
}
|
||||
|
||||
func TestAuditRecord_Good_EntryFields(t *testing.T) {
|
||||
func TestAudit_AuditRecord_Good_EntryFields(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
|
||||
result := EvalResult{
|
||||
|
|
@ -50,7 +50,7 @@ func TestAuditRecord_Good_EntryFields(t *testing.T) {
|
|||
assert.False(t, e.Timestamp.IsZero())
|
||||
}
|
||||
|
||||
func TestAuditRecord_Good_NoRepo(t *testing.T) {
|
||||
func TestAudit_AuditRecord_Good_NoRepo(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
result := EvalResult{
|
||||
Decision: Allow,
|
||||
|
|
@ -66,7 +66,7 @@ func TestAuditRecord_Good_NoRepo(t *testing.T) {
|
|||
assert.Empty(t, entries[0].Repo)
|
||||
}
|
||||
|
||||
func TestAuditEntries_Good_Snapshot(t *testing.T) {
|
||||
func TestAudit_AuditEntries_Good_Snapshot(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
log.Record(EvalResult{Agent: "A", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
|
||||
|
||||
|
|
@ -78,12 +78,12 @@ func TestAuditEntries_Good_Snapshot(t *testing.T) {
|
|||
assert.Equal(t, "A", log.Entries()[0].Agent)
|
||||
}
|
||||
|
||||
func TestAuditEntries_Good_Empty(t *testing.T) {
|
||||
func TestAudit_AuditEntries_Good_Empty(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
assert.Empty(t, log.Entries())
|
||||
}
|
||||
|
||||
func TestAuditEntries_Good_AppendOnly(t *testing.T) {
|
||||
func TestAudit_AuditEntries_Good_AppendOnly(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
|
||||
for i := range 5 {
|
||||
|
|
@ -99,7 +99,7 @@ func TestAuditEntries_Good_AppendOnly(t *testing.T) {
|
|||
|
||||
// --- EntriesFor ---
|
||||
|
||||
func TestAuditEntriesFor_Good(t *testing.T) {
|
||||
func TestAudit_AuditEntriesFor_Good(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
|
||||
log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
|
||||
|
|
@ -121,7 +121,7 @@ func TestAuditEntriesFor_Good(t *testing.T) {
|
|||
assert.Equal(t, 2, count)
|
||||
}
|
||||
|
||||
func TestAuditEntriesSeq_Good(t *testing.T) {
|
||||
func TestAudit_AuditEntriesSeq_Good(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
|
||||
log.Record(EvalResult{Agent: "Clotho", Cap: CapCreatePR, Decision: Allow, Reason: "ok"}, "")
|
||||
|
|
@ -133,7 +133,7 @@ func TestAuditEntriesSeq_Good(t *testing.T) {
|
|||
assert.Equal(t, 2, count)
|
||||
}
|
||||
|
||||
func TestAuditEntriesFor_Bad_NotFound(t *testing.T) {
|
||||
func TestAudit_AuditEntriesFor_Bad_NotFound(t *testing.T) {
|
||||
log := NewAuditLog(nil)
|
||||
log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ func TestAuditEntriesFor_Bad_NotFound(t *testing.T) {
|
|||
|
||||
// --- Writer output ---
|
||||
|
||||
func TestAuditRecord_Good_WritesToWriter(t *testing.T) {
|
||||
func TestAudit_AuditRecord_Good_WritesToWriter(t *testing.T) {
|
||||
buf := core.NewBuilder()
|
||||
log := NewAuditLog(buf)
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ func TestAuditRecord_Good_WritesToWriter(t *testing.T) {
|
|||
assert.Equal(t, "host-uk/core", entry.Repo)
|
||||
}
|
||||
|
||||
func TestAuditRecord_Good_MultipleLines(t *testing.T) {
|
||||
func TestAudit_AuditRecord_Good_MultipleLines(t *testing.T) {
|
||||
buf := core.NewBuilder()
|
||||
log := NewAuditLog(buf)
|
||||
|
||||
|
|
@ -192,7 +192,7 @@ func TestAuditRecord_Good_MultipleLines(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuditRecord_Bad_WriterError(t *testing.T) {
|
||||
func TestAudit_AuditRecord_Bad_WriterError(t *testing.T) {
|
||||
log := NewAuditLog(&failWriter{})
|
||||
|
||||
result := EvalResult{
|
||||
|
|
@ -218,7 +218,7 @@ func (f *failWriter) Write(_ []byte) (int, error) {
|
|||
|
||||
// --- Decision JSON marshalling ---
|
||||
|
||||
func TestDecisionJSON_Good_RoundTrip(t *testing.T) {
|
||||
func TestAudit_DecisionJSON_Good_RoundTrip(t *testing.T) {
|
||||
decisions := []Decision{Deny, Allow, NeedsApproval}
|
||||
expected := []string{`"deny"`, `"allow"`, `"needs_approval"`}
|
||||
|
||||
|
|
@ -234,7 +234,7 @@ func TestDecisionJSON_Good_RoundTrip(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDecisionJSON_Bad_UnknownString(t *testing.T) {
|
||||
func TestAudit_DecisionJSON_Bad_UnknownString(t *testing.T) {
|
||||
var d Decision
|
||||
result := core.JSONUnmarshal([]byte(`"invalid"`), &d)
|
||||
err, _ := result.Value.(error)
|
||||
|
|
@ -242,7 +242,7 @@ func TestDecisionJSON_Bad_UnknownString(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "unknown decision")
|
||||
}
|
||||
|
||||
func TestDecisionJSON_Bad_NonString(t *testing.T) {
|
||||
func TestAudit_DecisionJSON_Bad_NonString(t *testing.T) {
|
||||
var d Decision
|
||||
result := core.JSONUnmarshal([]byte(`42`), &d)
|
||||
err, _ := result.Value.(error)
|
||||
|
|
@ -251,7 +251,7 @@ func TestDecisionJSON_Bad_NonString(t *testing.T) {
|
|||
|
||||
// --- Concurrent audit logging ---
|
||||
|
||||
func TestAuditConcurrent_Good(t *testing.T) {
|
||||
func TestAudit_AuditConcurrent_Good(t *testing.T) {
|
||||
buf := core.NewBuilder()
|
||||
log := NewAuditLog(buf)
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ func TestAuditConcurrent_Good(t *testing.T) {
|
|||
|
||||
// --- Integration: PolicyEngine + AuditLog ---
|
||||
|
||||
func TestAuditPolicyIntegration_Good(t *testing.T) {
|
||||
func TestAudit_AuditPolicyIntegration_Good(t *testing.T) {
|
||||
buf := core.NewBuilder()
|
||||
log := NewAuditLog(buf)
|
||||
pe := newTestEngine(t)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
// PolicyConfig is the JSON-serialisable representation of a trust policy.
|
||||
// Usage: use PolicyConfig with the other exported helpers in this package.
|
||||
type PolicyConfig struct {
|
||||
Tier int `json:"tier"`
|
||||
Allowed []string `json:"allowed"`
|
||||
|
|
@ -16,11 +17,13 @@ type PolicyConfig struct {
|
|||
}
|
||||
|
||||
// PoliciesConfig is the top-level configuration containing all tier policies.
|
||||
// Usage: use PoliciesConfig with the other exported helpers in this package.
|
||||
type PoliciesConfig struct {
|
||||
Policies []PolicyConfig `json:"policies"`
|
||||
}
|
||||
|
||||
// LoadPoliciesFromFile reads a JSON file and returns parsed policies.
|
||||
// Usage: call LoadPoliciesFromFile(...) during the package's normal workflow.
|
||||
func LoadPoliciesFromFile(path string) ([]Policy, error) {
|
||||
openResult := (&core.Fs{}).New("/").Open(path)
|
||||
if !openResult.OK {
|
||||
|
|
@ -31,6 +34,7 @@ func LoadPoliciesFromFile(path string) ([]Policy, error) {
|
|||
}
|
||||
|
||||
// LoadPolicies reads JSON from a reader and returns parsed policies.
|
||||
// Usage: call LoadPolicies(...) during the package's normal workflow.
|
||||
func LoadPolicies(r io.Reader) ([]Policy, error) {
|
||||
readResult := core.ReadAll(r)
|
||||
if !readResult.OK {
|
||||
|
|
@ -76,6 +80,7 @@ func convertPolicies(cfg PoliciesConfig) ([]Policy, error) {
|
|||
|
||||
// ApplyPolicies loads policies from a reader and sets them on the engine,
|
||||
// replacing any existing policies for the same tiers.
|
||||
// Usage: call ApplyPolicies(...) during the package's normal workflow.
|
||||
func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error {
|
||||
policies, err := LoadPolicies(r)
|
||||
if err != nil {
|
||||
|
|
@ -90,6 +95,7 @@ func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error {
|
|||
}
|
||||
|
||||
// ApplyPoliciesFromFile loads policies from a JSON file and sets them on the engine.
|
||||
// Usage: call ApplyPoliciesFromFile(...) during the package's normal workflow.
|
||||
func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error {
|
||||
openResult := (&core.Fs{}).New("/").Open(path)
|
||||
if !openResult.OK {
|
||||
|
|
@ -100,6 +106,7 @@ func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error {
|
|||
}
|
||||
|
||||
// ExportPolicies serialises the current policies as JSON to the given writer.
|
||||
// Usage: call ExportPolicies(...) during the package's normal workflow.
|
||||
func (pe *PolicyEngine) ExportPolicies(w io.Writer) error {
|
||||
var cfg PoliciesConfig
|
||||
for _, tier := range []Tier{TierUntrusted, TierVerified, TierFull} {
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ const validPolicyJSON = `{
|
|||
|
||||
// --- LoadPolicies ---
|
||||
|
||||
func TestLoadPolicies_Good(t *testing.T) {
|
||||
func TestConfig_LoadPolicies_Good(t *testing.T) {
|
||||
policies, err := LoadPolicies(core.NewReader(validPolicyJSON))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, policies, 3)
|
||||
}
|
||||
|
||||
func TestLoadPolicies_Good_FieldMapping(t *testing.T) {
|
||||
func TestConfig_LoadPolicies_Good_FieldMapping(t *testing.T) {
|
||||
policies, err := LoadPolicies(core.NewReader(validPolicyJSON))
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -60,33 +60,33 @@ func TestLoadPolicies_Good_FieldMapping(t *testing.T) {
|
|||
assert.Len(t, policies[2].Denied, 2)
|
||||
}
|
||||
|
||||
func TestLoadPolicies_Good_EmptyPolicies(t *testing.T) {
|
||||
func TestConfig_LoadPolicies_Good_EmptyPolicies(t *testing.T) {
|
||||
input := `{"policies": []}`
|
||||
policies, err := LoadPolicies(core.NewReader(input))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, policies)
|
||||
}
|
||||
|
||||
func TestLoadPolicies_Bad_InvalidJSON(t *testing.T) {
|
||||
func TestConfig_LoadPolicies_Bad_InvalidJSON(t *testing.T) {
|
||||
_, err := LoadPolicies(core.NewReader(`{invalid`))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLoadPolicies_Bad_InvalidTier(t *testing.T) {
|
||||
func TestConfig_LoadPolicies_Bad_InvalidTier(t *testing.T) {
|
||||
input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}`
|
||||
_, err := LoadPolicies(core.NewReader(input))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid tier")
|
||||
}
|
||||
|
||||
func TestLoadPolicies_Bad_TierTooHigh(t *testing.T) {
|
||||
func TestConfig_LoadPolicies_Bad_TierTooHigh(t *testing.T) {
|
||||
input := `{"policies": [{"tier": 99, "allowed": ["repo.push"]}]}`
|
||||
_, err := LoadPolicies(core.NewReader(input))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid tier")
|
||||
}
|
||||
|
||||
func TestLoadPolicies_Bad_UnknownField(t *testing.T) {
|
||||
func TestConfig_LoadPolicies_Bad_UnknownField(t *testing.T) {
|
||||
input := `{"policies": [{"tier": 1, "allowed": ["repo.push"], "bogus": true}]}`
|
||||
_, err := LoadPolicies(core.NewReader(input))
|
||||
assert.Error(t, err, "DisallowUnknownFields should reject unknown fields")
|
||||
|
|
@ -94,7 +94,7 @@ func TestLoadPolicies_Bad_UnknownField(t *testing.T) {
|
|||
|
||||
// --- LoadPoliciesFromFile ---
|
||||
|
||||
func TestLoadPoliciesFromFile_Good(t *testing.T) {
|
||||
func TestConfig_LoadPoliciesFromFile_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := core.Path(dir, "policies.json")
|
||||
writePolicyFile(t, path, validPolicyJSON)
|
||||
|
|
@ -104,14 +104,14 @@ func TestLoadPoliciesFromFile_Good(t *testing.T) {
|
|||
assert.Len(t, policies, 3)
|
||||
}
|
||||
|
||||
func TestLoadPoliciesFromFile_Bad_NotFound(t *testing.T) {
|
||||
func TestConfig_LoadPoliciesFromFile_Bad_NotFound(t *testing.T) {
|
||||
_, err := LoadPoliciesFromFile("/nonexistent/path/policies.json")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- ApplyPolicies ---
|
||||
|
||||
func TestApplyPolicies_Good(t *testing.T) {
|
||||
func TestConfig_ApplyPolicies_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "TestAgent", Tier: TierVerified}))
|
||||
pe := NewPolicyEngine(r)
|
||||
|
|
@ -135,7 +135,7 @@ func TestApplyPolicies_Good(t *testing.T) {
|
|||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) {
|
||||
func TestConfig_ApplyPolicies_Bad_InvalidJSON(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
pe := NewPolicyEngine(r)
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestApplyPolicies_Bad_InvalidTier(t *testing.T) {
|
||||
func TestConfig_ApplyPolicies_Bad_InvalidTier(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
pe := NewPolicyEngine(r)
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ func TestApplyPolicies_Bad_InvalidTier(t *testing.T) {
|
|||
|
||||
// --- ApplyPoliciesFromFile ---
|
||||
|
||||
func TestApplyPoliciesFromFile_Good(t *testing.T) {
|
||||
func TestConfig_ApplyPoliciesFromFile_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := core.Path(dir, "policies.json")
|
||||
writePolicyFile(t, path, validPolicyJSON)
|
||||
|
|
@ -172,7 +172,7 @@ func TestApplyPoliciesFromFile_Good(t *testing.T) {
|
|||
assert.Len(t, p.Allowed, 3)
|
||||
}
|
||||
|
||||
func TestApplyPoliciesFromFile_Bad_NotFound(t *testing.T) {
|
||||
func TestConfig_ApplyPoliciesFromFile_Bad_NotFound(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
pe := NewPolicyEngine(r)
|
||||
err := pe.ApplyPoliciesFromFile("/nonexistent/policies.json")
|
||||
|
|
@ -181,7 +181,7 @@ func TestApplyPoliciesFromFile_Bad_NotFound(t *testing.T) {
|
|||
|
||||
// --- ExportPolicies ---
|
||||
|
||||
func TestExportPolicies_Good(t *testing.T) {
|
||||
func TestConfig_ExportPolicies_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
pe := NewPolicyEngine(r) // loads defaults
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ func TestExportPolicies_Good(t *testing.T) {
|
|||
assert.Len(t, cfg.Policies, 3)
|
||||
}
|
||||
|
||||
func TestExportPolicies_Good_RoundTrip(t *testing.T) {
|
||||
func TestConfig_ExportPolicies_Good_RoundTrip(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "A", Tier: TierFull}))
|
||||
pe := NewPolicyEngine(r)
|
||||
|
|
@ -232,26 +232,26 @@ func writePolicyFile(t *testing.T, path, content string) {
|
|||
|
||||
// --- Helper conversion ---
|
||||
|
||||
func TestToCapabilities_Good(t *testing.T) {
|
||||
func TestConfig_ToCapabilities_Good(t *testing.T) {
|
||||
caps := toCapabilities([]string{"repo.push", "pr.merge"})
|
||||
assert.Len(t, caps, 2)
|
||||
assert.Equal(t, CapPushRepo, caps[0])
|
||||
assert.Equal(t, CapMergePR, caps[1])
|
||||
}
|
||||
|
||||
func TestToCapabilities_Good_Empty(t *testing.T) {
|
||||
func TestConfig_ToCapabilities_Good_Empty(t *testing.T) {
|
||||
assert.Nil(t, toCapabilities(nil))
|
||||
assert.Nil(t, toCapabilities([]string{}))
|
||||
}
|
||||
|
||||
func TestFromCapabilities_Good(t *testing.T) {
|
||||
func TestConfig_FromCapabilities_Good(t *testing.T) {
|
||||
ss := fromCapabilities([]Capability{CapPushRepo, CapMergePR})
|
||||
assert.Len(t, ss, 2)
|
||||
assert.Equal(t, "repo.push", ss[0])
|
||||
assert.Equal(t, "pr.merge", ss[1])
|
||||
}
|
||||
|
||||
func TestFromCapabilities_Good_Empty(t *testing.T) {
|
||||
func TestConfig_FromCapabilities_Good_Empty(t *testing.T) {
|
||||
assert.Nil(t, fromCapabilities(nil))
|
||||
assert.Nil(t, fromCapabilities([]Capability{}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
// Policy defines the access rules for a given trust tier.
|
||||
// Usage: use Policy with the other exported helpers in this package.
|
||||
type Policy struct {
|
||||
// Tier is the trust level this policy applies to.
|
||||
Tier Tier
|
||||
|
|
@ -20,24 +21,30 @@ type Policy struct {
|
|||
}
|
||||
|
||||
// PolicyEngine evaluates capability requests against registered policies.
|
||||
// Usage: use PolicyEngine with the other exported helpers in this package.
|
||||
type PolicyEngine struct {
|
||||
registry *Registry
|
||||
policies map[Tier]*Policy
|
||||
}
|
||||
|
||||
// Decision is the result of a policy evaluation.
|
||||
// Usage: use Decision with the other exported helpers in this package.
|
||||
type Decision int
|
||||
|
||||
const (
|
||||
// Deny means the action is not permitted.
|
||||
// Usage: compare or pass Deny when using the related package APIs.
|
||||
Deny Decision = iota
|
||||
// Allow means the action is permitted.
|
||||
// Usage: compare or pass Allow when using the related package APIs.
|
||||
Allow
|
||||
// NeedsApproval means the action requires human or higher-tier approval.
|
||||
// Usage: compare or pass NeedsApproval when using the related package APIs.
|
||||
NeedsApproval
|
||||
)
|
||||
|
||||
// String returns the human-readable name of the decision.
|
||||
// Usage: call String(...) during the package's normal workflow.
|
||||
func (d Decision) String() string {
|
||||
switch d {
|
||||
case Deny:
|
||||
|
|
@ -52,6 +59,7 @@ func (d Decision) String() string {
|
|||
}
|
||||
|
||||
// EvalResult contains the outcome of a capability evaluation.
|
||||
// Usage: use EvalResult with the other exported helpers in this package.
|
||||
type EvalResult struct {
|
||||
Decision Decision
|
||||
Agent string
|
||||
|
|
@ -60,6 +68,7 @@ type EvalResult struct {
|
|||
}
|
||||
|
||||
// NewPolicyEngine creates a policy engine with the given registry and default policies.
|
||||
// Usage: call NewPolicyEngine(...) to create a ready-to-use value.
|
||||
func NewPolicyEngine(registry *Registry) *PolicyEngine {
|
||||
pe := &PolicyEngine{
|
||||
registry: registry,
|
||||
|
|
@ -72,6 +81,7 @@ func NewPolicyEngine(registry *Registry) *PolicyEngine {
|
|||
// Evaluate checks whether the named agent can perform the given capability.
|
||||
// If the agent has scoped repos and the capability is repo-scoped, the repo
|
||||
// parameter is checked against the agent's allowed repos.
|
||||
// Usage: call Evaluate(...) during the package's normal workflow.
|
||||
func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) EvalResult {
|
||||
agent := pe.registry.Get(agentName)
|
||||
if agent == nil {
|
||||
|
|
@ -145,6 +155,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string)
|
|||
}
|
||||
|
||||
// SetPolicy replaces the policy for a given tier.
|
||||
// Usage: call SetPolicy(...) during the package's normal workflow.
|
||||
func (pe *PolicyEngine) SetPolicy(p Policy) error {
|
||||
if !p.Tier.Valid() {
|
||||
return coreerr.E("trust.SetPolicy", core.Sprintf("invalid tier %d", p.Tier), nil)
|
||||
|
|
@ -154,6 +165,7 @@ func (pe *PolicyEngine) SetPolicy(p Policy) error {
|
|||
}
|
||||
|
||||
// GetPolicy returns the policy for a tier, or nil if none is set.
|
||||
// Usage: call GetPolicy(...) during the package's normal workflow.
|
||||
func (pe *PolicyEngine) GetPolicy(t Tier) *Policy {
|
||||
return pe.policies[t]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,19 +29,19 @@ func newTestEngine(t *testing.T) *PolicyEngine {
|
|||
|
||||
// --- Decision ---
|
||||
|
||||
func TestDecisionString_Good(t *testing.T) {
|
||||
func TestPolicy_DecisionString_Good(t *testing.T) {
|
||||
assert.Equal(t, "deny", Deny.String())
|
||||
assert.Equal(t, "allow", Allow.String())
|
||||
assert.Equal(t, "needs_approval", NeedsApproval.String())
|
||||
}
|
||||
|
||||
func TestDecisionString_Bad_Unknown(t *testing.T) {
|
||||
func TestPolicy_DecisionString_Bad_Unknown(t *testing.T) {
|
||||
assert.Contains(t, Decision(99).String(), "unknown")
|
||||
}
|
||||
|
||||
// --- Tier 3 (Full Trust) ---
|
||||
|
||||
func TestEvaluate_Good_Tier3CanDoAnything(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier3CanDoAnything(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
|
||||
caps := []Capability{
|
||||
|
|
@ -57,56 +57,56 @@ func TestEvaluate_Good_Tier3CanDoAnything(t *testing.T) {
|
|||
|
||||
// --- Tier 2 (Verified) ---
|
||||
|
||||
func TestEvaluate_Good_Tier2CanCreatePR(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier2CanCreatePR(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Clotho", CapCreatePR, "host-uk/core")
|
||||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/core")
|
||||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Clotho", CapMergePR, "host-uk/core")
|
||||
assert.Equal(t, NeedsApproval, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Good_Tier2CanCreateIssue(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier2CanCreateIssue(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Clotho", CapCreateIssue, "")
|
||||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Clotho", CapAccessWorkspace, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier2CannotModifyFlows(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier2CannotModifyFlows(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Clotho", CapModifyFlows, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Clotho", CapRunPrivileged, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/secret-repo")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
assert.Contains(t, result.Reason, "does not have access")
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
// Push without specifying a repo should be denied for scoped agents.
|
||||
result := pe.Evaluate("Clotho", CapPushRepo, "")
|
||||
|
|
@ -115,43 +115,43 @@ func TestEvaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) {
|
|||
|
||||
// --- Tier 1 (Untrusted) ---
|
||||
|
||||
func TestEvaluate_Good_Tier1CanCreatePR(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier1CanCreatePR(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("BugSETI-001", CapCreatePR, "")
|
||||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Good_Tier1CanCommentIssue(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier1CanCommentIssue(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("BugSETI-001", CapCommentIssue, "")
|
||||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier1CannotPush(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier1CannotPush(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("BugSETI-001", CapPushRepo, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier1CannotMerge(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier1CannotMerge(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("BugSETI-001", CapMergePR, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier1CannotCreateIssue(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier1CannotCreateIssue(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("BugSETI-001", CapCreateIssue, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier1CannotReadSecrets(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier1CannotReadSecrets(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("BugSETI-001", CapReadSecrets, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("BugSETI-001", CapRunPrivileged, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
|
|
@ -159,14 +159,14 @@ func TestEvaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) {
|
|||
|
||||
// --- Edge cases ---
|
||||
|
||||
func TestEvaluate_Bad_UnknownAgent(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_UnknownAgent(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Unknown", CapCreatePR, "")
|
||||
assert.Equal(t, Deny, result.Decision)
|
||||
assert.Contains(t, result.Reason, "not registered")
|
||||
}
|
||||
|
||||
func TestEvaluate_Good_EvalResultFields(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_EvalResultFields(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
result := pe.Evaluate("Athena", CapPushRepo, "")
|
||||
assert.Equal(t, "Athena", result.Agent)
|
||||
|
|
@ -176,7 +176,7 @@ func TestEvaluate_Good_EvalResultFields(t *testing.T) {
|
|||
|
||||
// --- SetPolicy ---
|
||||
|
||||
func TestSetPolicy_Good(t *testing.T) {
|
||||
func TestPolicy_SetPolicy_Good(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
err := pe.SetPolicy(Policy{
|
||||
Tier: TierVerified,
|
||||
|
|
@ -189,64 +189,64 @@ func TestSetPolicy_Good(t *testing.T) {
|
|||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestSetPolicy_Bad_InvalidTier(t *testing.T) {
|
||||
func TestPolicy_SetPolicy_Bad_InvalidTier(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
err := pe.SetPolicy(Policy{Tier: Tier(0)})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid tier")
|
||||
}
|
||||
|
||||
func TestGetPolicy_Good(t *testing.T) {
|
||||
func TestPolicy_GetPolicy_Good(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
p := pe.GetPolicy(TierFull)
|
||||
require.NotNil(t, p)
|
||||
assert.Equal(t, TierFull, p.Tier)
|
||||
}
|
||||
|
||||
func TestGetPolicy_Bad_NotFound(t *testing.T) {
|
||||
func TestPolicy_GetPolicy_Bad_NotFound(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
assert.Nil(t, pe.GetPolicy(Tier(99)))
|
||||
}
|
||||
|
||||
// --- isRepoScoped / repoAllowed helpers ---
|
||||
|
||||
func TestIsRepoScoped_Good(t *testing.T) {
|
||||
func TestPolicy_IsRepoScoped_Good(t *testing.T) {
|
||||
assert.True(t, isRepoScoped(CapPushRepo))
|
||||
assert.True(t, isRepoScoped(CapCreatePR))
|
||||
assert.True(t, isRepoScoped(CapMergePR))
|
||||
assert.True(t, isRepoScoped(CapReadSecrets))
|
||||
}
|
||||
|
||||
func TestIsRepoScoped_Bad_NotScoped(t *testing.T) {
|
||||
func TestPolicy_IsRepoScoped_Bad_NotScoped(t *testing.T) {
|
||||
assert.False(t, isRepoScoped(CapRunPrivileged))
|
||||
assert.False(t, isRepoScoped(CapAccessWorkspace))
|
||||
assert.False(t, isRepoScoped(CapModifyFlows))
|
||||
}
|
||||
|
||||
func TestRepoAllowed_Good(t *testing.T) {
|
||||
func TestPolicy_RepoAllowed_Good(t *testing.T) {
|
||||
scoped := []string{"host-uk/core", "host-uk/docs"}
|
||||
assert.True(t, repoAllowed(scoped, "host-uk/core"))
|
||||
assert.True(t, repoAllowed(scoped, "host-uk/docs"))
|
||||
}
|
||||
|
||||
func TestRepoAllowed_Bad_NotInScope(t *testing.T) {
|
||||
func TestPolicy_RepoAllowed_Bad_NotInScope(t *testing.T) {
|
||||
scoped := []string{"host-uk/core"}
|
||||
assert.False(t, repoAllowed(scoped, "host-uk/secret"))
|
||||
}
|
||||
|
||||
func TestRepoAllowed_Bad_EmptyRepo(t *testing.T) {
|
||||
func TestPolicy_RepoAllowed_Bad_EmptyRepo(t *testing.T) {
|
||||
scoped := []string{"host-uk/core"}
|
||||
assert.False(t, repoAllowed(scoped, ""))
|
||||
}
|
||||
|
||||
func TestRepoAllowed_Bad_EmptyScope(t *testing.T) {
|
||||
func TestPolicy_RepoAllowed_Bad_EmptyScope(t *testing.T) {
|
||||
assert.False(t, repoAllowed(nil, "host-uk/core"))
|
||||
assert.False(t, repoAllowed([]string{}, "host-uk/core"))
|
||||
}
|
||||
|
||||
// --- Tier 3 ignores repo scoping ---
|
||||
|
||||
func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier3IgnoresRepoScope(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "Virgil",
|
||||
|
|
@ -261,7 +261,7 @@ func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) {
|
|||
|
||||
// --- Default rate limits ---
|
||||
|
||||
func TestDefaultRateLimit_Good(t *testing.T) {
|
||||
func TestPolicy_DefaultRateLimit_Good(t *testing.T) {
|
||||
assert.Equal(t, 10, defaultRateLimit(TierUntrusted))
|
||||
assert.Equal(t, 60, defaultRateLimit(TierVerified))
|
||||
assert.Equal(t, 0, defaultRateLimit(TierFull))
|
||||
|
|
@ -270,11 +270,11 @@ func TestDefaultRateLimit_Good(t *testing.T) {
|
|||
|
||||
// --- Phase 0 Additions ---
|
||||
|
||||
// TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2
|
||||
// TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2
|
||||
// agent with empty ScopedRepos is treated as "unrestricted" for repo-scoped
|
||||
// capabilities. NOTE: This is a potential security concern documented in
|
||||
// FINDINGS.md — empty ScopedRepos bypasses the repo scope check entirely.
|
||||
func TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "Hypnos",
|
||||
|
|
@ -301,9 +301,9 @@ func TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) {
|
|||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
// TestEvaluate_Bad_CapabilityNotInAnyList verifies that a capability not in
|
||||
// TestPolicy_Evaluate_Bad_CapabilityNotInAnyList verifies that a capability not in
|
||||
// allowed, denied, or requires_approval lists defaults to deny.
|
||||
func TestEvaluate_Bad_CapabilityNotInAnyList(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_CapabilityNotInAnyList(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "TestAgent",
|
||||
|
|
@ -325,9 +325,9 @@ func TestEvaluate_Bad_CapabilityNotInAnyList(t *testing.T) {
|
|||
assert.Contains(t, result.Reason, "not granted")
|
||||
}
|
||||
|
||||
// TestEvaluate_Bad_UnknownCapability verifies that a completely invented
|
||||
// TestPolicy_Evaluate_Bad_UnknownCapability verifies that a completely invented
|
||||
// capability string is denied.
|
||||
func TestEvaluate_Bad_UnknownCapability(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_UnknownCapability(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
|
||||
result := pe.Evaluate("Athena", Capability("nonexistent.capability"), "")
|
||||
|
|
@ -335,9 +335,9 @@ func TestEvaluate_Bad_UnknownCapability(t *testing.T) {
|
|||
assert.Contains(t, result.Reason, "not granted")
|
||||
}
|
||||
|
||||
// TestConcurrentEvaluate_Good verifies that concurrent policy evaluations
|
||||
// TestPolicy_ConcurrentEvaluate_Good verifies that concurrent policy evaluations
|
||||
// with 10 goroutines do not race.
|
||||
func TestConcurrentEvaluate_Good(t *testing.T) {
|
||||
func TestPolicy_ConcurrentEvaluate_Good(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
|
||||
const n = 10
|
||||
|
|
@ -360,10 +360,10 @@ func TestConcurrentEvaluate_Good(t *testing.T) {
|
|||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestEvaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that
|
||||
// TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that
|
||||
// a scoped agent requesting a repo-scoped capability without specifying
|
||||
// the repo is denied.
|
||||
func TestEvaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) {
|
||||
func TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) {
|
||||
pe := newTestEngine(t)
|
||||
|
||||
// Clotho has ScopedRepos but passes empty repo
|
||||
|
|
|
|||
|
|
@ -9,100 +9,100 @@ import (
|
|||
|
||||
// --- matchScope ---
|
||||
|
||||
func TestMatchScope_Good_ExactMatch(t *testing.T) {
|
||||
func TestScope_MatchScope_Good_ExactMatch(t *testing.T) {
|
||||
assert.True(t, matchScope("host-uk/core", "host-uk/core"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Good_SingleWildcard(t *testing.T) {
|
||||
func TestScope_MatchScope_Good_SingleWildcard(t *testing.T) {
|
||||
assert.True(t, matchScope("core/*", "core/php"))
|
||||
assert.True(t, matchScope("core/*", "core/go-crypt"))
|
||||
assert.True(t, matchScope("host-uk/*", "host-uk/core"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Good_RecursiveWildcard(t *testing.T) {
|
||||
func TestScope_MatchScope_Good_RecursiveWildcard(t *testing.T) {
|
||||
assert.True(t, matchScope("core/**", "core/php"))
|
||||
assert.True(t, matchScope("core/**", "core/php/sub"))
|
||||
assert.True(t, matchScope("core/**", "core/a/b/c"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Bad_ExactMismatch(t *testing.T) {
|
||||
func TestScope_MatchScope_Bad_ExactMismatch(t *testing.T) {
|
||||
assert.False(t, matchScope("host-uk/core", "host-uk/docs"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Bad_SingleWildcardNoNested(t *testing.T) {
|
||||
func TestScope_MatchScope_Bad_SingleWildcardNoNested(t *testing.T) {
|
||||
// "core/*" should NOT match "core/php/sub" — only single level.
|
||||
assert.False(t, matchScope("core/*", "core/php/sub"))
|
||||
assert.False(t, matchScope("core/*", "core/a/b"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) {
|
||||
func TestScope_MatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) {
|
||||
// "core/*" should NOT match "other/php".
|
||||
assert.False(t, matchScope("core/*", "other/php"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) {
|
||||
func TestScope_MatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) {
|
||||
assert.False(t, matchScope("core/**", "other/php"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Bad_EmptyRepo(t *testing.T) {
|
||||
func TestScope_MatchScope_Bad_EmptyRepo(t *testing.T) {
|
||||
assert.False(t, matchScope("core/*", ""))
|
||||
}
|
||||
|
||||
func TestMatchScope_Bad_WildcardInMiddle(t *testing.T) {
|
||||
func TestScope_MatchScope_Bad_WildcardInMiddle(t *testing.T) {
|
||||
// Wildcard not at the end — should not match.
|
||||
assert.False(t, matchScope("core/*/sub", "core/php/sub"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Bad_WildcardOnlyPrefix(t *testing.T) {
|
||||
func TestScope_MatchScope_Bad_WildcardOnlyPrefix(t *testing.T) {
|
||||
// "core/*" should not match the prefix itself.
|
||||
assert.False(t, matchScope("core/*", "core"))
|
||||
assert.False(t, matchScope("core/*", "core/"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) {
|
||||
func TestScope_MatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) {
|
||||
// "core/**" should also match single-level children.
|
||||
assert.True(t, matchScope("core/**", "core/php"))
|
||||
}
|
||||
|
||||
func TestMatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) {
|
||||
func TestScope_MatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) {
|
||||
assert.False(t, matchScope("core/**", "core"))
|
||||
assert.False(t, matchScope("core/**", "corefoo"))
|
||||
}
|
||||
|
||||
// --- repoAllowed with wildcards ---
|
||||
|
||||
func TestRepoAllowedWildcard_Good(t *testing.T) {
|
||||
func TestScope_RepoAllowedWildcard_Good(t *testing.T) {
|
||||
scoped := []string{"core/*", "host-uk/docs"}
|
||||
assert.True(t, repoAllowed(scoped, "core/php"))
|
||||
assert.True(t, repoAllowed(scoped, "core/go-crypt"))
|
||||
assert.True(t, repoAllowed(scoped, "host-uk/docs"))
|
||||
}
|
||||
|
||||
func TestRepoAllowedWildcard_Good_Recursive(t *testing.T) {
|
||||
func TestScope_RepoAllowedWildcard_Good_Recursive(t *testing.T) {
|
||||
scoped := []string{"core/**"}
|
||||
assert.True(t, repoAllowed(scoped, "core/php"))
|
||||
assert.True(t, repoAllowed(scoped, "core/php/sub"))
|
||||
}
|
||||
|
||||
func TestRepoAllowedWildcard_Bad_NoMatch(t *testing.T) {
|
||||
func TestScope_RepoAllowedWildcard_Bad_NoMatch(t *testing.T) {
|
||||
scoped := []string{"core/*"}
|
||||
assert.False(t, repoAllowed(scoped, "other/repo"))
|
||||
assert.False(t, repoAllowed(scoped, "core/php/sub"))
|
||||
}
|
||||
|
||||
func TestRepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) {
|
||||
func TestScope_RepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) {
|
||||
scoped := []string{"core/*"}
|
||||
assert.False(t, repoAllowed(scoped, ""))
|
||||
}
|
||||
|
||||
func TestRepoAllowedWildcard_Bad_EmptyScope(t *testing.T) {
|
||||
func TestScope_RepoAllowedWildcard_Bad_EmptyScope(t *testing.T) {
|
||||
assert.False(t, repoAllowed(nil, "core/php"))
|
||||
assert.False(t, repoAllowed([]string{}, "core/php"))
|
||||
}
|
||||
|
||||
// --- Integration: PolicyEngine with wildcard scopes ---
|
||||
|
||||
func TestEvaluateWildcardScope_Good_SingleLevel(t *testing.T) {
|
||||
func TestScope_EvaluateWildcardScope_Good_SingleLevel(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "WildAgent",
|
||||
|
|
@ -118,7 +118,7 @@ func TestEvaluateWildcardScope_Good_SingleLevel(t *testing.T) {
|
|||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluateWildcardScope_Bad_OutOfScope(t *testing.T) {
|
||||
func TestScope_EvaluateWildcardScope_Bad_OutOfScope(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "WildAgent",
|
||||
|
|
@ -132,7 +132,7 @@ func TestEvaluateWildcardScope_Bad_OutOfScope(t *testing.T) {
|
|||
assert.Contains(t, result.Reason, "does not have access")
|
||||
}
|
||||
|
||||
func TestEvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) {
|
||||
func TestScope_EvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "WildAgent",
|
||||
|
|
@ -145,7 +145,7 @@ func TestEvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) {
|
|||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) {
|
||||
func TestScope_EvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "DeepAgent",
|
||||
|
|
@ -158,7 +158,7 @@ func TestEvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) {
|
|||
assert.Equal(t, Allow, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) {
|
||||
func TestScope_EvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "MixedAgent",
|
||||
|
|
@ -180,7 +180,7 @@ func TestEvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) {
|
|||
assert.Equal(t, Deny, result.Decision)
|
||||
}
|
||||
|
||||
func TestEvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) {
|
||||
func TestScope_EvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{
|
||||
Name: "ScopedSecrets",
|
||||
|
|
|
|||
|
|
@ -20,18 +20,23 @@ import (
|
|||
)
|
||||
|
||||
// Tier represents an agent's trust level in the system.
|
||||
// Usage: use Tier with the other exported helpers in this package.
|
||||
type Tier int
|
||||
|
||||
const (
|
||||
// TierUntrusted is for external/community agents with minimal access.
|
||||
// Usage: compare or pass TierUntrusted when using the related package APIs.
|
||||
TierUntrusted Tier = 1
|
||||
// TierVerified is for partner agents with scoped access.
|
||||
// Usage: compare or pass TierVerified when using the related package APIs.
|
||||
TierVerified Tier = 2
|
||||
// TierFull is for internal agents with full access.
|
||||
// Usage: compare or pass TierFull when using the related package APIs.
|
||||
TierFull Tier = 3
|
||||
)
|
||||
|
||||
// String returns the human-readable name of the tier.
|
||||
// Usage: call String(...) during the package's normal workflow.
|
||||
func (t Tier) String() string {
|
||||
switch t {
|
||||
case TierUntrusted:
|
||||
|
|
@ -46,26 +51,47 @@ func (t Tier) String() string {
|
|||
}
|
||||
|
||||
// Valid returns true if the tier is a recognised trust level.
|
||||
// Usage: call Valid(...) during the package's normal workflow.
|
||||
func (t Tier) Valid() bool {
|
||||
return t >= TierUntrusted && t <= TierFull
|
||||
}
|
||||
|
||||
// Capability represents a specific action an agent can perform.
|
||||
// Usage: use Capability with the other exported helpers in this package.
|
||||
type Capability string
|
||||
|
||||
const (
|
||||
CapPushRepo Capability = "repo.push"
|
||||
CapMergePR Capability = "pr.merge"
|
||||
CapCreatePR Capability = "pr.create"
|
||||
CapCreateIssue Capability = "issue.create"
|
||||
CapCommentIssue Capability = "issue.comment"
|
||||
CapReadSecrets Capability = "secrets.read"
|
||||
CapRunPrivileged Capability = "cmd.privileged"
|
||||
// CapPushRepo allows pushing commits to a repository.
|
||||
// Usage: pass CapPushRepo to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapPushRepo Capability = "repo.push"
|
||||
// CapMergePR allows merging a pull request.
|
||||
// Usage: pass CapMergePR to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapMergePR Capability = "pr.merge"
|
||||
// CapCreatePR allows creating a pull request.
|
||||
// Usage: pass CapCreatePR to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapCreatePR Capability = "pr.create"
|
||||
// CapCreateIssue allows creating an issue.
|
||||
// Usage: pass CapCreateIssue to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapCreateIssue Capability = "issue.create"
|
||||
// CapCommentIssue allows commenting on an issue.
|
||||
// Usage: pass CapCommentIssue to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapCommentIssue Capability = "issue.comment"
|
||||
// CapReadSecrets allows reading secret material.
|
||||
// Usage: pass CapReadSecrets to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapReadSecrets Capability = "secrets.read"
|
||||
// CapRunPrivileged allows running privileged commands.
|
||||
// Usage: pass CapRunPrivileged to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapRunPrivileged Capability = "cmd.privileged"
|
||||
// CapAccessWorkspace allows accessing the workspace filesystem.
|
||||
// Usage: pass CapAccessWorkspace to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapAccessWorkspace Capability = "workspace.access"
|
||||
CapModifyFlows Capability = "flows.modify"
|
||||
// CapModifyFlows allows modifying workflow definitions.
|
||||
// Usage: pass CapModifyFlows to PolicyEngine.Evaluate or include it in a Policy.
|
||||
CapModifyFlows Capability = "flows.modify"
|
||||
)
|
||||
|
||||
// Agent represents an agent identity in the trust system.
|
||||
// Usage: use Agent with the other exported helpers in this package.
|
||||
type Agent struct {
|
||||
// Name is the unique identifier for the agent (e.g., "Athena", "Clotho").
|
||||
Name string
|
||||
|
|
@ -83,12 +109,14 @@ type Agent struct {
|
|||
}
|
||||
|
||||
// Registry manages agent identities and their trust tiers.
|
||||
// Usage: use Registry with the other exported helpers in this package.
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
agents map[string]*Agent
|
||||
}
|
||||
|
||||
// NewRegistry creates an empty agent registry.
|
||||
// Usage: call NewRegistry(...) to create a ready-to-use value.
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
agents: make(map[string]*Agent),
|
||||
|
|
@ -97,6 +125,7 @@ func NewRegistry() *Registry {
|
|||
|
||||
// Register adds or updates an agent in the registry.
|
||||
// Returns an error if the agent name is empty or the tier is invalid.
|
||||
// Usage: call Register(...) during the package's normal workflow.
|
||||
func (r *Registry) Register(agent Agent) error {
|
||||
if agent.Name == "" {
|
||||
return coreerr.E("trust.Register", "agent name is required", nil)
|
||||
|
|
@ -118,6 +147,7 @@ func (r *Registry) Register(agent Agent) error {
|
|||
}
|
||||
|
||||
// Get returns the agent with the given name, or nil if not found.
|
||||
// Usage: call Get(...) during the package's normal workflow.
|
||||
func (r *Registry) Get(name string) *Agent {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
|
@ -125,6 +155,7 @@ func (r *Registry) Get(name string) *Agent {
|
|||
}
|
||||
|
||||
// Remove deletes an agent from the registry.
|
||||
// Usage: call Remove(...) during the package's normal workflow.
|
||||
func (r *Registry) Remove(name string) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
|
@ -136,6 +167,7 @@ func (r *Registry) Remove(name string) bool {
|
|||
}
|
||||
|
||||
// List returns all registered agents. The returned slice is a snapshot.
|
||||
// Usage: call List(...) during the package's normal workflow.
|
||||
func (r *Registry) List() []Agent {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
|
@ -147,6 +179,7 @@ func (r *Registry) List() []Agent {
|
|||
}
|
||||
|
||||
// ListSeq returns an iterator over all registered agents.
|
||||
// Usage: call ListSeq(...) during the package's normal workflow.
|
||||
func (r *Registry) ListSeq() iter.Seq[Agent] {
|
||||
return func(yield func(Agent) bool) {
|
||||
r.mu.RLock()
|
||||
|
|
@ -160,6 +193,7 @@ func (r *Registry) ListSeq() iter.Seq[Agent] {
|
|||
}
|
||||
|
||||
// Len returns the number of registered agents.
|
||||
// Usage: call Len(...) during the package's normal workflow.
|
||||
func (r *Registry) Len() int {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
|
|
|||
|
|
@ -12,23 +12,23 @@ import (
|
|||
|
||||
// --- Tier ---
|
||||
|
||||
func TestTierString_Good(t *testing.T) {
|
||||
func TestTrust_TierString_Good(t *testing.T) {
|
||||
assert.Equal(t, "untrusted", TierUntrusted.String())
|
||||
assert.Equal(t, "verified", TierVerified.String())
|
||||
assert.Equal(t, "full", TierFull.String())
|
||||
}
|
||||
|
||||
func TestTierString_Bad_Unknown(t *testing.T) {
|
||||
func TestTrust_TierString_Bad_Unknown(t *testing.T) {
|
||||
assert.Contains(t, Tier(99).String(), "unknown")
|
||||
}
|
||||
|
||||
func TestTierValid_Good(t *testing.T) {
|
||||
func TestTrust_TierValid_Good(t *testing.T) {
|
||||
assert.True(t, TierUntrusted.Valid())
|
||||
assert.True(t, TierVerified.Valid())
|
||||
assert.True(t, TierFull.Valid())
|
||||
}
|
||||
|
||||
func TestTierValid_Bad(t *testing.T) {
|
||||
func TestTrust_TierValid_Bad(t *testing.T) {
|
||||
assert.False(t, Tier(0).Valid())
|
||||
assert.False(t, Tier(4).Valid())
|
||||
assert.False(t, Tier(-1).Valid())
|
||||
|
|
@ -36,14 +36,14 @@ func TestTierValid_Bad(t *testing.T) {
|
|||
|
||||
// --- Registry ---
|
||||
|
||||
func TestRegistryRegister_Good(t *testing.T) {
|
||||
func TestTrust_RegistryRegister_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
err := r.Register(Agent{Name: "Athena", Tier: TierFull})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, r.Len())
|
||||
}
|
||||
|
||||
func TestRegistryRegister_Good_SetsDefaults(t *testing.T) {
|
||||
func TestTrust_RegistryRegister_Good_SetsDefaults(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
err := r.Register(Agent{Name: "Athena", Tier: TierFull})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -54,7 +54,7 @@ func TestRegistryRegister_Good_SetsDefaults(t *testing.T) {
|
|||
assert.False(t, a.CreatedAt.IsZero())
|
||||
}
|
||||
|
||||
func TestRegistryRegister_Good_TierDefaults(t *testing.T) {
|
||||
func TestTrust_RegistryRegister_Good_TierDefaults(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "A", Tier: TierUntrusted}))
|
||||
require.NoError(t, r.Register(Agent{Name: "B", Tier: TierVerified}))
|
||||
|
|
@ -65,14 +65,14 @@ func TestRegistryRegister_Good_TierDefaults(t *testing.T) {
|
|||
assert.Equal(t, 0, r.Get("C").RateLimit)
|
||||
}
|
||||
|
||||
func TestRegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) {
|
||||
func TestTrust_RegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
err := r.Register(Agent{Name: "Custom", Tier: TierVerified, RateLimit: 30})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 30, r.Get("Custom").RateLimit)
|
||||
}
|
||||
|
||||
func TestRegistryRegister_Good_Update(t *testing.T) {
|
||||
func TestTrust_RegistryRegister_Good_Update(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierVerified}))
|
||||
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
|
||||
|
|
@ -81,21 +81,21 @@ func TestRegistryRegister_Good_Update(t *testing.T) {
|
|||
assert.Equal(t, TierFull, r.Get("Athena").Tier)
|
||||
}
|
||||
|
||||
func TestRegistryRegister_Bad_EmptyName(t *testing.T) {
|
||||
func TestTrust_RegistryRegister_Bad_EmptyName(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
err := r.Register(Agent{Tier: TierFull})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "name is required")
|
||||
}
|
||||
|
||||
func TestRegistryRegister_Bad_InvalidTier(t *testing.T) {
|
||||
func TestTrust_RegistryRegister_Bad_InvalidTier(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
err := r.Register(Agent{Name: "Bad", Tier: Tier(0)})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid tier")
|
||||
}
|
||||
|
||||
func TestRegistryGet_Good(t *testing.T) {
|
||||
func TestTrust_RegistryGet_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
|
||||
a := r.Get("Athena")
|
||||
|
|
@ -103,24 +103,24 @@ func TestRegistryGet_Good(t *testing.T) {
|
|||
assert.Equal(t, "Athena", a.Name)
|
||||
}
|
||||
|
||||
func TestRegistryGet_Bad_NotFound(t *testing.T) {
|
||||
func TestTrust_RegistryGet_Bad_NotFound(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
assert.Nil(t, r.Get("nonexistent"))
|
||||
}
|
||||
|
||||
func TestRegistryRemove_Good(t *testing.T) {
|
||||
func TestTrust_RegistryRemove_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
|
||||
assert.True(t, r.Remove("Athena"))
|
||||
assert.Equal(t, 0, r.Len())
|
||||
}
|
||||
|
||||
func TestRegistryRemove_Bad_NotFound(t *testing.T) {
|
||||
func TestTrust_RegistryRemove_Bad_NotFound(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
assert.False(t, r.Remove("nonexistent"))
|
||||
}
|
||||
|
||||
func TestRegistryList_Good(t *testing.T) {
|
||||
func TestTrust_RegistryList_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
|
||||
require.NoError(t, r.Register(Agent{Name: "Clotho", Tier: TierVerified}))
|
||||
|
|
@ -136,12 +136,12 @@ func TestRegistryList_Good(t *testing.T) {
|
|||
assert.True(t, names["Clotho"])
|
||||
}
|
||||
|
||||
func TestRegistryList_Good_Empty(t *testing.T) {
|
||||
func TestTrust_RegistryList_Good_Empty(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
assert.Empty(t, r.List())
|
||||
}
|
||||
|
||||
func TestRegistryList_Good_Snapshot(t *testing.T) {
|
||||
func TestTrust_RegistryList_Good_Snapshot(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
|
||||
agents := r.List()
|
||||
|
|
@ -151,7 +151,7 @@ func TestRegistryList_Good_Snapshot(t *testing.T) {
|
|||
assert.Equal(t, TierFull, r.Get("Athena").Tier)
|
||||
}
|
||||
|
||||
func TestRegistryListSeq_Good(t *testing.T) {
|
||||
func TestTrust_RegistryListSeq_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
|
||||
require.NoError(t, r.Register(Agent{Name: "Clotho", Tier: TierVerified}))
|
||||
|
|
@ -169,7 +169,7 @@ func TestRegistryListSeq_Good(t *testing.T) {
|
|||
|
||||
// --- Agent ---
|
||||
|
||||
func TestAgentTokenExpiry_Good(t *testing.T) {
|
||||
func TestTrust_AgentTokenExpiry_Good(t *testing.T) {
|
||||
agent := Agent{
|
||||
Name: "Test",
|
||||
Tier: TierVerified,
|
||||
|
|
@ -183,9 +183,9 @@ func TestAgentTokenExpiry_Good(t *testing.T) {
|
|||
|
||||
// --- Phase 0 Additions ---
|
||||
|
||||
// TestConcurrentRegistryOperations_Good verifies that Register/Get/Remove
|
||||
// TestTrust_ConcurrentRegistryOperations_Good verifies that Register/Get/Remove
|
||||
// from 10 goroutines do not race.
|
||||
func TestConcurrentRegistryOperations_Good(t *testing.T) {
|
||||
func TestTrust_ConcurrentRegistryOperations_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
|
||||
const n = 10
|
||||
|
|
@ -224,24 +224,24 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) {
|
|||
// No panic or data race = success (run with -race flag)
|
||||
}
|
||||
|
||||
// TestRegisterTierZero_Bad verifies that Tier 0 is rejected.
|
||||
func TestRegisterTierZero_Bad(t *testing.T) {
|
||||
// TestTrust_RegisterTierZero_Bad verifies that Tier 0 is rejected.
|
||||
func TestTrust_RegisterTierZero_Bad(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
err := r.Register(Agent{Name: "InvalidTierAgent", Tier: Tier(0)})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid tier")
|
||||
}
|
||||
|
||||
// TestRegisterNegativeTier_Bad verifies that negative tiers are rejected.
|
||||
func TestRegisterNegativeTier_Bad(t *testing.T) {
|
||||
// TestTrust_RegisterNegativeTier_Bad verifies that negative tiers are rejected.
|
||||
func TestTrust_RegisterNegativeTier_Bad(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
err := r.Register(Agent{Name: "NegativeTier", Tier: Tier(-1)})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid tier")
|
||||
}
|
||||
|
||||
// TestTokenExpiryBoundary_Good verifies token expiry checking.
|
||||
func TestTokenExpiryBoundary_Good(t *testing.T) {
|
||||
// TestTrust_TokenExpiryBoundary_Good verifies token expiry checking.
|
||||
func TestTrust_TokenExpiryBoundary_Good(t *testing.T) {
|
||||
// Token that expires in the future — should be valid
|
||||
futureAgent := Agent{
|
||||
Name: "FutureAgent",
|
||||
|
|
@ -256,8 +256,8 @@ func TestTokenExpiryBoundary_Good(t *testing.T) {
|
|||
"token should now be expired")
|
||||
}
|
||||
|
||||
// TestTokenExpiryZeroValue_Ugly verifies zero-value TokenExpiresAt behaviour.
|
||||
func TestTokenExpiryZeroValue_Ugly(t *testing.T) {
|
||||
// TestTrust_TokenExpiryZeroValue_Ugly verifies zero-value TokenExpiresAt behaviour.
|
||||
func TestTrust_TokenExpiryZeroValue_Ugly(t *testing.T) {
|
||||
agent := Agent{
|
||||
Name: "ZeroExpiry",
|
||||
Tier: TierVerified,
|
||||
|
|
@ -274,8 +274,8 @@ func TestTokenExpiryZeroValue_Ugly(t *testing.T) {
|
|||
"zero-value token expiry should be in the past")
|
||||
}
|
||||
|
||||
// TestConcurrentListDuringMutations_Good verifies List is safe during writes.
|
||||
func TestConcurrentListDuringMutations_Good(t *testing.T) {
|
||||
// TestTrust_ConcurrentListDuringMutations_Good verifies List is safe during writes.
|
||||
func TestTrust_ConcurrentListDuringMutations_Good(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
|
||||
// Pre-populate
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue