Files
MosisService/docs/SANDBOX_MILESTONE_17.md

473 lines
12 KiB
Markdown

# Milestone 17: Virtual Hardware - Contacts
**Status**: Complete
**Goal**: Contact access with granular read/write permissions.
---
## Overview
This milestone implements secure Contacts access for Lua apps:
- Separate permissions: `contacts.read` and `contacts.write`
- Limited contact fields: name, phone, email (no raw account data)
- Query with search/pagination support
- Mock mode for desktop testing
- Maximum 100 results per query
### Key Deliverables
1. **ContactsInterface class** - Contact query and management
2. **Contact struct** - Limited contact fields (privacy-preserving)
3. **Lua contacts API** - `contacts.query()`, `contacts.add()`, `contacts.update()`, `contacts.delete()`
4. **Privacy protection** - Limited fields, permission enforcement
---
## File Structure
```
src/main/cpp/sandbox/
├── contacts_interface.h # NEW - Contacts API header
└── contacts_interface.cpp # NEW - Contacts implementation
```
---
## Implementation Details
### 1. ContactsInterface Class
```cpp
// contacts_interface.h
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <functional>
#include <mutex>
#include <unordered_map>
struct lua_State;
namespace mosis {
struct Contact {
int id = 0; // Internal ID
std::string name; // Display name
std::string phone; // Primary phone number
std::string email; // Primary email
};
struct QueryOptions {
std::string search; // Search string (matches name, phone, email)
int limit = 50; // Max results (capped at 100)
int offset = 0; // Pagination offset
};
enum class ContactsError {
None,
PermissionDenied,
NotFound,
InvalidData,
LimitExceeded
};
class ContactsInterface {
public:
ContactsInterface(const std::string& app_id);
~ContactsInterface();
// Permission checks
bool HasReadPermission() const { return m_has_read_permission; }
void SetReadPermission(bool granted) { m_has_read_permission = granted; }
bool HasWritePermission() const { return m_has_write_permission; }
void SetWritePermission(bool granted) { m_has_write_permission = granted; }
// Query contacts (requires read permission)
std::vector<Contact> Query(const QueryOptions& options, std::string& error);
// Get single contact by ID (requires read permission)
std::optional<Contact> GetById(int id, std::string& error);
// Add contact (requires write permission)
int Add(const Contact& contact, std::string& error);
// Update contact (requires write permission)
bool Update(const Contact& contact, std::string& error);
// Delete contact (requires write permission)
bool Delete(int id, std::string& error);
// Get total contact count (requires read permission)
int GetCount(std::string& error);
// For testing
void SetMockMode(bool enabled) { m_mock_mode = enabled; }
bool IsMockMode() const { return m_mock_mode; }
// Mock mode: set contacts directly
void SetMockContacts(const std::vector<Contact>& contacts);
// Clear all mock contacts
void ClearMockContacts();
private:
std::string m_app_id;
bool m_mock_mode = true;
bool m_has_read_permission = false;
bool m_has_write_permission = false;
std::vector<Contact> m_mock_contacts;
int m_next_id = 1;
mutable std::mutex m_mutex;
static constexpr int MAX_RESULTS = 100;
bool MatchesSearch(const Contact& contact, const std::string& search) const;
};
// Register contacts.* APIs as globals
void RegisterContactsAPI(lua_State* L, ContactsInterface* contacts);
} // namespace mosis
```
### 2. Permission Requirements
| Action | Permission Required |
|--------|---------------------|
| Query contacts | `contacts.read` |
| Get contact by ID | `contacts.read` |
| Get contact count | `contacts.read` |
| Add contact | `contacts.write` |
| Update contact | `contacts.write` |
| Delete contact | `contacts.write` |
### 3. Privacy Protection
Contact fields exposed:
- `id` - Internal identifier
- `name` - Display name
- `phone` - Primary phone number
- `email` - Primary email address
NOT exposed:
- Account type/name (Google, Exchange, etc.)
- Contact photo
- Multiple phone numbers/emails
- Address, notes, organization
- Social profiles
- Custom fields
- Sync metadata
### 4. Lua API
```lua
-- Query contacts (requires contacts.read)
local results = contacts.query({
search = "John", -- optional: search name/phone/email
limit = 20, -- optional: max results (default 50, max 100)
offset = 0 -- optional: pagination offset
})
for _, c in ipairs(results) do
print(c.id, c.name, c.phone, c.email)
end
-- Get by ID (requires contacts.read)
local contact = contacts.getById(123)
if contact then
print(contact.name)
end
-- Get total count (requires contacts.read)
local count = contacts.getCount()
-- Add contact (requires contacts.write)
local newId = contacts.add({
name = "John Doe",
phone = "555-1234",
email = "john@example.com"
})
-- Update contact (requires contacts.write)
local success = contacts.update({
id = 123,
name = "John Smith",
phone = "555-5678",
email = "john.smith@example.com"
})
-- Delete contact (requires contacts.write)
local success = contacts.delete(123)
```
### 5. Error Handling
```lua
-- Errors are raised via Lua errors
-- Use pcall for safe error handling
local ok, result = pcall(function()
return contacts.query({search = "test"})
end)
if not ok then
print("Error:", result) -- e.g., "permission denied: contacts.read required"
end
```
---
## Test Cases
### Test 1: Query Requires Read Permission
```cpp
bool Test_ContactsRequiresReadPermission(std::string& error_msg) {
mosis::ContactsInterface contacts("test.app");
// No permission granted
std::string err;
auto results = contacts.Query({}, err);
EXPECT_TRUE(results.empty());
EXPECT_TRUE(err.find("permission") != std::string::npos);
return true;
}
```
### Test 2: Add Requires Write Permission
```cpp
bool Test_ContactsRequiresWritePermission(std::string& error_msg) {
mosis::ContactsInterface contacts("test.app");
contacts.SetReadPermission(true); // Read OK, but not write
std::string err;
mosis::Contact contact{"John Doe", "555-1234", "john@test.com"};
int id = contacts.Add(contact, err);
EXPECT_TRUE(id == 0); // Failed
EXPECT_TRUE(err.find("permission") != std::string::npos);
return true;
}
```
### Test 3: Query Returns Matching Contacts
```cpp
bool Test_ContactsQueryWorks(std::string& error_msg) {
mosis::ContactsInterface contacts("test.app");
contacts.SetReadPermission(true);
// Set mock contacts
std::vector<mosis::Contact> mockContacts = {
{1, "John Doe", "555-1234", "john@test.com"},
{2, "Jane Doe", "555-5678", "jane@test.com"},
{3, "Bob Smith", "555-9999", "bob@test.com"}
};
contacts.SetMockContacts(mockContacts);
std::string err;
auto results = contacts.Query({.search = "Doe"}, err);
EXPECT_TRUE(err.empty());
EXPECT_TRUE(results.size() == 2); // John and Jane
EXPECT_TRUE(results[0].name == "John Doe" || results[0].name == "Jane Doe");
return true;
}
```
### Test 4: Query Respects Limit
```cpp
bool Test_ContactsQueryLimit(std::string& error_msg) {
mosis::ContactsInterface contacts("test.app");
contacts.SetReadPermission(true);
// Set 50 mock contacts
std::vector<mosis::Contact> mockContacts;
for (int i = 0; i < 50; i++) {
mockContacts.push_back({i + 1, "Contact " + std::to_string(i), "555-" + std::to_string(i), ""});
}
contacts.SetMockContacts(mockContacts);
std::string err;
// Query with limit
auto results = contacts.Query({.limit = 10}, err);
EXPECT_TRUE(results.size() == 10);
// Query with excessive limit (capped at MAX_RESULTS)
results = contacts.Query({.limit = 200}, err);
EXPECT_TRUE(results.size() == 50); // All contacts (< MAX_RESULTS)
return true;
}
```
### Test 5: Add/Update/Delete Works
```cpp
bool Test_ContactsCRUD(std::string& error_msg) {
mosis::ContactsInterface contacts("test.app");
contacts.SetReadPermission(true);
contacts.SetWritePermission(true);
std::string err;
// Add
mosis::Contact newContact{0, "Test User", "555-1111", "test@test.com"};
int id = contacts.Add(newContact, err);
EXPECT_TRUE(id > 0);
EXPECT_TRUE(err.empty());
// Read back
auto contact = contacts.GetById(id, err);
EXPECT_TRUE(contact.has_value());
EXPECT_TRUE(contact->name == "Test User");
// Update
contact->name = "Updated User";
contact->phone = "555-2222";
bool updated = contacts.Update(*contact, err);
EXPECT_TRUE(updated);
// Verify update
contact = contacts.GetById(id, err);
EXPECT_TRUE(contact->name == "Updated User");
EXPECT_TRUE(contact->phone == "555-2222");
// Delete
bool deleted = contacts.Delete(id, err);
EXPECT_TRUE(deleted);
// Verify deleted
contact = contacts.GetById(id, err);
EXPECT_TRUE(!contact.has_value());
return true;
}
```
### Test 6: Delete Nonexistent Fails
```cpp
bool Test_ContactsDeleteNotFound(std::string& error_msg) {
mosis::ContactsInterface contacts("test.app");
contacts.SetWritePermission(true);
std::string err;
bool deleted = contacts.Delete(99999, err);
EXPECT_TRUE(!deleted);
EXPECT_TRUE(err.find("not found") != std::string::npos);
return true;
}
```
### Test 7: Lua Integration
```cpp
bool Test_ContactsLuaIntegration(std::string& error_msg) {
SandboxContext ctx = TestContext();
LuaSandbox sandbox(ctx);
mosis::ContactsInterface contacts("test.app");
mosis::RegisterContactsAPI(sandbox.GetState(), &contacts);
std::string script = R"lua(
-- Test that contacts global exists
if not contacts then
error("contacts global not found")
end
if not contacts.query then
error("contacts.query not found")
end
if not contacts.getById then
error("contacts.getById not found")
end
if not contacts.getCount then
error("contacts.getCount not found")
end
if not contacts.add then
error("contacts.add not found")
end
if not contacts.update then
error("contacts.update not found")
end
if not contacts.delete then
error("contacts.delete not found")
end
)lua";
bool ok = sandbox.LoadString(script, "contacts_test");
if (!ok) {
error_msg = "Lua test failed: " + sandbox.GetLastError();
return false;
}
return true;
}
```
---
## Acceptance Criteria
All tests must pass:
- [x] `Test_ContactsRequiresReadPermission` - Read permission required for query
- [x] `Test_ContactsRequiresWritePermission` - Write permission required for add
- [x] `Test_ContactsQueryWorks` - Query returns matching contacts
- [x] `Test_ContactsQueryLimit` - Query respects limit/pagination
- [x] `Test_ContactsCRUD` - Add/update/delete works
- [x] `Test_ContactsDeleteNotFound` - Delete nonexistent contact fails gracefully
- [x] `Test_ContactsLuaIntegration` - Lua API works
---
## Dependencies
- Milestone 1 (LuaSandbox)
- Milestone 2 (PermissionGate)
---
## Notes
### Desktop vs Android Implementation
For desktop testing, ContactsInterface operates in mock mode:
- Stores contacts in memory
- Full CRUD support for testing
- Search matches name, phone, or email
On Android, the real implementation would:
1. Use ContentResolver to query ContactsContract
2. Handle READ_CONTACTS and WRITE_CONTACTS permissions
3. Use proper contact lookup/insert/update/delete URIs
### Security Considerations
1. **Separate permissions**: Read and write are independent
2. **Limited fields**: Only name, phone, email exposed
3. **Query limits**: Maximum 100 results per query
4. **Audit logging**: All contact operations logged
### Privacy Features
1. **No account info**: Account type/name hidden
2. **No photos**: Contact photos not accessible
3. **Single values**: Only primary phone/email (no full list)
4. **No sync metadata**: Internal sync state hidden
---
## Next Steps
After Milestone 17 passes:
1. Milestone 18: Inter-App Communication