mirror of
https://github.com/anyproto/any-sync.git
synced 2025-06-07 21:47:02 +09:00
616 lines
16 KiB
Go
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
|
|
}
|