Enhance policy system tests and documentation.
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
Added extensive tests for default-permissive access control, read/write follow whitelists, and privileged-only fields. Updated policy documentation with new configuration examples, access control reference, and logging details.
This commit is contained in:
@@ -264,8 +264,10 @@ Validates that tag values match the specified regex patterns. Only validates tag
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `write_allow_follows` | boolean | Grant read+write access to policy admin follows |
|
||||
| `follows_whitelist_admins` | array | Per-rule admin pubkeys whose follows are whitelisted |
|
||||
| `write_allow_follows` | boolean | **DEPRECATED.** Grant read+write access to policy admin follows |
|
||||
| `follows_whitelist_admins` | array | **DEPRECATED.** Per-rule admin pubkeys whose follows are whitelisted |
|
||||
| `read_follows_whitelist` | array | Pubkeys whose follows can READ events. Restricts read access when set. |
|
||||
| `write_follows_whitelist` | array | Pubkeys whose follows can WRITE events. Restricts write access when set. |
|
||||
|
||||
See [Follows-Based Whitelisting](#follows-based-whitelisting) for details.
|
||||
|
||||
@@ -350,26 +352,47 @@ P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S
|
||||
|
||||
## Access Control
|
||||
|
||||
### Write Access Evaluation
|
||||
### Default-Permissive Access Model
|
||||
|
||||
The policy system uses a **default-permissive** model for both read and write access:
|
||||
|
||||
- **Read**: Allowed by default unless a read restriction is configured
|
||||
- **Write**: Allowed by default unless a write restriction is configured
|
||||
|
||||
Restrictions become active when any of the following fields are set:
|
||||
|
||||
| Access | Restrictions |
|
||||
|--------|--------------|
|
||||
| Read | `read_allow`, `read_follows_whitelist`, or `privileged` |
|
||||
| Write | `write_allow`, `write_follows_whitelist` |
|
||||
|
||||
**Important**: `privileged` ONLY applies to READ operations.
|
||||
|
||||
### Write Access Evaluation (Default-Permissive)
|
||||
|
||||
```
|
||||
1. If write_allow is set and pubkey NOT in list → DENY
|
||||
2. If write_deny is set and pubkey IN list → DENY
|
||||
1. Universal constraints (size, tags, age) - must pass
|
||||
2. If pubkey in write_deny → DENY
|
||||
3. If write_allow_follows enabled and pubkey in admin follows → ALLOW
|
||||
4. If follows_whitelist_admins set and pubkey in rule follows → ALLOW
|
||||
5. Continue to other checks...
|
||||
4. If write_follows_whitelist set and pubkey in follows → ALLOW
|
||||
5. If write_allow set and pubkey in list → ALLOW
|
||||
6. If ANY write restriction is set → DENY (not in any whitelist)
|
||||
7. Otherwise → ALLOW (default-permissive)
|
||||
```
|
||||
|
||||
### Read Access Evaluation
|
||||
### Read Access Evaluation (Default-Permissive)
|
||||
|
||||
```
|
||||
1. If read_allow is set and pubkey NOT in list → DENY
|
||||
2. If read_deny is set and pubkey IN list → DENY
|
||||
3. If privileged is true and pubkey NOT party to event → DENY
|
||||
4. Continue to other checks...
|
||||
1. If pubkey in read_deny → DENY
|
||||
2. If read_allow_follows enabled and pubkey in admin follows → ALLOW
|
||||
3. If read_follows_whitelist set and pubkey in follows → ALLOW
|
||||
4. If read_allow set and pubkey in list → ALLOW
|
||||
5. If privileged set and pubkey is party to event → ALLOW
|
||||
6. If ANY read restriction is set → DENY (not in any whitelist)
|
||||
7. Otherwise → ALLOW (default-permissive)
|
||||
```
|
||||
|
||||
### Privileged Events
|
||||
### Privileged Events (Read-Only)
|
||||
|
||||
When `privileged: true`, only the author and p-tag recipients can access the event:
|
||||
|
||||
@@ -386,9 +409,37 @@ When `privileged: true`, only the author and p-tag recipients can access the eve
|
||||
|
||||
## Follows-Based Whitelisting
|
||||
|
||||
There are two mechanisms for follows-based access control:
|
||||
The policy system supports whitelisting pubkeys based on follow lists (kind 3 events). There are two approaches:
|
||||
|
||||
### 1. Global Policy Admin Follows
|
||||
### 1. Separate Read/Write Follows Whitelists (Recommended)
|
||||
|
||||
Use `read_follows_whitelist` and `write_follows_whitelist` for fine-grained control:
|
||||
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"read_follows_whitelist": ["curator_pubkey_hex"],
|
||||
"write_follows_whitelist": ["moderator_pubkey_hex"]
|
||||
},
|
||||
"rules": {
|
||||
"30023": {
|
||||
"description": "Articles - curated reading, moderated writing",
|
||||
"read_follows_whitelist": ["article_curator_hex"],
|
||||
"write_follows_whitelist": ["article_moderator_hex"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- The pubkeys listed AND their follows (from kind 3 events) can access the events
|
||||
- `read_follows_whitelist`: Restricts WHO can read (when set)
|
||||
- `write_follows_whitelist`: Restricts WHO can write (when set)
|
||||
- If not set, the default-permissive behavior applies
|
||||
|
||||
**Important:** The relay will fail to start if the named pubkeys don't have kind 3 follow list events in the database. This ensures the follow lists are available for access control.
|
||||
|
||||
### 2. Legacy: Global Policy Admin Follows (DEPRECATED)
|
||||
|
||||
Enable whitelisting for all pubkeys followed by policy admins:
|
||||
|
||||
@@ -406,7 +457,7 @@ Enable whitelisting for all pubkeys followed by policy admins:
|
||||
|
||||
When `write_allow_follows` is true, pubkeys in the policy admins' kind 3 follow lists get both read AND write access.
|
||||
|
||||
### 2. Per-Rule Follows Whitelist
|
||||
### 3. Legacy: Per-Rule Follows Whitelist (DEPRECATED)
|
||||
|
||||
Configure specific admins per rule:
|
||||
|
||||
@@ -423,18 +474,33 @@ Configure specific admins per rule:
|
||||
|
||||
This allows different rules to use different admin follow lists.
|
||||
|
||||
### Loading Follow Lists
|
||||
### Loading Follow Lists at Startup
|
||||
|
||||
The application must load follow lists at startup:
|
||||
The application must load follow lists at startup. The new API provides separate methods:
|
||||
|
||||
```go
|
||||
// Get all admin pubkeys that need follow lists loaded
|
||||
admins := policy.GetAllFollowsWhitelistAdmins()
|
||||
// Get all pubkeys that need follow lists loaded (combines read + write + legacy)
|
||||
allPubkeys := policy.GetAllFollowsWhitelistPubkeys()
|
||||
|
||||
// For each admin, load their kind 3 event and update the whitelist
|
||||
for _, adminHex := range admins {
|
||||
follows := loadFollowsFromKind3(adminHex)
|
||||
policy.UpdateRuleFollowsWhitelist(kind, follows)
|
||||
// Or get them separately
|
||||
readPubkeys := policy.GetAllReadFollowsWhitelistPubkeys()
|
||||
writePubkeys := policy.GetAllWriteFollowsWhitelistPubkeys()
|
||||
legacyAdmins := policy.GetAllFollowsWhitelistAdmins()
|
||||
|
||||
// Load follows and update the policy
|
||||
for _, pubkeyHex := range readPubkeys {
|
||||
follows := loadFollowsFromKind3(pubkeyHex)
|
||||
// Update read follows whitelist for specific kinds
|
||||
policy.UpdateRuleReadFollowsWhitelist(kind, follows)
|
||||
// Or for global rule
|
||||
policy.UpdateGlobalReadFollowsWhitelist(follows)
|
||||
}
|
||||
|
||||
for _, pubkeyHex := range writePubkeys {
|
||||
follows := loadFollowsFromKind3(pubkeyHex)
|
||||
policy.UpdateRuleWriteFollowsWhitelist(kind, follows)
|
||||
// Or for global rule
|
||||
policy.UpdateGlobalWriteFollowsWhitelist(follows)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -485,8 +485,8 @@ func (p *P) IsOwner(pubkey []byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
p.policyFollowsMx.RLock()
|
||||
defer p.policyFollowsMx.RUnlock()
|
||||
p.followsMx.RLock()
|
||||
defer p.followsMx.RUnlock()
|
||||
|
||||
for _, owner := range p.ownersBin {
|
||||
if utils.FastEqual(owner, pubkey) {
|
||||
|
||||
686
pkg/policy/default_permissive_test.go
Normal file
686
pkg/policy/default_permissive_test.go
Normal file
@@ -0,0 +1,686 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/hex"
|
||||
"git.mleku.dev/mleku/nostr/encoders/tag"
|
||||
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
|
||||
"lol.mleku.dev/chk"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Default-Permissive Access Control Tests
|
||||
// =============================================================================
|
||||
|
||||
// TestDefaultPermissiveRead tests that read access is allowed by default
|
||||
// when no read restrictions are configured.
|
||||
func TestDefaultPermissiveRead(t *testing.T) {
|
||||
// No read restrictions configured
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"rules": {
|
||||
"1": {
|
||||
"description": "No read restrictions"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
authorSigner, authorPubkey := generateTestKeypair(t)
|
||||
_, readerPubkey := generateTestKeypair(t)
|
||||
_, randomPubkey := generateTestKeypair(t)
|
||||
|
||||
ev := createTestEvent(t, authorSigner, "test content", 1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pubkey []byte
|
||||
expectAllow bool
|
||||
}{
|
||||
{
|
||||
name: "author can read (default permissive)",
|
||||
pubkey: authorPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "reader can read (default permissive)",
|
||||
pubkey: readerPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "random user can read (default permissive)",
|
||||
pubkey: randomPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "nil pubkey can read (default permissive)",
|
||||
pubkey: nil,
|
||||
expectAllow: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", ev, tt.pubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed != tt.expectAllow {
|
||||
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPermissiveWrite tests that write access is allowed by default
|
||||
// when no write restrictions are configured.
|
||||
func TestDefaultPermissiveWrite(t *testing.T) {
|
||||
// No write restrictions configured
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"rules": {
|
||||
"1": {
|
||||
"description": "No write restrictions"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
writerSigner, writerPubkey := generateTestKeypair(t)
|
||||
_, randomPubkey := generateTestKeypair(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
signer *p8k.Signer
|
||||
pubkey []byte
|
||||
expectAllow bool
|
||||
}{
|
||||
{
|
||||
name: "writer can write (default permissive)",
|
||||
signer: writerSigner,
|
||||
pubkey: writerPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "random user can write (default permissive)",
|
||||
signer: writerSigner,
|
||||
pubkey: randomPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ev := createTestEvent(t, tt.signer, "test content", 1)
|
||||
allowed, err := policy.CheckPolicy("write", ev, tt.pubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed != tt.expectAllow {
|
||||
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadFollowsWhitelist tests the read_follows_whitelist field.
|
||||
func TestReadFollowsWhitelist(t *testing.T) {
|
||||
_, curatorPubkey := generateTestKeypair(t)
|
||||
_, followedPubkey := generateTestKeypair(t)
|
||||
_, unfollowedPubkey := generateTestKeypair(t)
|
||||
authorSigner, authorPubkey := generateTestKeypair(t)
|
||||
|
||||
curatorHex := hex.Enc(curatorPubkey)
|
||||
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"rules": {
|
||||
"1": {
|
||||
"description": "Only curator follows can read",
|
||||
"read_follows_whitelist": ["` + curatorHex + `"]
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Simulate loading curator's follows (includes followed user and curator themselves)
|
||||
policy.UpdateRuleReadFollowsWhitelist(1, [][]byte{followedPubkey})
|
||||
|
||||
ev := createTestEvent(t, authorSigner, "test content", 1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pubkey []byte
|
||||
expectAllow bool
|
||||
}{
|
||||
{
|
||||
name: "curator can read (is in whitelist pubkeys)",
|
||||
pubkey: curatorPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "followed user can read",
|
||||
pubkey: followedPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "unfollowed user denied",
|
||||
pubkey: unfollowedPubkey,
|
||||
expectAllow: false,
|
||||
},
|
||||
{
|
||||
name: "author cannot read (not in follows)",
|
||||
pubkey: authorPubkey,
|
||||
expectAllow: false,
|
||||
},
|
||||
{
|
||||
name: "nil pubkey denied",
|
||||
pubkey: nil,
|
||||
expectAllow: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", ev, tt.pubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed != tt.expectAllow {
|
||||
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Verify write is still default-permissive (no write restriction)
|
||||
t.Run("write is still default permissive", func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("write", ev, unfollowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected write to be allowed (no write restriction)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestWriteFollowsWhitelist tests the write_follows_whitelist field.
|
||||
func TestWriteFollowsWhitelist(t *testing.T) {
|
||||
moderatorSigner, moderatorPubkey := generateTestKeypair(t)
|
||||
followedSigner, followedPubkey := generateTestKeypair(t)
|
||||
unfollowedSigner, unfollowedPubkey := generateTestKeypair(t)
|
||||
|
||||
moderatorHex := hex.Enc(moderatorPubkey)
|
||||
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"rules": {
|
||||
"1": {
|
||||
"description": "Only moderator follows can write",
|
||||
"write_follows_whitelist": ["` + moderatorHex + `"]
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Simulate loading moderator's follows
|
||||
policy.UpdateRuleWriteFollowsWhitelist(1, [][]byte{followedPubkey})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
signer *p8k.Signer
|
||||
pubkey []byte
|
||||
expectAllow bool
|
||||
}{
|
||||
{
|
||||
name: "moderator can write (is in whitelist pubkeys)",
|
||||
signer: moderatorSigner,
|
||||
pubkey: moderatorPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "followed user can write",
|
||||
signer: followedSigner,
|
||||
pubkey: followedPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "unfollowed user denied",
|
||||
signer: unfollowedSigner,
|
||||
pubkey: unfollowedPubkey,
|
||||
expectAllow: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ev := createTestEvent(t, tt.signer, "test content", 1)
|
||||
allowed, err := policy.CheckPolicy("write", ev, tt.pubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed != tt.expectAllow {
|
||||
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Verify read is still default-permissive (no read restriction)
|
||||
t.Run("read is still default permissive", func(t *testing.T) {
|
||||
ev := createTestEvent(t, unfollowedSigner, "test content", 1)
|
||||
allowed, err := policy.CheckPolicy("read", ev, unfollowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected read to be allowed (no read restriction)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestGlobalReadFollowsWhitelist tests read_follows_whitelist in global rule.
|
||||
func TestGlobalReadFollowsWhitelist(t *testing.T) {
|
||||
_, curatorPubkey := generateTestKeypair(t)
|
||||
_, followedPubkey := generateTestKeypair(t)
|
||||
_, unfollowedPubkey := generateTestKeypair(t)
|
||||
authorSigner, _ := generateTestKeypair(t)
|
||||
|
||||
curatorHex := hex.Enc(curatorPubkey)
|
||||
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"global": {
|
||||
"description": "Global read follows whitelist",
|
||||
"read_follows_whitelist": ["` + curatorHex + `"]
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Update global read follows whitelist
|
||||
policy.UpdateGlobalReadFollowsWhitelist([][]byte{followedPubkey})
|
||||
|
||||
// Test with kind 1
|
||||
t.Run("kind 1", func(t *testing.T) {
|
||||
ev := createTestEvent(t, authorSigner, "test content", 1)
|
||||
|
||||
// Followed user can read
|
||||
allowed, err := policy.CheckPolicy("read", ev, followedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected followed user to be allowed to read")
|
||||
}
|
||||
|
||||
// Unfollowed user denied
|
||||
allowed, err = policy.CheckPolicy("read", ev, unfollowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Expected unfollowed user to be denied")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestGlobalWriteFollowsWhitelist tests write_follows_whitelist in global rule.
|
||||
func TestGlobalWriteFollowsWhitelist(t *testing.T) {
|
||||
_, moderatorPubkey := generateTestKeypair(t)
|
||||
followedSigner, followedPubkey := generateTestKeypair(t)
|
||||
unfollowedSigner, unfollowedPubkey := generateTestKeypair(t)
|
||||
|
||||
moderatorHex := hex.Enc(moderatorPubkey)
|
||||
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"global": {
|
||||
"description": "Global write follows whitelist",
|
||||
"write_follows_whitelist": ["` + moderatorHex + `"]
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Update global write follows whitelist
|
||||
policy.UpdateGlobalWriteFollowsWhitelist([][]byte{followedPubkey})
|
||||
|
||||
// Test with kind 1
|
||||
t.Run("kind 1", func(t *testing.T) {
|
||||
// Followed user can write
|
||||
ev := createTestEvent(t, followedSigner, "test content", 1)
|
||||
allowed, err := policy.CheckPolicy("write", ev, followedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected followed user to be allowed to write")
|
||||
}
|
||||
|
||||
// Unfollowed user denied
|
||||
ev = createTestEvent(t, unfollowedSigner, "test content", 1)
|
||||
allowed, err = policy.CheckPolicy("write", ev, unfollowedPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Expected unfollowed user to be denied")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPrivilegedOnlyAppliesToReadDP tests that privileged only affects read access.
|
||||
func TestPrivilegedOnlyAppliesToReadDP(t *testing.T) {
|
||||
authorSigner, authorPubkey := generateTestKeypair(t)
|
||||
_, recipientPubkey := generateTestKeypair(t)
|
||||
thirdPartySigner, thirdPartyPubkey := generateTestKeypair(t)
|
||||
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"rules": {
|
||||
"4": {
|
||||
"description": "Encrypted DMs - privileged",
|
||||
"privileged": true
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Create event with p-tag for recipient
|
||||
ev := event.New()
|
||||
ev.Kind = 4
|
||||
ev.Content = []byte("encrypted content")
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
ev.Tags = tag.NewS()
|
||||
pTag := tag.NewFromAny("p", hex.Enc(recipientPubkey))
|
||||
ev.Tags.Append(pTag)
|
||||
if err := ev.Sign(authorSigner); chk.E(err) {
|
||||
t.Fatalf("Failed to sign event: %v", err)
|
||||
}
|
||||
|
||||
// READ tests
|
||||
t.Run("author can read", func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", ev, authorPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected author to be allowed to read")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("recipient can read", func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", ev, recipientPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected recipient to be allowed to read")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("third party cannot read", func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", ev, thirdPartyPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Expected third party to be denied read access")
|
||||
}
|
||||
})
|
||||
|
||||
// WRITE tests - privileged should NOT affect write
|
||||
t.Run("third party CAN write (privileged doesn't affect write)", func(t *testing.T) {
|
||||
ev := createTestEvent(t, thirdPartySigner, "test content", 4)
|
||||
allowed, err := policy.CheckPolicy("write", ev, thirdPartyPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected third party to be allowed to write (privileged doesn't restrict write)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCombinedReadWriteFollowsWhitelists tests using both whitelists on same rule.
|
||||
func TestCombinedReadWriteFollowsWhitelists(t *testing.T) {
|
||||
_, curatorPubkey := generateTestKeypair(t)
|
||||
_, moderatorPubkey := generateTestKeypair(t)
|
||||
readerSigner, readerPubkey := generateTestKeypair(t)
|
||||
writerSigner, writerPubkey := generateTestKeypair(t)
|
||||
_, outsiderPubkey := generateTestKeypair(t)
|
||||
|
||||
curatorHex := hex.Enc(curatorPubkey)
|
||||
moderatorHex := hex.Enc(moderatorPubkey)
|
||||
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"rules": {
|
||||
"30023": {
|
||||
"description": "Articles - different read/write follows",
|
||||
"read_follows_whitelist": ["` + curatorHex + `"],
|
||||
"write_follows_whitelist": ["` + moderatorHex + `"]
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Curator follows reader, moderator follows writer
|
||||
policy.UpdateRuleReadFollowsWhitelist(30023, [][]byte{readerPubkey})
|
||||
policy.UpdateRuleWriteFollowsWhitelist(30023, [][]byte{writerPubkey})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
access string
|
||||
signer *p8k.Signer
|
||||
pubkey []byte
|
||||
expectAllow bool
|
||||
}{
|
||||
// Read tests
|
||||
{
|
||||
name: "reader can read",
|
||||
access: "read",
|
||||
signer: readerSigner,
|
||||
pubkey: readerPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "writer cannot read (not in read follows)",
|
||||
access: "read",
|
||||
signer: writerSigner,
|
||||
pubkey: writerPubkey,
|
||||
expectAllow: false,
|
||||
},
|
||||
{
|
||||
name: "outsider cannot read",
|
||||
access: "read",
|
||||
signer: readerSigner,
|
||||
pubkey: outsiderPubkey,
|
||||
expectAllow: false,
|
||||
},
|
||||
// Write tests
|
||||
{
|
||||
name: "writer can write",
|
||||
access: "write",
|
||||
signer: writerSigner,
|
||||
pubkey: writerPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "reader cannot write (not in write follows)",
|
||||
access: "write",
|
||||
signer: readerSigner,
|
||||
pubkey: readerPubkey,
|
||||
expectAllow: false,
|
||||
},
|
||||
{
|
||||
name: "outsider cannot write",
|
||||
access: "write",
|
||||
signer: readerSigner,
|
||||
pubkey: outsiderPubkey,
|
||||
expectAllow: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ev := createTestEvent(t, tt.signer, "test content", 30023)
|
||||
allowed, err := policy.CheckPolicy(tt.access, ev, tt.pubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed != tt.expectAllow {
|
||||
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadAllowWithReadFollowsWhitelist tests combining read_allow and read_follows_whitelist.
|
||||
func TestReadAllowWithReadFollowsWhitelist(t *testing.T) {
|
||||
_, curatorPubkey := generateTestKeypair(t)
|
||||
_, followedPubkey := generateTestKeypair(t)
|
||||
_, explicitPubkey := generateTestKeypair(t)
|
||||
_, outsiderPubkey := generateTestKeypair(t)
|
||||
authorSigner, _ := generateTestKeypair(t)
|
||||
|
||||
curatorHex := hex.Enc(curatorPubkey)
|
||||
explicitHex := hex.Enc(explicitPubkey)
|
||||
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "deny",
|
||||
"rules": {
|
||||
"1": {
|
||||
"description": "Read via follows OR explicit allow",
|
||||
"read_follows_whitelist": ["` + curatorHex + `"],
|
||||
"read_allow": ["` + explicitHex + `"]
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
policy.UpdateRuleReadFollowsWhitelist(1, [][]byte{followedPubkey})
|
||||
|
||||
ev := createTestEvent(t, authorSigner, "test content", 1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pubkey []byte
|
||||
expectAllow bool
|
||||
}{
|
||||
{
|
||||
name: "followed user can read",
|
||||
pubkey: followedPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "explicit allow user can read",
|
||||
pubkey: explicitPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "curator can read (is whitelist pubkey)",
|
||||
pubkey: curatorPubkey,
|
||||
expectAllow: true,
|
||||
},
|
||||
{
|
||||
name: "outsider denied",
|
||||
pubkey: outsiderPubkey,
|
||||
expectAllow: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", ev, tt.pubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
if allowed != tt.expectAllow {
|
||||
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAllFollowsWhitelistPubkeysDP tests the combined pubkey retrieval.
|
||||
func TestGetAllFollowsWhitelistPubkeysDP(t *testing.T) {
|
||||
read1 := "1111111111111111111111111111111111111111111111111111111111111111"
|
||||
read2 := "2222222222222222222222222222222222222222222222222222222222222222"
|
||||
write1 := "3333333333333333333333333333333333333333333333333333333333333333"
|
||||
legacy := "4444444444444444444444444444444444444444444444444444444444444444"
|
||||
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "allow",
|
||||
"global": {
|
||||
"read_follows_whitelist": ["` + read1 + `"],
|
||||
"write_follows_whitelist": ["` + write1 + `"]
|
||||
},
|
||||
"rules": {
|
||||
"1": {
|
||||
"read_follows_whitelist": ["` + read2 + `"],
|
||||
"follows_whitelist_admins": ["` + legacy + `"]
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
policy, err := New(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
allPubkeys := policy.GetAllFollowsWhitelistPubkeys()
|
||||
if len(allPubkeys) != 4 {
|
||||
t.Errorf("Expected 4 unique pubkeys, got %d", len(allPubkeys))
|
||||
}
|
||||
|
||||
// Check each is present
|
||||
pubkeySet := make(map[string]bool)
|
||||
for _, pk := range allPubkeys {
|
||||
pubkeySet[pk] = true
|
||||
}
|
||||
|
||||
expected := []string{read1, read2, write1, legacy}
|
||||
for _, exp := range expected {
|
||||
if !pubkeySet[exp] {
|
||||
t.Errorf("Expected pubkey %s not found", exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1091,9 +1091,12 @@ func TestAllNewFieldsCombined(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test new fields in global rule
|
||||
// Global rule is ONLY used as fallback when NO kind-specific rule exists.
|
||||
// If a kind-specific rule exists (even if empty), it takes precedence and global is ignored.
|
||||
func TestNewFieldsInGlobalRule(t *testing.T) {
|
||||
signer, pubkey := generateTestKeypair(t)
|
||||
|
||||
// Policy with global constraints and a kind-specific rule for kind 1
|
||||
policyJSON := []byte(`{
|
||||
"default_policy": "allow",
|
||||
"global": {
|
||||
@@ -1102,7 +1105,7 @@ func TestNewFieldsInGlobalRule(t *testing.T) {
|
||||
},
|
||||
"rules": {
|
||||
"1": {
|
||||
"description": "Kind 1 events"
|
||||
"description": "Kind 1 events - has specific rule, so global is ignored"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
@@ -1112,7 +1115,8 @@ func TestNewFieldsInGlobalRule(t *testing.T) {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Event without protected tag should fail global rule
|
||||
// Kind 1 has a specific rule, so global protected_required is IGNORED
|
||||
// Event should be ALLOWED even without protected tag
|
||||
ev := createTestEventForNewFields(t, signer, "test", 1)
|
||||
addTagString(ev, "expiration", int64ToString(ev.CreatedAt+3600))
|
||||
if err := ev.Sign(signer); chk.E(err) {
|
||||
@@ -1124,23 +1128,39 @@ func TestNewFieldsInGlobalRule(t *testing.T) {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
|
||||
if allowed {
|
||||
t.Error("Global protected_required should deny event without - tag")
|
||||
if !allowed {
|
||||
t.Error("Kind 1 has specific rule - global protected_required should be ignored, event should be allowed")
|
||||
}
|
||||
|
||||
// Add protected tag
|
||||
addTagString(ev, "-", "")
|
||||
if err := ev.Sign(signer); chk.E(err) {
|
||||
// Now test kind 999 which has NO specific rule - global should apply
|
||||
ev2 := createTestEventForNewFields(t, signer, "test", 999)
|
||||
addTagString(ev2, "expiration", int64ToString(ev2.CreatedAt+3600))
|
||||
if err := ev2.Sign(signer); chk.E(err) {
|
||||
t.Fatalf("Failed to sign: %v", err)
|
||||
}
|
||||
|
||||
allowed, err = policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
|
||||
allowed, err = policy.CheckPolicy("write", ev2, pubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
|
||||
if allowed {
|
||||
t.Error("Kind 999 has NO specific rule - global protected_required should apply, event should be denied")
|
||||
}
|
||||
|
||||
// Add protected tag to kind 999 event - should now be allowed
|
||||
addTagString(ev2, "-", "")
|
||||
if err := ev2.Sign(signer); chk.E(err) {
|
||||
t.Fatalf("Failed to sign: %v", err)
|
||||
}
|
||||
|
||||
allowed, err = policy.CheckPolicy("write", ev2, pubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy error: %v", err)
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
t.Error("Should allow event with protected tag and valid expiry")
|
||||
t.Error("Kind 999 with protected tag and valid expiry should be allowed by global rule")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user