implemented nip-86 relay management API and added to relay client
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/interfaces/acl"
|
||||
"next.orly.dev/pkg/utils/atomic"
|
||||
)
|
||||
@@ -78,3 +79,21 @@ func (s *S) AddFollow(pub []byte) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPolicy checks if an event is allowed by the active ACL policy
|
||||
func (s *S) CheckPolicy(ev *event.E) (allowed bool, err error) {
|
||||
for _, i := range s.ACL {
|
||||
if i.Type() == s.Active.Load() {
|
||||
// Check if the ACL implementation has a CheckPolicy method
|
||||
if policyChecker, ok := i.(interface {
|
||||
CheckPolicy(ev *event.E) (allowed bool, err error)
|
||||
}); ok {
|
||||
return policyChecker.CheckPolicy(ev)
|
||||
}
|
||||
// If no CheckPolicy method, default to allowing
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
// If no active ACL, default to allowing
|
||||
return true, nil
|
||||
}
|
||||
|
||||
223
pkg/acl/managed.go
Normal file
223
pkg/acl/managed.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"lol.mleku.dev/errorf"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/app/config"
|
||||
"next.orly.dev/pkg/database"
|
||||
"next.orly.dev/pkg/encoders/bech32encoding"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/utils"
|
||||
)
|
||||
|
||||
type Managed struct {
|
||||
Ctx context.Context
|
||||
cfg *config.C
|
||||
*database.D
|
||||
managedACL *database.ManagedACL
|
||||
owners [][]byte
|
||||
admins [][]byte
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *Managed) Configure(cfg ...any) (err error) {
|
||||
log.I.F("configuring managed ACL")
|
||||
for _, ca := range cfg {
|
||||
switch c := ca.(type) {
|
||||
case *config.C:
|
||||
m.cfg = c
|
||||
case *database.D:
|
||||
m.D = c
|
||||
m.managedACL = database.NewManagedACL(c)
|
||||
case context.Context:
|
||||
m.Ctx = c
|
||||
default:
|
||||
err = errorf.E("invalid type: %T", reflect.TypeOf(ca))
|
||||
}
|
||||
}
|
||||
if m.cfg == nil || m.D == nil {
|
||||
err = errorf.E("both config and database must be set")
|
||||
return
|
||||
}
|
||||
|
||||
// Load owners
|
||||
for _, owner := range m.cfg.Owners {
|
||||
if len(owner) == 0 {
|
||||
continue
|
||||
}
|
||||
var pk []byte
|
||||
if pk, err = bech32encoding.NpubOrHexToPublicKeyBinary(owner); err != nil {
|
||||
continue
|
||||
}
|
||||
m.owners = append(m.owners, pk)
|
||||
}
|
||||
|
||||
// Load admins
|
||||
for _, admin := range m.cfg.Admins {
|
||||
if len(admin) == 0 {
|
||||
continue
|
||||
}
|
||||
var pk []byte
|
||||
if pk, err = bech32encoding.NpubOrHexToPublicKeyBinary(admin); err != nil {
|
||||
continue
|
||||
}
|
||||
m.admins = append(m.admins, pk)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Managed) GetAccessLevel(pub []byte, address string) (level string) {
|
||||
m.mx.RLock()
|
||||
defer m.mx.RUnlock()
|
||||
|
||||
// If no pubkey provided and auth is required, return "none"
|
||||
if len(pub) == 0 && m.cfg.AuthRequired {
|
||||
return "none"
|
||||
}
|
||||
|
||||
// Check owners first
|
||||
for _, v := range m.owners {
|
||||
if utils.FastEqual(v, pub) {
|
||||
return "owner"
|
||||
}
|
||||
}
|
||||
|
||||
// Check admins
|
||||
for _, v := range m.admins {
|
||||
if utils.FastEqual(v, pub) {
|
||||
return "admin"
|
||||
}
|
||||
}
|
||||
|
||||
// Check if pubkey is banned
|
||||
pubkeyHex := hex.EncodeToString(pub)
|
||||
if banned, err := m.managedACL.IsPubkeyBanned(pubkeyHex); err == nil && banned {
|
||||
return "banned"
|
||||
}
|
||||
|
||||
// Check if pubkey is explicitly allowed
|
||||
if allowed, err := m.managedACL.IsPubkeyAllowed(pubkeyHex); err == nil && allowed {
|
||||
return "write"
|
||||
}
|
||||
|
||||
// Check if IP is blocked
|
||||
if blocked, err := m.managedACL.IsIPBlocked(address); err == nil && blocked {
|
||||
return "blocked"
|
||||
}
|
||||
|
||||
// Default to read-only for managed mode
|
||||
return "read"
|
||||
}
|
||||
|
||||
func (m *Managed) CheckPolicy(ev *event.E) (allowed bool, err error) {
|
||||
// Check if event is banned
|
||||
eventID := hex.EncodeToString(ev.ID)
|
||||
if banned, err := m.managedACL.IsEventBanned(eventID); err == nil && banned {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if event is explicitly allowed
|
||||
if allowed, err := m.managedACL.IsEventAllowed(eventID); err == nil && allowed {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check if event kind is allowed
|
||||
if allowed, err := m.managedACL.IsKindAllowed(int(ev.Kind)); err == nil && !allowed {
|
||||
// If there are allowed kinds configured and this kind is not in the list, deny
|
||||
allowedKinds, err := m.managedACL.ListAllowedKinds()
|
||||
if err == nil && len(allowedKinds) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check if author is banned
|
||||
authorHex := hex.EncodeToString(ev.Pubkey)
|
||||
if banned, err := m.managedACL.IsPubkeyBanned(authorHex); err == nil && banned {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if author is explicitly allowed
|
||||
if allowed, err := m.managedACL.IsPubkeyAllowed(authorHex); err == nil && allowed {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// For managed mode, default to allowing events from owners and admins
|
||||
for _, v := range m.owners {
|
||||
if utils.FastEqual(v, ev.Pubkey) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range m.admins {
|
||||
if utils.FastEqual(v, ev.Pubkey) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should add this event to moderation queue
|
||||
// This could be extended to add events to moderation based on content analysis
|
||||
// For now, we'll just allow the event
|
||||
|
||||
// Default to allowing events in managed mode (can be restricted by explicit bans/allows)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *Managed) GetACLInfo() (name, description, documentation string) {
|
||||
return "managed", "managed ACL with NIP-86 support",
|
||||
`Managed ACL mode provides fine-grained access control through NIP-86 management API.
|
||||
|
||||
Features:
|
||||
- Ban/allow specific pubkeys
|
||||
- Ban/allow specific events
|
||||
- Block IP addresses
|
||||
- Allow/deny specific event kinds
|
||||
- Relay metadata management
|
||||
- Event moderation queue
|
||||
|
||||
This mode requires explicit management through the NIP-86 API endpoints.
|
||||
Only relay owners can access the management interface and API.`
|
||||
}
|
||||
|
||||
func (m *Managed) Type() string {
|
||||
return "managed"
|
||||
}
|
||||
|
||||
func (m *Managed) Syncer() {
|
||||
// Managed ACL doesn't need background syncing
|
||||
// All management is done through the API
|
||||
}
|
||||
|
||||
// Helper methods for the management API
|
||||
|
||||
// IsIPBlocked checks if an IP address is blocked
|
||||
func (m *Managed) IsIPBlocked(ip string) bool {
|
||||
// Parse IP to handle both IPv4 and IPv6
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
blocked, err := m.managedACL.IsIPBlocked(ip)
|
||||
if err != nil {
|
||||
log.W.F("error checking if IP is blocked: %v", err)
|
||||
return false
|
||||
}
|
||||
return blocked
|
||||
}
|
||||
|
||||
// GetManagedACL returns the managed ACL database instance
|
||||
func (m *Managed) GetManagedACL() *database.ManagedACL {
|
||||
return m.managedACL
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.T.F("registering managed ACL")
|
||||
Registry.Register(new(Managed))
|
||||
}
|
||||
107
pkg/acl/managed_minimal_test.go
Normal file
107
pkg/acl/managed_minimal_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/app/config"
|
||||
"next.orly.dev/pkg/database"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
)
|
||||
|
||||
func TestManagedACL_BasicFunctionality(t *testing.T) {
|
||||
// Setup test database
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Use a temporary directory for the test database
|
||||
tmpDir := t.TempDir()
|
||||
db, err := database.New(ctx, cancel, tmpDir, "test.db")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Setup managed ACL
|
||||
cfg := &config.C{
|
||||
AuthRequired: false,
|
||||
Owners: []string{"owner1"},
|
||||
Admins: []string{"admin1"},
|
||||
}
|
||||
|
||||
managed := &Managed{
|
||||
Ctx: ctx,
|
||||
cfg: cfg,
|
||||
D: db,
|
||||
managedACL: database.NewManagedACL(db),
|
||||
owners: [][]byte{[]byte("owner1")},
|
||||
admins: [][]byte{[]byte("admin1")},
|
||||
}
|
||||
|
||||
// Test basic functionality
|
||||
t.Run("owner should get owner access", func(t *testing.T) {
|
||||
level := managed.GetAccessLevel([]byte("owner1"), "127.0.0.1")
|
||||
if level != "owner" {
|
||||
t.Errorf("GetAccessLevel() = %v, want owner", level)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("admin should get admin access", func(t *testing.T) {
|
||||
level := managed.GetAccessLevel([]byte("admin1"), "127.0.0.1")
|
||||
if level != "admin" {
|
||||
t.Errorf("GetAccessLevel() = %v, want admin", level)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default user should get read access", func(t *testing.T) {
|
||||
level := managed.GetAccessLevel([]byte("user1"), "127.0.0.1")
|
||||
if level != "read" {
|
||||
t.Errorf("GetAccessLevel() = %v, want read", level)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("owner event should be allowed", func(t *testing.T) {
|
||||
ev := createMinimalTestEvent("owner1", 1)
|
||||
allowed, err := managed.CheckPolicy(ev)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy() error = %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Errorf("CheckPolicy() = %v, want true", allowed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("admin event should be allowed", func(t *testing.T) {
|
||||
ev := createMinimalTestEvent("admin1", 1)
|
||||
allowed, err := managed.CheckPolicy(ev)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy() error = %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Errorf("CheckPolicy() = %v, want true", allowed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default event should be allowed", func(t *testing.T) {
|
||||
ev := createMinimalTestEvent("user1", 1)
|
||||
allowed, err := managed.CheckPolicy(ev)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPolicy() error = %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Errorf("CheckPolicy() = %v, want true", allowed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createMinimalTestEvent(pubkey string, kind uint16) *event.E {
|
||||
ev := event.New()
|
||||
ev.Pubkey = []byte(pubkey)
|
||||
ev.Kind = kind
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
ev.Content = []byte("test content")
|
||||
ev.Tags = nil
|
||||
ev.ID = ev.GetIDBytes()
|
||||
return ev
|
||||
}
|
||||
Reference in New Issue
Block a user