package list import ( "context" "fmt" "path/filepath" "testing" anystore "github.com/anyproto/any-store" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anyproto/any-sync/commonspace/headsync/headstorage" "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 aclFixture struct { ownerKeys *accountdata.AccountKeys accountKeys *accountdata.AccountKeys ownerAcl *aclList accountAcl *aclList spaceId string } func createStore(ctx context.Context, t *testing.T) anystore.DB { path := filepath.Join(t.TempDir(), "list.db") db, err := anystore.Open(ctx, path, nil) require.NoError(t, err) t.Cleanup(func() { err := db.Close() require.NoError(t, err) }) return db } var mockMetadata = []byte("very important metadata") func newFixture(t *testing.T) *aclFixture { ownerKeys, err := accountdata.NewRandom() require.NoError(t, err) accountKeys, err := accountdata.NewRandom() require.NoError(t, err) spaceId := "spaceId" ctx := context.Background() ownerAcl, err := newDerivedAclWithStoreProvider(spaceId, ownerKeys, []byte("metadata"), func(root *consensusproto.RawRecordWithId) (Storage, error) { store := createStore(ctx, t) headStorage, err := headstorage.New(ctx, store) require.NoError(t, err) return CreateStorage(ctx, root, headStorage, store) }) require.NoError(t, err) accountAcl, err := newAclWithStoreProvider(ownerAcl.Root(), accountKeys, func(root *consensusproto.RawRecordWithId) (Storage, error) { store := createStore(ctx, t) headStorage, err := headstorage.New(ctx, store) require.NoError(t, err) return CreateStorage(ctx, root, headStorage, store) }) require.NoError(t, err) require.Equal(t, ownerAcl.AclState().lastRecordId, ownerAcl.Id()) require.Equal(t, ownerAcl.AclState().lastRecordId, accountAcl.AclState().lastRecordId) require.NotEmpty(t, ownerAcl.Id()) meta, err := ownerAcl.AclState().GetMetadata(ownerKeys.SignKey.GetPublic(), true) require.NoError(t, err) require.Equal(t, []byte("metadata"), meta) return &aclFixture{ ownerKeys: ownerKeys, accountKeys: accountKeys, ownerAcl: ownerAcl.(*aclList), accountAcl: accountAcl.(*aclList), spaceId: spaceId, } } func (fx *aclFixture) addRec(t *testing.T, rec *consensusproto.RawRecordWithId) { err := fx.ownerAcl.AddRawRecord(rec) require.NoError(t, err) err = fx.accountAcl.AddRawRecord(rec) require.NoError(t, err) } func (fx *aclFixture) inviteAccount(t *testing.T, perms AclPermissions) { var ( ownerAcl = fx.ownerAcl ownerState = func() *AclState { return ownerAcl.aclState } accountAcl = fx.accountAcl accountState = func() *AclState { return accountAcl.aclState } ) // building invite inv, err := ownerAcl.RecordBuilder().BuildInvite() require.NoError(t, err) inviteRec := WrapAclRecord(inv.InviteRec) fx.addRec(t, inviteRec) // building request join requestJoin, err := accountAcl.RecordBuilder().BuildRequestJoin(RequestJoinPayload{ InviteKey: inv.InviteKey, Metadata: mockMetadata, }) require.NoError(t, err) requestJoinRec := WrapAclRecord(requestJoin) fx.addRec(t, requestJoinRec) // building request accept requestAccept, err := ownerAcl.RecordBuilder().BuildRequestAccept(RequestAcceptPayload{ RequestRecordId: requestJoinRec.Id, Permissions: perms, }) require.NoError(t, err) // validate err = ownerAcl.ValidateRawRecord(requestAccept, nil) require.NoError(t, err) requestAcceptRec := WrapAclRecord(requestAccept) fx.addRec(t, requestAcceptRec) // checking acl state for _, acl := range []*aclList{ownerAcl, accountAcl} { require.True(t, acl.AclState().Permissions(ownerAcl.AclState().pubKey).IsOwner()) require.True(t, acl.AclState().Permissions(acl.AclState().pubKey).CanWrite()) require.Equal(t, 0, len(acl.AclState().pendingRequests)) } permsAtJoinRec, err := ownerState().PermissionsAtRecord(requestJoinRec.Id, accountState().pubKey) require.NoError(t, err) require.Equal(t, AclPermissionsNone, permsAtJoinRec) permsAtRec, err := ownerState().PermissionsAtRecord(requestAcceptRec.Id, accountState().pubKey) require.NoError(t, err) require.True(t, permsAtRec == perms) require.Equal(t, ownerAcl.AclState().lastRecordId, requestAcceptRec.Id) require.Equal(t, ownerAcl.AclState().lastRecordId, accountAcl.AclState().lastRecordId) require.NotEmpty(t, requestAcceptRec.Id) } func TestAclList_BuildRoot(t *testing.T) { randomKeys, err := accountdata.NewRandom() require.NoError(t, err) randomAcl, err := NewInMemoryDerivedAcl("spaceId", randomKeys) require.NoError(t, err) fmt.Println(randomAcl.Id()) } func TestAclList_InvitePipeline(t *testing.T) { fx := newFixture(t) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer)) } func TestAclList_PermissionsAtRecord(t *testing.T) { t.Run("non-existing record", func(t *testing.T) { fx := newFixture(t) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer)) _, err := fx.ownerAcl.aclState.PermissionsAtRecord("some", fx.ownerKeys.SignKey.GetPublic()) require.Equal(t, ErrNoSuchRecord, err) }) } func TestAclList_InviteRevoke(t *testing.T) { fx := newFixture(t) var ( ownerAcl = fx.ownerAcl ownerState = func() *AclState { return ownerAcl.aclState } accountAcl = fx.accountAcl accountState = func() *AclState { return accountAcl.aclState } ) // building invite inv, err := fx.ownerAcl.RecordBuilder().BuildInvite() require.NoError(t, err) inviteRec := WrapAclRecord(inv.InviteRec) fx.addRec(t, inviteRec) // building invite revoke inviteRevoke, err := fx.ownerAcl.RecordBuilder().BuildInviteRevoke(ownerState().lastRecordId) require.NoError(t, err) inviteRevokeRec := WrapAclRecord(inviteRevoke) fx.addRec(t, inviteRevokeRec) // checking acl state require.True(t, ownerState().Permissions(ownerState().pubKey).IsOwner()) require.True(t, ownerState().Permissions(accountState().pubKey).NoPermissions()) require.Empty(t, ownerState().invites) require.Empty(t, accountState().invites) } func TestAclList_RequestDecline(t *testing.T) { fx := newFixture(t) var ( ownerAcl = fx.ownerAcl ownerState = func() *AclState { return ownerAcl.aclState } accountAcl = fx.accountAcl accountState = func() *AclState { return accountAcl.aclState } ) // building invite inv, err := ownerAcl.RecordBuilder().BuildInvite() require.NoError(t, err) inviteRec := WrapAclRecord(inv.InviteRec) fx.addRec(t, inviteRec) // building request join requestJoin, err := accountAcl.RecordBuilder().BuildRequestJoin(RequestJoinPayload{ InviteKey: inv.InviteKey, }) require.NoError(t, err) requestJoinRec := WrapAclRecord(requestJoin) fx.addRec(t, requestJoinRec) // building request decline requestDecline, err := ownerAcl.RecordBuilder().BuildRequestDecline(ownerState().lastRecordId) require.NoError(t, err) requestDeclineRec := WrapAclRecord(requestDecline) fx.addRec(t, requestDeclineRec) // checking acl state require.True(t, ownerState().Permissions(ownerState().pubKey).IsOwner()) require.True(t, ownerState().Permissions(accountState().pubKey).NoPermissions()) require.Empty(t, ownerState().pendingRequests) require.Empty(t, accountState().pendingRequests) } func TestAclList_Remove(t *testing.T) { fx := newFixture(t) var ( ownerAcl = fx.ownerAcl ownerState = func() *AclState { return ownerAcl.aclState } accountAcl = fx.accountAcl accountState = func() *AclState { return accountAcl.aclState } ) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer)) newReadKey := crypto.NewAES() privKey, pubKey, err := crypto.GenerateRandomEd25519KeyPair() require.NoError(t, err) remove, err := fx.ownerAcl.RecordBuilder().BuildAccountRemove(AccountRemovePayload{ Identities: []crypto.PubKey{fx.accountKeys.SignKey.GetPublic()}, Change: ReadKeyChangePayload{ MetadataKey: privKey, ReadKey: newReadKey, }, }) require.NoError(t, err) removeRec := WrapAclRecord(remove) fx.addRec(t, removeRec) // checking acl state require.True(t, ownerState().Permissions(ownerState().pubKey).IsOwner()) require.True(t, ownerState().Permissions(accountState().pubKey).NoPermissions()) require.True(t, ownerState().keys[removeRec.Id].ReadKey.Equals(newReadKey)) require.True(t, ownerState().keys[removeRec.Id].MetadataPrivKey.Equals(privKey)) require.True(t, ownerState().keys[removeRec.Id].MetadataPubKey.Equals(pubKey)) require.NotEmpty(t, ownerState().keys[fx.ownerAcl.Id()]) require.Equal(t, 0, len(ownerState().pendingRequests)) require.Equal(t, 0, len(accountState().pendingRequests)) require.True(t, accountState().Permissions(ownerState().pubKey).IsOwner()) require.True(t, accountState().Permissions(accountState().pubKey).NoPermissions()) require.NotEmpty(t, accountState().keys[removeRec.Id]) require.Nil(t, accountState().keys[removeRec.Id].MetadataPrivKey) require.NotNil(t, accountState().keys[removeRec.Id].MetadataPubKey) require.Nil(t, accountState().keys[removeRec.Id].ReadKey) require.NotEmpty(t, accountState().keys[fx.ownerAcl.Id()]) } func TestAclList_FixAcceptPanic(t *testing.T) { fx := newFixture(t) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer)) _, err := BuildAclListWithIdentity(fx.accountKeys, fx.ownerAcl.storage, recordverifier.NewValidateFull()) require.NoError(t, err) } func TestAclList_KeyChangeInvite(t *testing.T) { fx := newFixture(t) newReadKey := crypto.NewAES() privKey, _, err := crypto.GenerateRandomEd25519KeyPair() require.NoError(t, err) readKeyChange, err := fx.ownerAcl.RecordBuilder().BuildReadKeyChange(ReadKeyChangePayload{ MetadataKey: privKey, ReadKey: newReadKey, }) require.NoError(t, err) readKeyRec := WrapAclRecord(readKeyChange) fx.addRec(t, readKeyRec) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer)) } func TestAclList_MetadataDecrypt(t *testing.T) { fx := newFixture(t) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer)) meta, err := fx.ownerAcl.AclState().GetMetadata(fx.accountKeys.SignKey.GetPublic(), true) require.NoError(t, err) require.Equal(t, mockMetadata, meta) meta, err = fx.ownerAcl.AclState().GetMetadata(fx.accountKeys.SignKey.GetPublic(), false) require.NoError(t, err) require.NotEqual(t, mockMetadata, meta) } func TestAclList_ReadKeyChange(t *testing.T) { fx := newFixture(t) var ( ownerAcl = fx.ownerAcl ownerState = func() *AclState { return ownerAcl.aclState } accountAcl = fx.accountAcl accountState = func() *AclState { return accountAcl.aclState } ) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Admin)) newReadKey := crypto.NewAES() privKey, pubKey, err := crypto.GenerateRandomEd25519KeyPair() require.NoError(t, err) readKeyChange, err := fx.ownerAcl.RecordBuilder().BuildReadKeyChange(ReadKeyChangePayload{ MetadataKey: privKey, ReadKey: newReadKey, }) require.NoError(t, err) readKeyRec := WrapAclRecord(readKeyChange) fx.addRec(t, readKeyRec) // checking acl state require.True(t, ownerState().Permissions(ownerState().pubKey).IsOwner()) require.True(t, ownerState().Permissions(accountState().pubKey).CanManageAccounts()) require.True(t, ownerState().keys[readKeyRec.Id].ReadKey.Equals(newReadKey)) require.True(t, ownerState().keys[readKeyRec.Id].MetadataPrivKey.Equals(privKey)) require.True(t, ownerState().keys[readKeyRec.Id].MetadataPubKey.Equals(pubKey)) require.True(t, accountState().keys[readKeyRec.Id].ReadKey.Equals(newReadKey)) require.NotEmpty(t, ownerState().keys[fx.ownerAcl.Id()]) require.NotEmpty(t, accountState().keys[fx.ownerAcl.Id()]) readKey, err := ownerState().CurrentReadKey() require.NoError(t, err) require.True(t, newReadKey.Equals(readKey)) require.Equal(t, 0, len(ownerState().pendingRequests)) require.Equal(t, 0, len(accountState().pendingRequests)) } func TestAclList_PermissionChange(t *testing.T) { fx := newFixture(t) var ( ownerAcl = fx.ownerAcl ownerState = func() *AclState { return ownerAcl.aclState } accountAcl = fx.accountAcl accountState = func() *AclState { return accountAcl.aclState } ) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Admin)) permissionChange, err := fx.ownerAcl.RecordBuilder().BuildPermissionChange(PermissionChangePayload{ Identity: fx.accountKeys.SignKey.GetPublic(), Permissions: AclPermissions(aclrecordproto.AclUserPermissions_Writer), }) require.NoError(t, err) permissionChangeRec := WrapAclRecord(permissionChange) fx.addRec(t, permissionChangeRec) // checking acl state for _, acl := range []*aclList{fx.ownerAcl, fx.accountAcl} { require.True(t, acl.AclState().Permissions(ownerState().pubKey).IsOwner()) require.True(t, acl.AclState().Permissions(accountState().pubKey).CanWrite()) require.Equal(t, 0, len(acl.AclState().pendingRequests)) require.NotEmpty(t, acl.AclState().keys[fx.ownerAcl.Id()]) } } func TestAclList_RequestRemove(t *testing.T) { fx := newFixture(t) var ( ownerAcl = fx.ownerAcl ownerState = func() *AclState { return ownerAcl.aclState } accountAcl = fx.accountAcl accountState = func() *AclState { return accountAcl.aclState } ) fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer)) removeRequest, err := fx.accountAcl.RecordBuilder().BuildRequestRemove() require.NoError(t, err) removeRequestRec := WrapAclRecord(removeRequest) fx.addRec(t, removeRequestRec) recs := fx.accountAcl.AclState().RemoveRecords() require.Len(t, recs, 1) require.True(t, accountState().pubKey.Equals(recs[0].RequestIdentity)) newReadKey := crypto.NewAES() privKey, _, err := crypto.GenerateRandomEd25519KeyPair() require.NoError(t, err) remove, err := fx.ownerAcl.RecordBuilder().BuildAccountRemove(AccountRemovePayload{ Identities: []crypto.PubKey{recs[0].RequestIdentity}, Change: ReadKeyChangePayload{ MetadataKey: privKey, ReadKey: newReadKey, }, }) require.NoError(t, err) removeRec := WrapAclRecord(remove) fx.addRec(t, removeRec) // checking acl state for _, acl := range []*aclList{fx.ownerAcl, fx.accountAcl} { require.True(t, acl.AclState().Permissions(ownerState().pubKey).IsOwner()) require.True(t, acl.AclState().Permissions(accountState().pubKey).NoPermissions()) require.Equal(t, 0, len(acl.AclState().pendingRequests)) } require.True(t, ownerState().keys[removeRec.Id].ReadKey.Equals(newReadKey)) require.NotEmpty(t, ownerState().keys[fx.ownerAcl.Id()]) require.Nil(t, accountState().keys[removeRec.Id].MetadataPrivKey) require.NotNil(t, accountState().keys[removeRec.Id].MetadataPubKey) require.Nil(t, accountState().keys[removeRec.Id].ReadKey) require.NotEmpty(t, accountState().keys[fx.ownerAcl.Id()]) } func TestAclState_OwnerPubKey(t *testing.T) { fx := newFixture(t) pubKey, err := fx.ownerAcl.AclState().OwnerPubKey() require.NoError(t, err) assert.Equal(t, fx.ownerKeys.SignKey.GetPublic().Account(), pubKey.Account()) }