@lanonasis/cli
Version:
Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security
235 lines (234 loc) • 8.5 kB
JavaScript
/**
* Memory Access Control System
* Implements granular permissions and audit logging inspired by mem0's ACL system
*/
import { CLIConfig } from '../utils/config.js';
import { logger } from './logger.js';
export class MemoryAccessControl {
config;
accessRules = new Map();
accessLogs = [];
constructor() {
this.config = new CLIConfig();
}
/**
* Check if user has access to create memories in an app
*/
async checkCreateAccess(userId, appId) {
try {
// Default: users can create memories in their own apps
if (await this.isUserApp(userId, appId)) {
return true;
}
// Check explicit permissions
const rules = this.getAccessRules(userId, appId);
return rules.some(rule => rule.permission === 'write' || rule.permission === 'admin');
}
catch (error) {
logger.error('Access check failed', { error, userId, appId });
return false;
}
}
/**
* Check if user has access to a specific memory
*/
async checkMemoryAccess(memoryId, appId) {
try {
const memory = await this.getMemoryInfo(memoryId);
const currentUserId = await this.getCurrentUserId();
if (!memory) {
return false;
}
// Owner always has access
if (memory.user_id === currentUserId) {
return true;
}
// Check app-level permissions using CURRENT user ID, not memory owner
const rules = this.getAccessRules(currentUserId, appId);
return rules.some(rule => rule.granted &&
(!rule.expires_at || new Date(rule.expires_at) > new Date()) &&
(rule.memory_id === memoryId || !rule.memory_id));
}
catch (error) {
logger.error('Memory access check failed', { error, memoryId, appId });
return false;
}
}
/**
* Get list of accessible memory IDs for user/app combination
*/
async getAccessibleMemories(userId, appId) {
try {
// Get user's own memories
const ownMemories = await this.getUserMemories(userId);
// Get shared memories based on permissions
const sharedMemories = await this.getSharedMemories(userId, appId);
// Combine and deduplicate
const combined = [...ownMemories, ...sharedMemories];
const deduped = [];
const seen = new Set();
combined.forEach(memoryId => {
if (!seen.has(memoryId)) {
seen.add(memoryId);
deduped.push(memoryId);
}
});
return deduped;
}
catch (error) {
logger.error('Failed to get accessible memories', { error, userId, appId });
return [];
}
}
/**
* Log memory access for audit trail
*/
async logMemoryAccess(memoryId, appId, accessType, metadata) {
try {
const userId = await this.getCurrentUserId();
const logEntry = {
id: this.generateId(),
user_id: userId,
app_id: appId,
memory_id: memoryId,
access_type: accessType,
timestamp: new Date().toISOString(),
success: true,
metadata
};
this.accessLogs.push(logEntry);
// In production, this would be persisted to database
logger.debug('Memory access logged', logEntry);
// Keep only recent logs in memory (last 1000)
if (this.accessLogs.length > 1000) {
this.accessLogs = this.accessLogs.slice(-1000);
}
}
catch (error) {
logger.error('Failed to log memory access', { error, memoryId, appId, accessType });
}
}
/**
* Grant access to a memory or app
*/
async grantAccess(userId, appId, permission, memoryId, expiresAt) {
const rule = {
id: this.generateId(),
user_id: userId,
app_id: appId,
memory_id: memoryId,
permission,
granted: true,
created_at: new Date().toISOString(),
expires_at: expiresAt
};
const key = `${userId}:${appId}`;
const existingRules = this.accessRules.get(key) || [];
existingRules.push(rule);
this.accessRules.set(key, existingRules);
logger.info('Access granted', { userId, appId, permission, memoryId });
}
/**
* Revoke access to a memory or app
*/
async revokeAccess(_userId, _appId, memoryId) {
const key = `${_userId}:${_appId}`;
const existingRules = this.accessRules.get(key) || [];
const updatedRules = existingRules.map(rule => {
if (!memoryId || rule.memory_id === memoryId) {
return { ...rule, granted: false };
}
return rule;
});
this.accessRules.set(key, updatedRules);
logger.info('Access revoked', { userId: _userId, appId: _appId, memoryId });
}
/**
* Get access logs for audit purposes
*/
getAccessLogs(userId, appId, memoryId, limit = 100) {
let logs = this.accessLogs;
if (userId) {
logs = logs.filter(log => log.user_id === userId);
}
if (appId) {
logs = logs.filter(log => log.app_id === appId);
}
if (memoryId) {
logs = logs.filter(log => log.memory_id === memoryId);
}
return logs
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, limit);
}
/**
* Private helper methods
*/
getAccessRules(userId, appId) {
const key = `${userId}:${appId}`;
return this.accessRules.get(key) || [];
}
async isUserApp(userId, appId) {
// In a real implementation, this would check if the app belongs to the user
// For now, assume apps starting with user ID belong to them
return appId.startsWith(userId) || appId === 'default';
}
async getCurrentUserId() {
const token = this.config.get('token');
if (token) {
try {
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
return payload.sub || payload.user_id || 'anonymous';
}
catch {
return 'anonymous';
}
}
return 'anonymous';
}
async getMemoryInfo(memoryId) {
try {
// This would typically fetch from the API
const apiUrl = this.config.get('apiUrl') || 'https://api.lanonasis.com';
const token = this.config.get('token');
const axios = (await import('axios')).default;
const response = await axios.get(`${apiUrl}/api/v1/memories/${memoryId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data;
}
catch (error) {
logger.error('Failed to get memory info', { error, memoryId });
return null;
}
}
async getUserMemories(userId) {
try {
const apiUrl = this.config.get('apiUrl') || 'https://api.lanonasis.com';
const token = this.config.get('token');
const axios = (await import('axios')).default;
const response = await axios.get(`${apiUrl}/api/v1/memories?user_id=${userId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data.memories?.map((m) => m.id) || [];
}
catch (error) {
logger.error('Failed to get user memories', { error, userId });
return [];
}
}
async getSharedMemories(_userId, _appId) {
// This would implement logic to find memories shared with the user
// through explicit permissions or app-level sharing
return [];
}
generateId() {
return `acl_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
}
}