1
0
Fork 0
mirror of https://github.com/anyproto/any-sync.git synced 2025-06-07 21:47:02 +09:00
any-sync/commonspace/object/acl/list/acltestsuite.go
2025-05-12 13:59:29 +02:00

616 lines
16 KiB
Go

package list
import (
"errors"
"fmt"
"strings"
"github.com/anyproto/any-sync/commonspace/object/accountdata"
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
"github.com/anyproto/any-sync/commonspace/object/acl/recordverifier"
"github.com/anyproto/any-sync/consensus/consensusproto"
"github.com/anyproto/any-sync/util/crypto"
)
type TestAclState struct {
Keys *accountdata.AccountKeys
Acl AclList
}
type accountExpectedState struct {
perms AclPermissions
status AclStatus
metadata []byte
pseudoId string
recCmd string
}
type AclTestExecutor struct {
owner string
spaceId string
ownerKeys *accountdata.AccountKeys
ownerMeta []byte
root *consensusproto.RawRecordWithId
invites map[string]crypto.PrivKey
actualAccounts map[string]*TestAclState
expectedAccounts map[string]*accountExpectedState
expectedPermissions map[string][]accountExpectedState
}
func NewAclExecutor(spaceId string) *AclTestExecutor {
return &AclTestExecutor{
spaceId: spaceId,
invites: map[string]crypto.PrivKey{},
actualAccounts: make(map[string]*TestAclState),
expectedAccounts: make(map[string]*accountExpectedState),
expectedPermissions: make(map[string][]accountExpectedState),
}
}
func NewExternalKeysAclExecutor(spaceId string, keys *accountdata.AccountKeys, ownerMeta []byte, root *consensusproto.RawRecordWithId) *AclTestExecutor {
return &AclTestExecutor{
spaceId: spaceId,
ownerKeys: keys,
root: root,
ownerMeta: ownerMeta,
invites: map[string]crypto.PrivKey{},
actualAccounts: make(map[string]*TestAclState),
expectedAccounts: make(map[string]*accountExpectedState),
expectedPermissions: make(map[string][]accountExpectedState),
}
}
var (
errIncorrectParts = errors.New("incorrect parts")
)
func (a *AclTestExecutor) buildBatchRequest(args []string, acl AclList, getPerm func(perm string) AclPermissions, addRec func(rec *consensusproto.RawRecordWithId) error) (afterAll []func(), err error) {
// remove:a,b,c;add:d,rw,m1|e,r,m2;changes:f,rw|g,r;revoke:inv1id;decline:g,h;
batchPayload := BatchRequestPayload{}
for _, arg := range args {
parts := strings.Split(arg, ":")
if len(parts) != 2 {
return nil, errIncorrectParts
}
command := parts[0]
commandArgs := strings.Split(parts[1], "|")
switch command {
case "add":
var payloads []AccountAdd
for _, arg := range commandArgs {
argParts := strings.Split(arg, ",")
if len(argParts) != 3 {
return nil, errIncorrectParts
}
keys, err := accountdata.NewRandom()
if err != nil {
return nil, err
}
ownerAcl := a.actualAccounts[a.owner].Acl.(*aclList)
accountAcl, err := BuildAclListWithIdentity(keys, ownerAcl.storage, recordverifier.NewValidateFull())
if err != nil {
return nil, err
}
state := &TestAclState{
Keys: keys,
Acl: accountAcl,
}
account := argParts[0]
a.actualAccounts[account] = state
a.expectedAccounts[account] = &accountExpectedState{
perms: getPerm(argParts[1]),
status: StatusActive,
metadata: []byte(argParts[2]),
pseudoId: account,
}
payloads = append(payloads, AccountAdd{
Identity: keys.SignKey.GetPublic(),
Permissions: getPerm(argParts[1]),
Metadata: []byte(argParts[2]),
})
}
defer func() {
if err != nil {
for _, arg := range args {
argParts := strings.Split(arg, ",")
account := argParts[0]
delete(a.expectedAccounts, account)
delete(a.actualAccounts, account)
}
}
}()
batchPayload.Additions = payloads
case "remove":
identities := strings.Split(commandArgs[0], ",")
var pubKeys []crypto.PubKey
for _, id := range identities {
pk := a.actualAccounts[id].Keys.SignKey.GetPublic()
pubKeys = append(pubKeys, pk)
}
priv, _, err := crypto.GenerateRandomEd25519KeyPair()
if err != nil {
return nil, err
}
sym := crypto.NewAES()
payload := AccountRemovePayload{
Identities: pubKeys,
Change: ReadKeyChangePayload{
MetadataKey: priv,
ReadKey: sym,
},
}
batchPayload.Removals = payload
afterAll = append(afterAll, func() {
for _, id := range identities {
a.expectedAccounts[id].status = StatusRemoved
a.expectedAccounts[id].perms = AclPermissionsNone
}
})
case "changes":
var payloads []PermissionChangePayload
for _, arg := range commandArgs {
argParts := strings.Split(arg, ",")
if len(argParts) != 2 {
return nil, errIncorrectParts
}
changed := a.actualAccounts[argParts[0]].Keys.SignKey.GetPublic()
perms := getPerm(argParts[1])
payloads = append(payloads, PermissionChangePayload{
Identity: changed,
Permissions: perms,
})
afterAll = append(afterAll, func() {
a.expectedAccounts[argParts[0]].perms = perms
})
}
batchPayload.Changes = payloads
case "revoke":
invite := a.invites[commandArgs[0]]
invId, err := acl.AclState().GetInviteIdByPrivKey(invite)
if err != nil {
return nil, err
}
batchPayload.InviteRevokes = append(batchPayload.InviteRevokes, invId)
case "decline":
id := commandArgs[0]
pk := a.actualAccounts[id].Keys.SignKey.GetPublic()
rec, err := acl.AclState().JoinRecord(pk, false)
if err != nil {
return nil, err
}
batchPayload.Declines = append(batchPayload.Declines, rec.RecordId)
afterAll = append(afterAll, func() {
a.expectedAccounts[id].status = StatusDeclined
})
case "approve":
recs, err := acl.AclState().JoinRecords(false)
if err != nil {
return nil, err
}
argParts := strings.Split(commandArgs[0], ",")
if len(argParts) != 2 {
return nil, errIncorrectParts
}
approved := a.actualAccounts[argParts[0]].Keys.SignKey.GetPublic()
var recId string
for _, rec := range recs {
if rec.RequestIdentity.Equals(approved) {
recId = rec.RecordId
}
}
if recId == "" {
return nil, fmt.Errorf("no join records for approve")
}
perms := getPerm(argParts[1])
afterAll = append(afterAll, func() {
a.expectedAccounts[argParts[0]].status = StatusActive
a.expectedAccounts[argParts[0]].perms = perms
})
batchPayload.Approvals = append(batchPayload.Approvals, RequestAcceptPayload{
RequestRecordId: recId,
Permissions: perms,
})
}
}
res, err := acl.RecordBuilder().BuildBatchRequest(batchPayload)
if err != nil {
return nil, err
}
return afterAll, addRec(WrapAclRecord(res))
}
func (a *AclTestExecutor) Execute(cmd string) (err error) {
parts := strings.Split(cmd, "::")
if len(parts) != 2 {
return errIncorrectParts
}
commandParts := strings.Split(parts[0], ".")
if len(commandParts) != 2 {
return errIncorrectParts
}
account := commandParts[0]
command := commandParts[1]
args := strings.Split(parts[1], ";")
if len(args) == 0 {
return errIncorrectParts
}
var isNewAccount bool
if _, exists := a.actualAccounts[account]; !exists {
isNewAccount = true
keys, err := accountdata.NewRandom()
if err != nil {
return err
}
if len(a.expectedAccounts) == 0 {
meta := []byte(account)
var acl AclList
if a.ownerKeys == nil {
acl, err = newInMemoryDerivedAclMetadata(a.spaceId, keys, meta)
if err != nil {
return err
}
} else {
keys = a.ownerKeys
meta = a.ownerMeta
acl, err = newInMemoryAclWithRoot(keys, a.root)
if err != nil {
return err
}
}
state := &TestAclState{
Keys: keys,
Acl: acl,
}
a.actualAccounts[account] = state
a.expectedAccounts[account] = &accountExpectedState{
perms: AclPermissions(aclrecordproto.AclUserPermissions_Owner),
status: StatusActive,
metadata: meta,
pseudoId: account,
}
a.owner = account
return nil
} else {
ownerAcl := a.actualAccounts[a.owner].Acl.(*aclList)
copyStorage := ownerAcl.storage.(*inMemoryStorage).Copy()
accountAcl, err := BuildAclListWithIdentity(keys, copyStorage, recordverifier.NewValidateFull())
if err != nil {
return err
}
state := &TestAclState{
Keys: keys,
Acl: accountAcl,
}
a.actualAccounts[account] = state
a.expectedAccounts[account] = &accountExpectedState{
metadata: []byte(account),
pseudoId: account,
}
}
} else if a.expectedAccounts[account].status == StatusRemoved {
keys := a.actualAccounts[account].Keys
ownerAcl := a.actualAccounts[a.owner].Acl.(*aclList)
copyStorage := ownerAcl.storage.(*inMemoryStorage).Copy()
accountAcl, err := BuildAclListWithIdentity(keys, copyStorage, recordverifier.NewValidateFull())
if err != nil {
return err
}
a.actualAccounts[account].Acl = accountAcl
}
acl := a.actualAccounts[account].Acl
var afterAll []func()
defer func() {
if err != nil {
if !isNewAccount {
return
}
delete(a.expectedAccounts, account)
delete(a.actualAccounts, account)
} else {
for _, f := range afterAll {
f()
}
head := acl.Head().Id
var states []accountExpectedState
for _, state := range a.expectedAccounts {
cp := *state
cp.recCmd = cmd
states = append(states, cp)
}
a.expectedPermissions[head] = states
}
}()
addRec := func(rec *consensusproto.RawRecordWithId) error {
for _, acc := range a.actualAccounts {
err := acc.Acl.AddRawRecord(rec)
if err != nil {
return err
}
}
return nil
}
getPerm := func(perm string) AclPermissions {
var aclPerm aclrecordproto.AclUserPermissions
switch perm {
case "own":
aclPerm = aclrecordproto.AclUserPermissions_Owner
case "adm":
aclPerm = aclrecordproto.AclUserPermissions_Admin
case "rw":
aclPerm = aclrecordproto.AclUserPermissions_Writer
case "none":
aclPerm = aclrecordproto.AclUserPermissions_None
case "r":
aclPerm = aclrecordproto.AclUserPermissions_Reader
case "g":
aclPerm = aclrecordproto.AclUserPermissions_Guest
}
return AclPermissions(aclPerm)
}
switch command {
case "join":
invite := a.invites[args[0]]
requestJoin, err := acl.RecordBuilder().BuildRequestJoin(RequestJoinPayload{
InviteKey: invite,
Metadata: []byte(account),
})
if err != nil {
return err
}
err = addRec(WrapAclRecord(requestJoin))
if err != nil {
return err
}
a.expectedAccounts[account].status = StatusJoining
case "invite":
res, err := acl.RecordBuilder().BuildInvite()
if err != nil {
return err
}
a.invites[args[0]] = res.InviteKey
err = addRec(WrapAclRecord(res.InviteRec))
if err != nil {
return err
}
case "invite_anyone":
res, err := acl.RecordBuilder().BuildInviteAnyone()
if err != nil {
return err
}
a.invites[args[0]] = res.InviteKey
err = addRec(WrapAclRecord(res.InviteRec))
if err != nil {
return err
}
case "batch":
afterAll, err = a.buildBatchRequest(args, acl, getPerm, addRec)
if err != nil {
return err
}
case "approve":
recs, err := acl.AclState().JoinRecords(false)
if err != nil {
return err
}
argParts := strings.Split(args[0], ",")
if len(argParts) != 2 {
return errIncorrectParts
}
approved := a.actualAccounts[argParts[0]].Keys.SignKey.GetPublic()
var recId string
for _, rec := range recs {
if rec.RequestIdentity.Equals(approved) {
recId = rec.RecordId
}
}
if recId == "" {
return fmt.Errorf("no join records for approve")
}
perms := getPerm(argParts[1])
res, err := acl.RecordBuilder().BuildRequestAccept(RequestAcceptPayload{
RequestRecordId: recId,
Permissions: perms,
})
if err != nil {
return err
}
err = addRec(WrapAclRecord(res))
if err != nil {
return err
}
a.expectedAccounts[argParts[0]].status = StatusActive
a.expectedAccounts[argParts[0]].perms = perms
case "changes":
var payloads []PermissionChangePayload
for _, arg := range args {
argParts := strings.Split(arg, ",")
if len(argParts) != 2 {
return errIncorrectParts
}
changed := a.actualAccounts[argParts[0]].Keys.SignKey.GetPublic()
perms := getPerm(argParts[1])
payloads = append(payloads, PermissionChangePayload{
Identity: changed,
Permissions: perms,
})
afterAll = append(afterAll, func() {
a.expectedAccounts[argParts[0]].perms = perms
})
}
permissionChanges := PermissionChangesPayload{Changes: payloads}
res, err := acl.RecordBuilder().BuildPermissionChanges(permissionChanges)
if err != nil {
return err
}
err = addRec(WrapAclRecord(res))
if err != nil {
return err
}
case "add":
var payloads []AccountAdd
for _, arg := range args {
argParts := strings.Split(arg, ",")
if len(argParts) != 3 {
return errIncorrectParts
}
keys, err := accountdata.NewRandom()
if err != nil {
return err
}
ownerAcl := a.actualAccounts[a.owner].Acl.(*aclList)
accountAcl, err := BuildAclListWithIdentity(keys, ownerAcl.storage, recordverifier.NewValidateFull())
if err != nil {
return err
}
state := &TestAclState{
Keys: keys,
Acl: accountAcl,
}
account = argParts[0]
a.actualAccounts[account] = state
a.expectedAccounts[account] = &accountExpectedState{
perms: getPerm(argParts[1]),
status: StatusActive,
metadata: []byte(argParts[2]),
pseudoId: account,
}
payloads = append(payloads, AccountAdd{
Identity: keys.SignKey.GetPublic(),
Permissions: getPerm(argParts[1]),
Metadata: []byte(argParts[2]),
})
}
defer func() {
if err != nil {
for _, arg := range args {
argParts := strings.Split(arg, ",")
account := argParts[0]
delete(a.expectedAccounts, account)
delete(a.actualAccounts, account)
}
}
}()
res, err := acl.RecordBuilder().BuildAccountsAdd(AccountsAddPayload{Additions: payloads})
if err != nil {
return err
}
err = addRec(WrapAclRecord(res))
if err != nil {
return err
}
case "invite_join":
invite := a.invites[args[0]]
inviteJoin, err := acl.RecordBuilder().BuildInviteJoin(InviteJoinPayload{
InviteKey: invite,
Metadata: []byte(account),
})
if err != nil {
return err
}
err = addRec(WrapAclRecord(inviteJoin))
if err != nil {
return err
}
a.expectedAccounts[account].status = StatusActive
a.expectedAccounts[account].perms = AclPermissions(aclrecordproto.AclUserPermissions_Reader)
case "remove":
identities := strings.Split(args[0], ",")
var pubKeys []crypto.PubKey
for _, id := range identities {
pk := a.actualAccounts[id].Keys.SignKey.GetPublic()
pubKeys = append(pubKeys, pk)
}
priv, _, err := crypto.GenerateRandomEd25519KeyPair()
if err != nil {
return err
}
sym := crypto.NewAES()
res, err := acl.RecordBuilder().BuildAccountRemove(AccountRemovePayload{
Identities: pubKeys,
Change: ReadKeyChangePayload{
MetadataKey: priv,
ReadKey: sym,
},
})
if err != nil {
return err
}
err = addRec(WrapAclRecord(res))
if err != nil {
return err
}
for _, id := range identities {
a.expectedAccounts[id].status = StatusRemoved
a.expectedAccounts[id].perms = AclPermissionsNone
}
case "request_remove":
id := args[0]
res, err := acl.RecordBuilder().BuildRequestRemove()
if err != nil {
return err
}
err = addRec(WrapAclRecord(res))
if err != nil {
return err
}
a.expectedAccounts[id].status = StatusRemoving
case "decline":
id := args[0]
pk := a.actualAccounts[id].Keys.SignKey.GetPublic()
rec, err := acl.AclState().JoinRecord(pk, false)
if err != nil {
return err
}
res, err := acl.RecordBuilder().BuildRequestDecline(rec.RecordId)
if err != nil {
return err
}
err = addRec(WrapAclRecord(res))
if err != nil {
return err
}
a.expectedAccounts[id].status = StatusDeclined
case "cancel":
id := args[0]
pk := a.actualAccounts[id].Keys.SignKey.GetPublic()
rec, err := acl.AclState().Record(pk)
if err != nil {
return err
}
res, err := acl.RecordBuilder().BuildRequestCancel(rec.RecordId)
if err != nil {
return err
}
err = addRec(WrapAclRecord(res))
if err != nil {
return err
}
if rec.Type == RequestTypeJoin {
a.expectedAccounts[id].status = StatusCanceled
} else {
a.expectedAccounts[id].status = StatusActive
}
case "revoke":
invite := a.invites[args[0]]
invId, err := acl.AclState().GetInviteIdByPrivKey(invite)
if err != nil {
return err
}
requestJoin, err := acl.RecordBuilder().BuildInviteRevoke(invId)
if err != nil {
return err
}
err = addRec(WrapAclRecord(requestJoin))
if err != nil {
return err
}
default:
return fmt.Errorf("unexpected")
}
return nil
}
func (a *AclTestExecutor) ActualAccounts() map[string]*TestAclState {
return a.actualAccounts
}