DEV-000061 checkpoint interaction chat foundation
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyCredentialRef = errors.New("empty credential ref")
|
||||
ErrPathOutsideRoot = errors.New("secret path outside protected root")
|
||||
ErrEmptySecret = errors.New("empty secret")
|
||||
)
|
||||
|
||||
// FileResolver resolves CredentialRef metadata against a protected local file
|
||||
// backend. It is a bootstrap mechanism, not canonical configuration storage.
|
||||
type FileResolver struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func NewFileResolver(root string) (*FileResolver, error) {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" {
|
||||
return nil, fmt.Errorf("secret backend root: %w", ErrPathOutsideRoot)
|
||||
}
|
||||
cleanRoot, err := filepath.Abs(filepath.Clean(root))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("secret backend root: %w", err)
|
||||
}
|
||||
return &FileResolver{root: cleanRoot}, nil
|
||||
}
|
||||
|
||||
func (r *FileResolver) ResolveFile(ref secretcontracts.SecretFileRef) (string, error) {
|
||||
if strings.TrimSpace(string(ref.CredentialRef)) == "" {
|
||||
return "", ErrEmptyCredentialRef
|
||||
}
|
||||
cleanPath, err := filepath.Abs(filepath.Clean(strings.TrimSpace(ref.Path)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !insideRoot(r.root, cleanPath) {
|
||||
return "", ErrPathOutsideRoot
|
||||
}
|
||||
content, err := os.ReadFile(cleanPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
secret := strings.TrimSpace(string(content))
|
||||
if secret == "" {
|
||||
return "", ErrEmptySecret
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
func insideRoot(root, path string) bool {
|
||||
rel, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return rel == "." || (!strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
)
|
||||
|
||||
func TestFileResolverResolvesSecretInsideRoot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, "interaction_db_password")
|
||||
if err := os.WriteFile(path, []byte("strong-secret\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resolver, err := NewFileResolver(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := resolver.ResolveFile(secretcontracts.SecretFileRef{
|
||||
CredentialRef: "credential://domovoy/interaction/db-password/v1",
|
||||
Purpose: secretcontracts.SecretPurposeDatabasePassword,
|
||||
Consumer: secretcontracts.RuntimeConsumerCore,
|
||||
Path: path,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "strong-secret" {
|
||||
t.Fatalf("unexpected secret value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileResolverRejectsPathOutsideRoot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
outside := filepath.Join(t.TempDir(), "secret")
|
||||
if err := os.WriteFile(outside, []byte("secret"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resolver, err := NewFileResolver(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = resolver.ResolveFile(secretcontracts.SecretFileRef{
|
||||
CredentialRef: "credential://domovoy/interaction/db-password/v1",
|
||||
Purpose: secretcontracts.SecretPurposeDatabasePassword,
|
||||
Consumer: secretcontracts.RuntimeConsumerCore,
|
||||
Path: outside,
|
||||
})
|
||||
if !errors.Is(err, ErrPathOutsideRoot) {
|
||||
t.Fatalf("expected ErrPathOutsideRoot, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileResolverRejectsEmptyCredentialRef(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, "secret")
|
||||
if err := os.WriteFile(path, []byte("secret"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resolver, err := NewFileResolver(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = resolver.ResolveFile(secretcontracts.SecretFileRef{
|
||||
Purpose: secretcontracts.SecretPurposeDatabasePassword,
|
||||
Consumer: secretcontracts.RuntimeConsumerCore,
|
||||
Path: path,
|
||||
})
|
||||
if !errors.Is(err, ErrEmptyCredentialRef) {
|
||||
t.Fatalf("expected ErrEmptyCredentialRef, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user