implement Milestone 17: Contacts interface with read/write permissions
This commit is contained in:
466
src/main/cpp/sandbox/contacts_interface.cpp
Normal file
466
src/main/cpp/sandbox/contacts_interface.cpp
Normal 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
|
||||
94
src/main/cpp/sandbox/contacts_interface.h
Normal file
94
src/main/cpp/sandbox/contacts_interface.h
Normal 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
|
||||
Reference in New Issue
Block a user