66 lines
1.6 KiB
Go
66 lines
1.6 KiB
Go
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))
|
|
}
|