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:
2025-09-07 16:13:07 +01:00
parent b6ea3d5181
commit f5a8c094e4
10 changed files with 293 additions and 17 deletions

59
pkg/acl/acl.go Normal file
View 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
View 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))
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}