Add default security configuration and policy recipes
Introduced default security settings with stricter access control, including policies requiring owner/admin privileges by default. Added multiple pre-configured policy recipes, custom validator support, and extended documentation for security, configurations, and use cases.
This commit is contained in:
@@ -161,6 +161,48 @@ func TestValidateJSON(t *testing.T) {
|
||||
}`),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid owners - single owner",
|
||||
json: []byte(`{
|
||||
"owners": ["0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"]
|
||||
}`),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid owners - multiple owners",
|
||||
json: []byte(`{
|
||||
"owners": [
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
|
||||
]
|
||||
}`),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid owners - wrong length",
|
||||
json: []byte(`{
|
||||
"owners": ["not-64-chars"]
|
||||
}`),
|
||||
expectError: true,
|
||||
errorSubstr: "invalid owner pubkey",
|
||||
},
|
||||
{
|
||||
name: "invalid owners - non-hex characters",
|
||||
json: []byte(`{
|
||||
"owners": ["zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"]
|
||||
}`),
|
||||
expectError: true,
|
||||
errorSubstr: "invalid owner pubkey",
|
||||
},
|
||||
{
|
||||
name: "valid policy with both owners and policy_admins",
|
||||
json: []byte(`{
|
||||
"owners": ["0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"],
|
||||
"policy_admins": ["fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"],
|
||||
"policy_follow_whitelist_enabled": true
|
||||
}`),
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -401,3 +443,65 @@ func TestReloadPreservesExistingOnFailure(t *testing.T) {
|
||||
func containsSubstring(s, substr string) bool {
|
||||
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
||||
}
|
||||
|
||||
// TestGetOwnersBin tests the GetOwnersBin method for policy-defined owners
|
||||
func TestGetOwnersBin(t *testing.T) {
|
||||
policy, cleanup := setupHotreloadTestPolicy(t, "test-get-owners-bin")
|
||||
defer cleanup()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "policy.json")
|
||||
|
||||
// Test 1: Policy with no owners
|
||||
emptyJSON := []byte(`{"default_policy": "allow"}`)
|
||||
if err := policy.Reload(emptyJSON, configPath); err != nil {
|
||||
t.Fatalf("Failed to reload policy: %v", err)
|
||||
}
|
||||
|
||||
owners := policy.GetOwnersBin()
|
||||
if len(owners) != 0 {
|
||||
t.Errorf("Expected 0 owners, got %d", len(owners))
|
||||
}
|
||||
|
||||
// Test 2: Policy with owners
|
||||
ownerHex := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
withOwnersJSON := []byte(`{
|
||||
"default_policy": "allow",
|
||||
"owners": ["` + ownerHex + `"]
|
||||
}`)
|
||||
if err := policy.Reload(withOwnersJSON, configPath); err != nil {
|
||||
t.Fatalf("Failed to reload policy with owners: %v", err)
|
||||
}
|
||||
|
||||
owners = policy.GetOwnersBin()
|
||||
if len(owners) != 1 {
|
||||
t.Errorf("Expected 1 owner, got %d", len(owners))
|
||||
}
|
||||
if len(owners) > 0 && len(owners[0]) != 32 {
|
||||
t.Errorf("Expected owner binary to be 32 bytes, got %d", len(owners[0]))
|
||||
}
|
||||
|
||||
// Test 3: GetOwners returns hex strings
|
||||
hexOwners := policy.GetOwners()
|
||||
if len(hexOwners) != 1 {
|
||||
t.Errorf("Expected 1 hex owner, got %d", len(hexOwners))
|
||||
}
|
||||
if len(hexOwners) > 0 && hexOwners[0] != ownerHex {
|
||||
t.Errorf("Expected owner %q, got %q", ownerHex, hexOwners[0])
|
||||
}
|
||||
|
||||
// Test 4: Policy with multiple owners
|
||||
owner2Hex := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
|
||||
multiOwnersJSON := []byte(`{
|
||||
"default_policy": "allow",
|
||||
"owners": ["` + ownerHex + `", "` + owner2Hex + `"]
|
||||
}`)
|
||||
if err := policy.Reload(multiOwnersJSON, configPath); err != nil {
|
||||
t.Fatalf("Failed to reload policy with multiple owners: %v", err)
|
||||
}
|
||||
|
||||
owners = policy.GetOwnersBin()
|
||||
if len(owners) != 2 {
|
||||
t.Errorf("Expected 2 owners, got %d", len(owners))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,10 +395,16 @@ type P struct {
|
||||
// When true and a rule has WriteAllowFollows=true, policy admin follows get read+write access.
|
||||
PolicyFollowWhitelistEnabled bool `json:"policy_follow_whitelist_enabled,omitempty"`
|
||||
|
||||
// Owners is a list of hex-encoded pubkeys that have full control of the relay.
|
||||
// These are merged with owners from the ORLY_OWNERS environment variable.
|
||||
// Useful for cloud deployments where environment variables cannot be modified.
|
||||
Owners []string `json:"owners,omitempty"`
|
||||
|
||||
// Unexported binary caches for faster comparison (populated from hex strings above)
|
||||
policyAdminsBin [][]byte // Binary cache for policy admin pubkeys
|
||||
policyFollows [][]byte // Cached follow list from policy admins (kind 3 events)
|
||||
policyFollowsMx sync.RWMutex // Protect follows list access
|
||||
ownersBin [][]byte // Binary cache for policy-defined owner pubkeys
|
||||
|
||||
// manager handles policy script execution.
|
||||
// Unexported to enforce use of public API methods (CheckPolicy, IsEnabled).
|
||||
@@ -413,6 +419,7 @@ type pJSON struct {
|
||||
DefaultPolicy string `json:"default_policy"`
|
||||
PolicyAdmins []string `json:"policy_admins,omitempty"`
|
||||
PolicyFollowWhitelistEnabled bool `json:"policy_follow_whitelist_enabled,omitempty"`
|
||||
Owners []string `json:"owners,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON unmarshalling to handle unexported fields.
|
||||
@@ -427,6 +434,7 @@ func (p *P) UnmarshalJSON(data []byte) error {
|
||||
p.DefaultPolicy = shadow.DefaultPolicy
|
||||
p.PolicyAdmins = shadow.PolicyAdmins
|
||||
p.PolicyFollowWhitelistEnabled = shadow.PolicyFollowWhitelistEnabled
|
||||
p.Owners = shadow.Owners
|
||||
|
||||
// Populate binary cache for policy admins
|
||||
if len(p.PolicyAdmins) > 0 {
|
||||
@@ -441,6 +449,19 @@ func (p *P) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Populate binary cache for policy-defined owners
|
||||
if len(p.Owners) > 0 {
|
||||
p.ownersBin = make([][]byte, 0, len(p.Owners))
|
||||
for _, hexPubkey := range p.Owners {
|
||||
binPubkey, err := hex.Dec(hexPubkey)
|
||||
if err != nil {
|
||||
log.W.F("failed to decode owner pubkey %q: %v", hexPubkey, err)
|
||||
continue
|
||||
}
|
||||
p.ownersBin = append(p.ownersBin, binPubkey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1735,6 +1756,16 @@ func (p *P) ValidateJSON(policyJSON []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate owners are valid hex pubkeys (64 characters)
|
||||
for _, owner := range tempPolicy.Owners {
|
||||
if len(owner) != 64 {
|
||||
return fmt.Errorf("invalid owner pubkey length: %q (expected 64 hex characters)", owner)
|
||||
}
|
||||
if _, err := hex.Dec(owner); err != nil {
|
||||
return fmt.Errorf("invalid owner pubkey format: %q: %v", owner, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate regex patterns in tag_validation rules and new fields
|
||||
for kind, rule := range tempPolicy.rules {
|
||||
for tagName, pattern := range rule.TagValidation {
|
||||
@@ -1835,7 +1866,9 @@ func (p *P) Reload(policyJSON []byte, configPath string) error {
|
||||
p.DefaultPolicy = tempPolicy.DefaultPolicy
|
||||
p.PolicyAdmins = tempPolicy.PolicyAdmins
|
||||
p.PolicyFollowWhitelistEnabled = tempPolicy.PolicyFollowWhitelistEnabled
|
||||
p.Owners = tempPolicy.Owners
|
||||
p.policyAdminsBin = tempPolicy.policyAdminsBin
|
||||
p.ownersBin = tempPolicy.ownersBin
|
||||
// Note: policyFollows is NOT reset here - it will be refreshed separately
|
||||
p.policyFollowsMx.Unlock()
|
||||
|
||||
@@ -1923,6 +1956,7 @@ func (p *P) SaveToFile(configPath string) error {
|
||||
DefaultPolicy: p.DefaultPolicy,
|
||||
PolicyAdmins: p.PolicyAdmins,
|
||||
PolicyFollowWhitelistEnabled: p.PolicyFollowWhitelistEnabled,
|
||||
Owners: p.Owners,
|
||||
}
|
||||
|
||||
// Marshal to JSON with indentation for readability
|
||||
@@ -2015,6 +2049,36 @@ func (p *P) GetPolicyAdminsBin() [][]byte {
|
||||
return result
|
||||
}
|
||||
|
||||
// GetOwnersBin returns a copy of the binary owner pubkeys defined in the policy.
|
||||
// These are merged with environment-defined owners by the application layer.
|
||||
// Useful for cloud deployments where environment variables cannot be modified.
|
||||
func (p *P) GetOwnersBin() [][]byte {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.policyFollowsMx.RLock()
|
||||
defer p.policyFollowsMx.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
result := make([][]byte, len(p.ownersBin))
|
||||
for i, owner := range p.ownersBin {
|
||||
ownerCopy := make([]byte, len(owner))
|
||||
copy(ownerCopy, owner)
|
||||
result[i] = ownerCopy
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetOwners returns the hex-encoded owner pubkeys defined in the policy.
|
||||
// These are merged with environment-defined owners by the application layer.
|
||||
func (p *P) GetOwners() []string {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.Owners
|
||||
}
|
||||
|
||||
// IsPolicyFollowWhitelistEnabled returns whether the policy follow whitelist feature is enabled.
|
||||
// When enabled, pubkeys followed by policy admins are automatically whitelisted for access
|
||||
// when rules have WriteAllowFollows=true.
|
||||
|
||||
Reference in New Issue
Block a user