//go:generate mockgen -destination mock_acl/mock_acl.go github.com/anyproto/any-sync/acl AclService package acl import ( "context" "errors" "time" "github.com/prometheus/client_golang/prometheus" commonaccount "github.com/anyproto/any-sync/accountservice" "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/app/logger" "github.com/anyproto/any-sync/app/ocache" "github.com/anyproto/any-sync/commonspace/object/acl/list" "github.com/anyproto/any-sync/consensus/consensusclient" "github.com/anyproto/any-sync/consensus/consensusproto" "github.com/anyproto/any-sync/metric" "github.com/anyproto/any-sync/util/crypto" ) const CName = "coordinator.acl" var log = logger.NewNamed(CName) var ErrLimitExceed = errors.New("limit exceed") type Limits struct { ReadMembers uint32 WriteMembers uint32 } func New() AclService { return &aclService{} } type AclService interface { AddRecord(ctx context.Context, spaceId string, rec *consensusproto.RawRecord, limits Limits) (result *consensusproto.RawRecordWithId, err error) RecordsAfter(ctx context.Context, spaceId, aclHead string) (result []*consensusproto.RawRecordWithId, err error) Permissions(ctx context.Context, identity crypto.PubKey, spaceId string) (res list.AclPermissions, err error) OwnerPubKey(ctx context.Context, spaceId string) (ownerIdentity crypto.PubKey, err error) ReadState(ctx context.Context, spaceId string, f func(s *list.AclState) error) (err error) HasRecord(ctx context.Context, spaceId, recordId string) (has bool, err error) app.ComponentRunnable } type aclService struct { consService consensusclient.Service cache ocache.OCache accountService commonaccount.Service } func (as *aclService) Init(a *app.App) (err error) { as.consService = app.MustComponent[consensusclient.Service](a) as.accountService = app.MustComponent[commonaccount.Service](a) var metricReg *prometheus.Registry if m := a.Component(metric.CName); m != nil { metricReg = m.(metric.Metric).Registry() } as.cache = ocache.New(as.loadObject, ocache.WithTTL(5*time.Minute), ocache.WithLogger(log.Sugar()), ocache.WithPrometheus(metricReg, "acl", ""), ) return } func (as *aclService) Name() (name string) { return CName } func (as *aclService) loadObject(ctx context.Context, id string) (ocache.Object, error) { return as.newAclObject(ctx, id) } func (as *aclService) get(ctx context.Context, spaceId string) (list.AclList, error) { obj, err := as.cache.Get(ctx, spaceId) if err != nil { return nil, err } aObj := obj.(*aclObject) aObj.lastUsage.Store(time.Now()) return aObj.AclList, nil } func (as *aclService) AddRecord(ctx context.Context, spaceId string, rec *consensusproto.RawRecord, limits Limits) (result *consensusproto.RawRecordWithId, err error) { if limits.ReadMembers <= 1 && limits.WriteMembers <= 1 { return nil, ErrLimitExceed } acl, err := as.get(ctx, spaceId) if err != nil { return nil, err } acl.RLock() defer acl.RUnlock() var beforeReaders, beforeWriters int for _, acc := range acl.AclState().CurrentAccounts() { if !acc.Permissions.NoPermissions() { beforeReaders++ } if acc.Permissions.CanWrite() { beforeWriters++ } } err = acl.ValidateRawRecord(rec, func(state *list.AclState) error { var readers, writers int for _, acc := range state.CurrentAccounts() { if acc.Permissions.NoPermissions() { continue } readers++ if acc.Permissions.CanWrite() { writers++ } } if readers > beforeReaders && uint32(readers) > limits.ReadMembers { return ErrLimitExceed } if writers > beforeWriters && uint32(writers) > limits.WriteMembers { return ErrLimitExceed } return nil }) if err != nil { return } return as.consService.AddRecord(ctx, spaceId, rec) } func (as *aclService) RecordsAfter(ctx context.Context, spaceId, aclHead string) (result []*consensusproto.RawRecordWithId, err error) { acl, err := as.get(ctx, spaceId) if err != nil { return nil, err } acl.RLock() defer acl.RUnlock() return acl.RecordsAfter(ctx, aclHead) } func (as *aclService) OwnerPubKey(ctx context.Context, spaceId string) (ownerIdentity crypto.PubKey, err error) { acl, err := as.get(ctx, spaceId) if err != nil { return } acl.RLock() defer acl.RUnlock() return acl.AclState().OwnerPubKey() } func (as *aclService) Permissions(ctx context.Context, identity crypto.PubKey, spaceId string) (res list.AclPermissions, err error) { acl, err := as.get(ctx, spaceId) if err != nil { return } acl.RLock() defer acl.RUnlock() return acl.AclState().Permissions(identity), nil } func (as *aclService) ReadState(ctx context.Context, spaceId string, f func(s *list.AclState) error) (err error) { acl, err := as.get(ctx, spaceId) if err != nil { return } acl.RLock() defer acl.RUnlock() return f(acl.AclState()) } func (as *aclService) HasRecord(ctx context.Context, spaceId, recordId string) (has bool, err error) { acl, err := as.get(ctx, spaceId) if err != nil { return } acl.RLock() defer acl.RUnlock() acl.Iterate(func(record *list.AclRecord) (isContinue bool) { if record.Id == recordId { has = true return false } return true }) return } func (as *aclService) Run(ctx context.Context) (err error) { return } func (as *aclService) Close(ctx context.Context) (err error) { return as.cache.Close() }