Introduce ACL registry with follows implementation, enhance SaveEvent for replaceable kinds, and refactor filter-based serial fetching. Update configs and dependencies.
This commit is contained in:
@@ -32,6 +32,8 @@ type C struct {
|
||||
Pprof string `env:"ORLY_PPROF" usage:"enable pprof in modes: cpu,memory,allocation"`
|
||||
IPWhitelist []string `env:"ORLY_IP_WHITELIST" usage:"comma-separated list of IP addresses to allow access from, matches on prefixes to allow private subnets, eg 10.0.0 = 10.0.0.0/8"`
|
||||
Admins []string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
|
||||
Owners []string `env:"ORLY_OWNERS" usage:"comma-separated list of owner npubs, who have full control of the relay for wipe and restart and other functions"`
|
||||
ACLMode string `env:"ORLY_ACL_MODE" usage:"ACL mode: follows,none" default:"follows"`
|
||||
}
|
||||
|
||||
// New creates and initializes a new configuration object for the relay
|
||||
|
||||
@@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
database "database.orly"
|
||||
"database.orly/indexes/types"
|
||||
"encoders.orly/envelopes/eventenvelope"
|
||||
"encoders.orly/event"
|
||||
@@ -21,22 +20,11 @@ import (
|
||||
func (l *Listener) GetSerialsFromFilter(f *filter.F) (
|
||||
sers types.Uint40s, err error,
|
||||
) {
|
||||
var idxs []database.Range
|
||||
if idxs, err = database.GetIndexesFromFilter(f); chk.E(err) {
|
||||
return
|
||||
}
|
||||
for _, idx := range idxs {
|
||||
var s types.Uint40s
|
||||
if s, err = l.GetSerialsByRange(idx); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
sers = append(sers, s...)
|
||||
}
|
||||
return
|
||||
return l.D.GetSerialsFromFilter(f)
|
||||
}
|
||||
|
||||
func (l *Listener) HandleDelete(env *eventenvelope.Submission) {
|
||||
log.T.C(
|
||||
log.I.C(
|
||||
func() string {
|
||||
return fmt.Sprintf(
|
||||
"delete event\n%s", env.E.Serialize(),
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
acl "acl.orly"
|
||||
"encoders.orly/envelopes/eventenvelope"
|
||||
"encoders.orly/kind"
|
||||
"lol.mleku.dev/chk"
|
||||
@@ -69,9 +70,15 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
|
||||
}
|
||||
}
|
||||
// store the event
|
||||
log.I.F("saving event %0x, %s", env.E.ID, env.E.Serialize())
|
||||
if _, _, err = l.SaveEvent(l.Ctx, env.E); chk.E(err) {
|
||||
return
|
||||
}
|
||||
// if a follow list was saved, reconfigure ACLs now that it is persisted
|
||||
if env.E.Kind == kind.FollowList.K {
|
||||
if err = acl.Registry.Configure(); chk.E(err) {
|
||||
}
|
||||
}
|
||||
l.publishers.Deliver(env.E)
|
||||
// Send a success response storing
|
||||
if err = Ok.Ok(l, env, ""); chk.E(err) {
|
||||
|
||||
3
go.mod
3
go.mod
@@ -3,6 +3,8 @@ module next.orly.dev
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
acl.orly v0.0.0-00010101000000-000000000000
|
||||
crypto.orly v0.0.0-00010101000000-000000000000
|
||||
database.orly v0.0.0-00010101000000-000000000000
|
||||
encoders.orly v0.0.0-00010101000000-000000000000
|
||||
github.com/adrg/xdg v0.5.3
|
||||
@@ -17,7 +19,6 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
crypto.orly v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
|
||||
|
||||
5
main.go
5
main.go
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
acl "acl.orly"
|
||||
database "database.orly"
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/log"
|
||||
@@ -28,6 +29,10 @@ func main() {
|
||||
); chk.E(err) {
|
||||
os.Exit(1)
|
||||
}
|
||||
acl.Registry.Active.Store(cfg.ACLMode)
|
||||
if err = acl.Registry.Configure(cfg, db); chk.E(err) {
|
||||
os.Exit(1)
|
||||
}
|
||||
quit := app.Run(ctx, cfg, db)
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, os.Interrupt)
|
||||
|
||||
59
pkg/acl/acl.go
Normal file
59
pkg/acl/acl.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"interfaces.orly/acl"
|
||||
"utils.orly/atomic"
|
||||
)
|
||||
|
||||
var Registry = &S{}
|
||||
|
||||
type S struct {
|
||||
ACL []acl.I
|
||||
Active atomic.String
|
||||
}
|
||||
|
||||
type A struct{ S }
|
||||
|
||||
func (s *S) Register(i acl.I) {
|
||||
(*s).ACL = append((*s).ACL, i)
|
||||
}
|
||||
|
||||
func (s *S) Configure(cfg ...any) (err error) {
|
||||
for _, i := range s.ACL {
|
||||
if i.Type() == s.Active.Load() {
|
||||
err = i.Configure(cfg...)
|
||||
return
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *S) GetAccessLevel(pub []byte) (level string) {
|
||||
for _, i := range s.ACL {
|
||||
if i.Type() == s.Active.Load() {
|
||||
level = i.GetAccessLevel(pub)
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *S) GetACLInfo() (name, description, documentation string) {
|
||||
for _, i := range s.ACL {
|
||||
if i.Type() == s.Active.Load() {
|
||||
name, description, documentation = i.GetACLInfo()
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *S) Type() (typ string) {
|
||||
for _, i := range s.ACL {
|
||||
if i.Type() == s.Active.Load() {
|
||||
typ = i.Type()
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
127
pkg/acl/follows.go
Normal file
127
pkg/acl/follows.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
database "database.orly"
|
||||
"database.orly/indexes/types"
|
||||
"encoders.orly/bech32encoding"
|
||||
"encoders.orly/event"
|
||||
"encoders.orly/filter"
|
||||
"encoders.orly/hex"
|
||||
"encoders.orly/kind"
|
||||
"encoders.orly/tag"
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/app/config"
|
||||
utils "utils.orly"
|
||||
)
|
||||
|
||||
type Follows struct {
|
||||
cfg *config.C
|
||||
*database.D
|
||||
followsMx sync.RWMutex
|
||||
admins [][]byte
|
||||
follows [][]byte
|
||||
}
|
||||
|
||||
func (f *Follows) Configure(cfg ...any) (err error) {
|
||||
log.I.F("configuring follows ACL")
|
||||
for _, ca := range cfg {
|
||||
switch c := ca.(type) {
|
||||
case *config.C:
|
||||
log.D.F("setting ACL config: %v", c)
|
||||
f.cfg = c
|
||||
case *database.D:
|
||||
log.D.F("setting ACL database: %s", c.Path())
|
||||
f.D = c
|
||||
default:
|
||||
err = errorf.E("invalid type: %T", reflect.TypeOf(ca))
|
||||
}
|
||||
}
|
||||
if f.cfg == nil || f.D == nil {
|
||||
err = errorf.E("both config and database must be set")
|
||||
return
|
||||
}
|
||||
// find admin follow lists
|
||||
f.followsMx.Lock()
|
||||
defer f.followsMx.Unlock()
|
||||
log.I.F("finding admins")
|
||||
f.follows, f.admins = nil, nil
|
||||
for _, admin := range f.cfg.Admins {
|
||||
log.I.F("%s", admin)
|
||||
var adm []byte
|
||||
if adm, err = bech32encoding.NpubOrHexToPublicKeyBinary(admin); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
log.I.F("admin: %0x", adm)
|
||||
f.admins = append(f.admins, adm)
|
||||
fl := &filter.F{
|
||||
Authors: tag.NewFromAny(adm),
|
||||
Kinds: kind.NewS(kind.New(kind.FollowList.K)),
|
||||
}
|
||||
var idxs []database.Range
|
||||
if idxs, err = database.GetIndexesFromFilter(fl); chk.E(err) {
|
||||
return
|
||||
}
|
||||
var sers types.Uint40s
|
||||
for _, idx := range idxs {
|
||||
var s types.Uint40s
|
||||
if s, err = f.D.GetSerialsByRange(idx); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
sers = append(sers, s...)
|
||||
}
|
||||
if len(sers) > 0 {
|
||||
for _, s := range sers {
|
||||
var ev *event.E
|
||||
if ev, err = f.D.FetchEventBySerial(s); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
log.I.F("admin follow list:\n%s", ev.Serialize())
|
||||
for _, v := range ev.Tags.GetAll([]byte("p")) {
|
||||
log.I.F("adding follow: %s", v.Value())
|
||||
var a []byte
|
||||
if a, err = hex.Dec(string(v.Value())); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
f.follows = append(f.follows, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Follows) GetAccessLevel(pub []byte) (level string) {
|
||||
if f.cfg == nil {
|
||||
return "write"
|
||||
}
|
||||
f.followsMx.RLock()
|
||||
defer f.followsMx.RUnlock()
|
||||
for _, v := range f.admins {
|
||||
if utils.FastEqual(v, pub) {
|
||||
return "admin"
|
||||
}
|
||||
}
|
||||
for _, v := range f.follows {
|
||||
if utils.FastEqual(v, pub) {
|
||||
return "write"
|
||||
}
|
||||
}
|
||||
return "read"
|
||||
}
|
||||
|
||||
func (f *Follows) GetACLInfo() (name, description, documentation string) {
|
||||
return "follows", "whitelist follows of admins",
|
||||
`This ACL mode searches for follow lists of admins and grants all followers write access`
|
||||
}
|
||||
|
||||
func (f *Follows) Type() string { return "follows" }
|
||||
|
||||
func init() {
|
||||
log.T.F("registering follows ACL")
|
||||
Registry.Register(new(Follows))
|
||||
}
|
||||
@@ -3,12 +3,14 @@ module acl.orly
|
||||
go 1.25.0
|
||||
|
||||
replace (
|
||||
acl.orly => ../acl
|
||||
crypto.orly => ../crypto
|
||||
encoders.orly => ../encoders
|
||||
database.orly => ../database
|
||||
encoders.orly => ../encoders
|
||||
interfaces.orly => ../interfaces
|
||||
next.orly.dev => ../../
|
||||
protocol.orly => ../protocol
|
||||
utils.orly => ../utils
|
||||
acl.orly => ../acl
|
||||
)
|
||||
|
||||
require interfaces.orly v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -7,20 +7,99 @@ import (
|
||||
"database.orly/indexes"
|
||||
"database.orly/indexes/types"
|
||||
"encoders.orly/event"
|
||||
"encoders.orly/filter"
|
||||
"encoders.orly/kind"
|
||||
"encoders.orly/tag"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
"lol.mleku.dev/log"
|
||||
)
|
||||
|
||||
func (d *D) GetSerialsFromFilter(f *filter.F) (
|
||||
sers types.Uint40s, err error,
|
||||
) {
|
||||
var idxs []Range
|
||||
if idxs, err = GetIndexesFromFilter(f); chk.E(err) {
|
||||
return
|
||||
}
|
||||
for _, idx := range idxs {
|
||||
var s types.Uint40s
|
||||
if s, err = d.GetSerialsByRange(idx); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
sers = append(sers, s...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SaveEvent saves an event to the database, generating all the necessary indexes.
|
||||
func (d *D) SaveEvent(c context.Context, ev *event.E) (kc, vc int, err error) {
|
||||
if ev == nil {
|
||||
err = errorf.E("nil event")
|
||||
return
|
||||
}
|
||||
// check if the event already exists
|
||||
var ser *types.Uint40
|
||||
if ser, err = d.GetSerialById(ev.ID); err == nil && ser != nil {
|
||||
err = errorf.E("event already exists: %0x", ev.ID)
|
||||
return
|
||||
}
|
||||
// check for replacement
|
||||
if kind.IsReplaceable(ev.Kind) {
|
||||
// find the events and delete them
|
||||
f := &filter.F{
|
||||
Authors: tag.NewFromBytesSlice(ev.Pubkey),
|
||||
Kinds: kind.NewS(kind.New(ev.Kind)),
|
||||
}
|
||||
var sers types.Uint40s
|
||||
if sers, err = d.GetSerialsFromFilter(f); chk.E(err) {
|
||||
return
|
||||
}
|
||||
// if found, delete them
|
||||
if len(sers) > 0 {
|
||||
for _, s := range sers {
|
||||
var oldEv *event.E
|
||||
if oldEv, err = d.FetchEventBySerial(s); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
if err = d.DeleteEventBySerial(
|
||||
c, s, oldEv,
|
||||
); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if kind.IsParameterizedReplaceable(ev.Kind) {
|
||||
// find the events and delete them
|
||||
f := &filter.F{
|
||||
Authors: tag.NewFromBytesSlice(ev.Pubkey),
|
||||
Kinds: kind.NewS(kind.New(ev.Kind)),
|
||||
Tags: tag.NewS(
|
||||
tag.NewFromAny(
|
||||
"d", ev.Tags.GetFirst([]byte("d")),
|
||||
),
|
||||
),
|
||||
}
|
||||
var sers types.Uint40s
|
||||
if sers, err = d.GetSerialsFromFilter(f); chk.E(err) {
|
||||
return
|
||||
}
|
||||
// if found, delete them
|
||||
if len(sers) > 0 {
|
||||
for _, s := range sers {
|
||||
var oldEv *event.E
|
||||
if oldEv, err = d.FetchEventBySerial(s); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
if err = d.DeleteEventBySerial(
|
||||
c, s, oldEv,
|
||||
); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get the next sequence number for the event
|
||||
var serial uint64
|
||||
if serial, err = d.seq.Next(); chk.E(err) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Package acl is an interface for implementing arbitrary access control lists.
|
||||
package acl
|
||||
|
||||
import (
|
||||
"interfaces.orly/typer"
|
||||
)
|
||||
|
||||
const (
|
||||
// Read means read only
|
||||
Read = "read"
|
||||
@@ -16,10 +20,12 @@ const (
|
||||
)
|
||||
|
||||
type I interface {
|
||||
Configure(cfg ...any) (err error)
|
||||
// GetAccessLevel returns the access level string for a given pubkey.
|
||||
GetAccessLevel(pub []byte) (level string)
|
||||
// GetACLInfo returns the name and a description of the ACL, which should
|
||||
// explain briefly how it works, and then a long text of documentation of
|
||||
// the ACL's rules and configuration (in asciidoc or markdown).
|
||||
GetACLInfo() (name, description, documentation string)
|
||||
typer.T
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user