implement Milestone 17: Contacts interface with read/write permissions

This commit is contained in:
2026-01-18 16:29:07 +01:00
parent 00b9ceb467
commit 72a06f542b
6 changed files with 1230 additions and 1 deletions

View File

@@ -0,0 +1,466 @@
// contacts_interface.cpp - Contacts interface implementation
// Milestone 17: Contact access with granular read/write permissions
#include "contacts_interface.h"
#include "lua_sandbox.h"
#include <lua.hpp>
#include <algorithm>
#include <cctype>
namespace mosis {
// Helper: case-insensitive string search
static bool ContainsIgnoreCase(const std::string& haystack, const std::string& needle) {
if (needle.empty()) return true;
if (haystack.empty()) return false;
auto it = std::search(
haystack.begin(), haystack.end(),
needle.begin(), needle.end(),
[](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
}
);
return it != haystack.end();
}
// ============================================================================
// ContactsInterface
// ============================================================================
ContactsInterface::ContactsInterface(const std::string& app_id)
: m_app_id(app_id)
{
}
ContactsInterface::~ContactsInterface() = default;
bool ContactsInterface::MatchesSearch(const Contact& contact, const std::string& search) const {
if (search.empty()) return true;
return ContainsIgnoreCase(contact.name, search) ||
ContainsIgnoreCase(contact.phone, search) ||
ContainsIgnoreCase(contact.email, search);
}
std::vector<Contact> ContactsInterface::Query(const QueryOptions& options, std::string& error) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_has_read_permission) {
error = "permission denied: contacts.read required";
return {};
}
std::vector<Contact> results;
// Cap limit at MAX_RESULTS
int limit = std::min(options.limit, MAX_RESULTS);
if (limit <= 0) limit = 50;
int offset = std::max(options.offset, 0);
// Filter and paginate
int skipped = 0;
for (const auto& contact : m_mock_contacts) {
if (MatchesSearch(contact, options.search)) {
if (skipped < offset) {
skipped++;
continue;
}
results.push_back(contact);
if (static_cast<int>(results.size()) >= limit) {
break;
}
}
}
return results;
}
std::optional<Contact> ContactsInterface::GetById(int id, std::string& error) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_has_read_permission) {
error = "permission denied: contacts.read required";
return std::nullopt;
}
for (const auto& contact : m_mock_contacts) {
if (contact.id == id) {
return contact;
}
}
error = "contact not found";
return std::nullopt;
}
int ContactsInterface::Add(const Contact& contact, std::string& error) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_has_write_permission) {
error = "permission denied: contacts.write required";
return 0;
}
if (contact.name.empty()) {
error = "contact name is required";
return 0;
}
Contact newContact = contact;
newContact.id = m_next_id++;
m_mock_contacts.push_back(newContact);
return newContact.id;
}
bool ContactsInterface::Update(const Contact& contact, std::string& error) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_has_write_permission) {
error = "permission denied: contacts.write required";
return false;
}
if (contact.id <= 0) {
error = "invalid contact id";
return false;
}
for (auto& c : m_mock_contacts) {
if (c.id == contact.id) {
c.name = contact.name;
c.phone = contact.phone;
c.email = contact.email;
return true;
}
}
error = "contact not found";
return false;
}
bool ContactsInterface::Delete(int id, std::string& error) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_has_write_permission) {
error = "permission denied: contacts.write required";
return false;
}
auto it = std::find_if(m_mock_contacts.begin(), m_mock_contacts.end(),
[id](const Contact& c) { return c.id == id; });
if (it == m_mock_contacts.end()) {
error = "contact not found";
return false;
}
m_mock_contacts.erase(it);
return true;
}
int ContactsInterface::GetCount(std::string& error) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_has_read_permission) {
error = "permission denied: contacts.read required";
return -1;
}
return static_cast<int>(m_mock_contacts.size());
}
void ContactsInterface::SetMockContacts(const std::vector<Contact>& contacts) {
std::lock_guard<std::mutex> lock(m_mutex);
m_mock_contacts = contacts;
// Update next ID to be higher than any existing ID
m_next_id = 1;
for (const auto& c : m_mock_contacts) {
if (c.id >= m_next_id) {
m_next_id = c.id + 1;
}
}
}
void ContactsInterface::ClearMockContacts() {
std::lock_guard<std::mutex> lock(m_mutex);
m_mock_contacts.clear();
m_next_id = 1;
}
// ============================================================================
// Lua API
// ============================================================================
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global (at -1)
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if _G has a metatable with __index (sandbox proxy pattern)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found proxy, set in the real _G (__index table)
lua_pushvalue(L, -4); // Copy value
lua_setfield(L, -2, name);
lua_pop(L, 4); // Pop __index, metatable, _G, original value
return;
}
lua_pop(L, 2); // Pop nil/__index and metatable
}
// No proxy, set in _G directly
lua_pushvalue(L, -2); // Copy value
lua_setfield(L, -2, name);
lua_pop(L, 2); // Pop _G and original value
}
static const char* CONTACTS_INTERFACE_KEY = "mosis.contacts_interface";
static ContactsInterface* GetContactsInterface(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, CONTACTS_INTERFACE_KEY);
auto* contacts = static_cast<ContactsInterface*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return contacts;
}
// Push Contact as Lua table
static void PushContact(lua_State* L, const Contact& contact) {
lua_createtable(L, 0, 4);
lua_pushinteger(L, contact.id);
lua_setfield(L, -2, "id");
lua_pushstring(L, contact.name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, contact.phone.c_str());
lua_setfield(L, -2, "phone");
lua_pushstring(L, contact.email.c_str());
lua_setfield(L, -2, "email");
}
// Read Contact from Lua table at given index
static Contact ReadContact(lua_State* L, int index) {
Contact contact;
if (lua_type(L, index) != LUA_TTABLE) {
return contact;
}
lua_getfield(L, index, "id");
if (lua_isinteger(L, -1)) {
contact.id = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
lua_getfield(L, index, "name");
if (lua_isstring(L, -1)) {
contact.name = lua_tostring(L, -1);
}
lua_pop(L, 1);
lua_getfield(L, index, "phone");
if (lua_isstring(L, -1)) {
contact.phone = lua_tostring(L, -1);
}
lua_pop(L, 1);
lua_getfield(L, index, "email");
if (lua_isstring(L, -1)) {
contact.email = lua_tostring(L, -1);
}
lua_pop(L, 1);
return contact;
}
// contacts.query(options) -> array of contacts
static int lua_contacts_query(lua_State* L) {
auto* contacts = GetContactsInterface(L);
if (!contacts) {
return luaL_error(L, "contacts interface not available");
}
QueryOptions options;
// Parse options table (optional)
if (lua_gettop(L) >= 1 && lua_istable(L, 1)) {
lua_getfield(L, 1, "search");
if (lua_isstring(L, -1)) {
options.search = lua_tostring(L, -1);
}
lua_pop(L, 1);
lua_getfield(L, 1, "limit");
if (lua_isinteger(L, -1)) {
options.limit = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
lua_getfield(L, 1, "offset");
if (lua_isinteger(L, -1)) {
options.offset = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
}
std::string error;
auto results = contacts->Query(options, error);
if (!error.empty()) {
return luaL_error(L, "%s", error.c_str());
}
// Return array of contacts
lua_createtable(L, static_cast<int>(results.size()), 0);
for (size_t i = 0; i < results.size(); i++) {
PushContact(L, results[i]);
lua_rawseti(L, -2, static_cast<int>(i + 1));
}
return 1;
}
// contacts.getById(id) -> contact or nil
static int lua_contacts_getById(lua_State* L) {
auto* contacts = GetContactsInterface(L);
if (!contacts) {
return luaL_error(L, "contacts interface not available");
}
int id = static_cast<int>(luaL_checkinteger(L, 1));
std::string error;
auto contact = contacts->GetById(id, error);
if (!error.empty() && error != "contact not found") {
return luaL_error(L, "%s", error.c_str());
}
if (contact) {
PushContact(L, *contact);
} else {
lua_pushnil(L);
}
return 1;
}
// contacts.getCount() -> number
static int lua_contacts_getCount(lua_State* L) {
auto* contacts = GetContactsInterface(L);
if (!contacts) {
return luaL_error(L, "contacts interface not available");
}
std::string error;
int count = contacts->GetCount(error);
if (!error.empty()) {
return luaL_error(L, "%s", error.c_str());
}
lua_pushinteger(L, count);
return 1;
}
// contacts.add(contact) -> id
static int lua_contacts_add(lua_State* L) {
auto* contacts = GetContactsInterface(L);
if (!contacts) {
return luaL_error(L, "contacts interface not available");
}
luaL_checktype(L, 1, LUA_TTABLE);
Contact contact = ReadContact(L, 1);
std::string error;
int id = contacts->Add(contact, error);
if (!error.empty()) {
return luaL_error(L, "%s", error.c_str());
}
lua_pushinteger(L, id);
return 1;
}
// contacts.update(contact) -> boolean
static int lua_contacts_update(lua_State* L) {
auto* contacts = GetContactsInterface(L);
if (!contacts) {
return luaL_error(L, "contacts interface not available");
}
luaL_checktype(L, 1, LUA_TTABLE);
Contact contact = ReadContact(L, 1);
std::string error;
bool success = contacts->Update(contact, error);
if (!error.empty() && !success) {
return luaL_error(L, "%s", error.c_str());
}
lua_pushboolean(L, success);
return 1;
}
// contacts.delete(id) -> boolean
static int lua_contacts_delete(lua_State* L) {
auto* contacts = GetContactsInterface(L);
if (!contacts) {
return luaL_error(L, "contacts interface not available");
}
int id = static_cast<int>(luaL_checkinteger(L, 1));
std::string error;
bool success = contacts->Delete(id, error);
if (!error.empty() && !success) {
return luaL_error(L, "%s", error.c_str());
}
lua_pushboolean(L, success);
return 1;
}
void RegisterContactsAPI(lua_State* L, ContactsInterface* contacts) {
// Store interface pointer
lua_pushlightuserdata(L, contacts);
lua_setfield(L, LUA_REGISTRYINDEX, CONTACTS_INTERFACE_KEY);
// Create contacts table
lua_createtable(L, 0, 6);
lua_pushcfunction(L, lua_contacts_query);
lua_setfield(L, -2, "query");
lua_pushcfunction(L, lua_contacts_getById);
lua_setfield(L, -2, "getById");
lua_pushcfunction(L, lua_contacts_getCount);
lua_setfield(L, -2, "getCount");
lua_pushcfunction(L, lua_contacts_add);
lua_setfield(L, -2, "add");
lua_pushcfunction(L, lua_contacts_update);
lua_setfield(L, -2, "update");
lua_pushcfunction(L, lua_contacts_delete);
lua_setfield(L, -2, "delete");
// Set as global "contacts" (bypassing sandbox proxy)
SetGlobalInRealG(L, "contacts");
}
} // namespace mosis

View File

@@ -0,0 +1,94 @@
// contacts_interface.h - Contacts interface for Lua sandbox
// Milestone 17: Contact access with granular read/write permissions
#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