merge branches and cleanup db

This commit is contained in:
timurgordon
2025-06-27 12:11:04 +03:00
parent 5563d7e27e
commit 1f9ec01934
177 changed files with 1202 additions and 174 deletions

1916
_archive/acldb/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
_archive/acldb/Cargo.toml Normal file
View File

@@ -0,0 +1,26 @@
[package]
name = "acldb"
version = "0.1.0"
edition = "2021"
description = "HeroDB ACL Layer: Implements ACL logic, data ops, and Actix RPC server as specified in instructions.md."
[dependencies]
ourdb = { path = "../ourdb" }
tst = { path = "../tst" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
actix-web = "4"
actix-rt = "2"
actix-cors = "0.7"
utoipa = { version = "5.3.1", features = ["actix_extras"] }
utoipa-redoc = { version = "6.0", features = ["actix-web"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"
env_logger = "0.11"
thiserror = "2.0"
sha2 = "0.10"
hex = "0.4"
base64 = "0.22"
dirs = "6.0"
async-trait = "0.1"

154
_archive/acldb/README.md Normal file
View File

@@ -0,0 +1,154 @@
# ACLDB - Access Control Database
ACLDB is a secure, permission-based database system that provides fine-grained access control for data storage and retrieval. It's designed to work with the HeroDB ecosystem, offering a robust solution for managing data with complex access control requirements.
## Overview
ACLDB organizes data into "circles" and "topics" with comprehensive access control lists (ACLs) that govern who can read, write, delete, or administer different pieces of data. It's built on top of OurDB and TST (Ternary Search Tree) for efficient storage and retrieval.
## Key Features
- **Fine-grained Access Control**: Define who can access what data with a hierarchical permission system
- **Circle-based Organization**: Group data by circles (e.g., organizations, teams, projects)
- **Topic-based Categorization**: Organize data within circles by topics
- **Permission Levels**: Supports Read, Write, Delete, Execute, and Admin permission levels
- **RPC API**: Access all functionality through a well-defined RPC interface
- **REST API Server**: Includes a built-in HTTP server with Swagger/OpenAPI documentation
- **Async/Await Support**: Built with Rust's async/await for efficient concurrency
## Architecture
ACLDB consists of several key components:
1. **ACLDB**: The main database instance for a specific circle
2. **ACLDBTopic**: A database instance for a specific topic within a circle
3. **ACL**: Access Control List for managing permissions
4. **Server**: HTTP server for exposing the RPC API
5. **RpcInterface**: Interface for handling RPC requests
Data is stored using:
- **OurDB**: For efficient data storage and retrieval
- **TST**: For key-to-id mapping and prefix searches
## Permission System
ACLDB implements a hierarchical permission system with the following levels:
- **Read**: Allows reading data
- **Write**: Includes Read permission and allows writing data
- **Delete**: Includes Write permission and allows deleting data
- **Execute**: Includes Delete permission and allows executing operations
- **Admin**: Includes all permissions and allows managing ACLs
## API Methods
The RPC API provides the following methods:
### ACL Management
- **aclupdate**: Update or create an ACL with specified permissions
- **aclremove**: Remove specific public keys from an existing ACL
- **acldel**: Delete an entire ACL
### Data Operations
- **set**: Store data with optional ACL protection
- **get**: Retrieve data with ACL verification
- **del**: Delete data with ACL verification
- **prefix**: Search for keys with a specific prefix
## Usage Examples
### Starting the Server
```bash
# Start the server on localhost:8080
cargo run
# Start the server on a specific host and port
cargo run -- 0.0.0.0 9000
```
### API Documentation
Once the server is running, you can access the API documentation at:
```
http://localhost:8080/redoc
```
### Using the API
#### Creating an ACL
```json
{
"method": "aclupdate",
"params": {
"caller_pubkey": "user_public_key",
"circle_id": "my_circle",
"name": "project_data",
"pubkeys": ["user1_pubkey", "user2_pubkey"],
"right": "write"
},
"signature": "signature_here"
}
```
#### Storing Data with ACL Protection
```json
{
"method": "set",
"params": {
"caller_pubkey": "user_public_key",
"circle_id": "my_circle",
"topic": "documents",
"key": "doc1",
"value": "base64_encoded_data",
"acl_id": 1
},
"signature": "signature_here"
}
```
#### Retrieving Data
```json
{
"method": "get",
"params": {
"caller_pubkey": "user_public_key",
"circle_id": "my_circle",
"topic": "documents",
"key": "doc1"
},
"signature": "signature_here"
}
```
## Integration with Other Systems
ACLDB is designed to work seamlessly with other components of the HeroDB ecosystem. It can be used as:
1. A standalone database with access control
2. A backend for applications requiring fine-grained permissions
3. A component in a larger distributed system
## Development
### Prerequisites
- Rust 1.56 or later
- Cargo
### Building
```bash
cargo build
```
### Running Tests
```bash
cargo test
```

97
_archive/acldb/src/acl.rs Normal file
View File

@@ -0,0 +1,97 @@
use std::collections::HashMap;
use serde::{Serialize, Deserialize};
/// Represents permission levels in the ACL system
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ACLRight {
/// Read permission
Read,
/// Write permission (includes Read)
Write,
/// Delete permission (includes Write and Read)
Delete,
/// Execute permission (includes Delete, Write, and Read)
Execute,
/// Admin permission (includes all other permissions)
Admin,
}
/// Access Control List for managing permissions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ACL {
/// Unique name for the ACL within a circle
pub name: String,
/// Map of public keys to their permission levels
permissions: HashMap<String, ACLRight>,
}
impl ACL {
/// Creates a new ACL with the given name
pub fn new(name: &str) -> Self {
ACL {
name: name.to_string(),
permissions: HashMap::new(),
}
}
/// Sets a permission for a public key
pub fn set_permission(&mut self, pubkey: &str, right: ACLRight) {
self.permissions.insert(pubkey.to_string(), right);
}
/// Removes a permission for a public key
pub fn remove_permission(&mut self, pubkey: &str) {
self.permissions.remove(pubkey);
}
/// Checks if a public key has at least the specified permission level
pub fn has_permission(&self, pubkey: &str, right: ACLRight) -> bool {
if let Some(assigned_right) = self.permissions.get(pubkey) {
return *assigned_right >= right;
}
false
}
/// Gets all public keys with their associated permissions
pub fn get_all_permissions(&self) -> &HashMap<String, ACLRight> {
&self.permissions
}
/// Gets the permission level for a specific public key
pub fn get_permission(&self, pubkey: &str) -> Option<ACLRight> {
self.permissions.get(pubkey).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_acl_permissions() {
let mut acl = ACL::new("test_acl");
// Set permissions
acl.set_permission("user1", ACLRight::Read);
acl.set_permission("user2", ACLRight::Write);
acl.set_permission("user3", ACLRight::Admin);
// Check permissions
assert!(acl.has_permission("user1", ACLRight::Read));
assert!(!acl.has_permission("user1", ACLRight::Write));
assert!(acl.has_permission("user2", ACLRight::Read));
assert!(acl.has_permission("user2", ACLRight::Write));
assert!(!acl.has_permission("user2", ACLRight::Delete));
assert!(acl.has_permission("user3", ACLRight::Read));
assert!(acl.has_permission("user3", ACLRight::Write));
assert!(acl.has_permission("user3", ACLRight::Delete));
assert!(acl.has_permission("user3", ACLRight::Execute));
assert!(acl.has_permission("user3", ACLRight::Admin));
// Remove permission
acl.remove_permission("user2");
assert!(!acl.has_permission("user2", ACLRight::Read));
}
}

View File

@@ -0,0 +1,47 @@
use thiserror::Error;
/// Error types for the ACLDB module
#[derive(Error, Debug)]
pub enum Error {
/// Permission denied error
#[error("Permission denied")]
PermissionDenied,
/// Record not found error
#[error("Record not found")]
NotFound,
/// Invalid operation error
#[error("Invalid operation: {0}")]
InvalidOperation(String),
/// Path error
#[error("Path error: {0}")]
PathError(String),
/// OurDB error
#[error("OurDB error: {0}")]
OurDBError(#[from] ourdb::Error),
/// TST error
#[error("TST error: {0}")]
TSTError(#[from] tst::Error),
/// IO error
#[error("IO error: {0}")]
IOError(#[from] std::io::Error),
/// Serialization error
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
/// Signature verification error
#[error("Signature verification error: {0}")]
SignatureError(String),
/// Invalid request error
#[error("Invalid request: {0}")]
InvalidRequest(String),
}

271
_archive/acldb/src/lib.rs Normal file
View File

@@ -0,0 +1,271 @@
mod acl;
mod error;
mod topic;
mod rpc;
pub mod server;
mod utils;
pub use acl::{ACL, ACLRight};
pub use error::Error;
pub use topic::ACLDBTopic;
pub use rpc::RpcInterface;
pub use server::Server;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
use ourdb::OurDB;
use tst::TST;
/// ACLDB represents an access-controlled database instance for a specific circle
pub struct ACLDB {
/// Circle ID
circle_id: String,
/// Base directory path
base_path: String,
/// OurDB instance for the circle
db: Arc<RwLock<OurDB>>,
/// TST instance for key-to-id mapping
tst: Arc<RwLock<TST>>,
/// Cache of loaded ACLs
acl_cache: HashMap<String, ACL>,
/// Topic instances
topics: HashMap<String, Arc<RwLock<ACLDBTopic>>>,
}
impl ACLDB {
/// Creates a new ACLDB instance for the specified circle
pub fn new(circle_id: &str) -> Result<Self, Error> {
let home_dir = dirs::home_dir().ok_or_else(|| Error::PathError("Home directory not found".to_string()))?;
let base_path = home_dir.join("hero/var/ourdb").join(circle_id);
// Create directory if it doesn't exist
std::fs::create_dir_all(&base_path)?;
// Initialize OurDB for the circle
let ourdb_path = base_path.join("data");
std::fs::create_dir_all(&ourdb_path)?;
let db_config = ourdb::OurDBConfig {
path: ourdb_path,
incremental_mode: true,
file_size: None,
keysize: None,
reset: Some(false),
};
let db = OurDB::new(db_config)?;
// Initialize TST for key-to-id mapping
let tst_path = base_path.join("tst").to_string_lossy().to_string();
let tst = TST::new(&tst_path, false)?;
Ok(ACLDB {
circle_id: circle_id.to_string(),
base_path: base_path.to_string_lossy().to_string(),
db: Arc::new(RwLock::new(db)),
tst: Arc::new(RwLock::new(tst)),
acl_cache: HashMap::new(),
topics: HashMap::new(),
})
}
/// Gets a topic instance, creating it if it doesn't exist
pub fn topic(&mut self, topic_name: &str) -> Arc<RwLock<ACLDBTopic>> {
if let Some(topic) = self.topics.get(topic_name) {
return Arc::clone(topic);
}
// Since OurDB and TST don't implement Clone, we'll create new instances
// In a real implementation, we would use a connection pool or similar
let topic = Arc::new(RwLock::new(ACLDBTopic::new(
self.circle_id.clone(),
topic_name.to_string(),
Arc::new(RwLock::new(OurDB::new(ourdb::OurDBConfig {
path: Path::new(&self.base_path).join("data").join(topic_name),
incremental_mode: true,
file_size: None,
keysize: None,
reset: Some(false),
}).unwrap())),
Arc::new(RwLock::new(TST::new(
&Path::new(&self.base_path).join("tst").join(topic_name).to_string_lossy(),
false
).unwrap())),
)));
self.topics.insert(topic_name.to_string(), Arc::clone(&topic));
topic
}
/// Updates or creates an ACL with specified permissions
pub async fn acl_update(&mut self, caller_pubkey: &str, name: &str, pubkeys: &[String], right: ACLRight) -> Result<(), Error> {
// Check if caller has admin rights
self.check_admin_rights(caller_pubkey).await?;
// Get or create the ACL
let mut acl = self.get_or_create_acl(name).await?;
// Update permissions for each public key
for pubkey in pubkeys {
acl.set_permission(pubkey, right);
}
// Save the updated ACL
self.save_acl(&acl).await?;
// Update cache
self.acl_cache.insert(name.to_string(), acl);
Ok(())
}
/// Removes specific public keys from an existing ACL
pub async fn acl_remove(&mut self, caller_pubkey: &str, name: &str, pubkeys: &[String]) -> Result<(), Error> {
// Check if caller has admin rights
self.check_admin_rights(caller_pubkey).await?;
// Get the ACL
let mut acl = self.get_acl(name).await?;
// Remove permissions for each public key
for pubkey in pubkeys {
acl.remove_permission(pubkey);
}
// Save the updated ACL
self.save_acl(&acl).await?;
// Update cache
self.acl_cache.insert(name.to_string(), acl);
Ok(())
}
/// Deletes an entire ACL
pub async fn acl_del(&mut self, caller_pubkey: &str, name: &str) -> Result<(), Error> {
// Check if caller has admin rights
self.check_admin_rights(caller_pubkey).await?;
// Get the ACL to ensure it exists
let _acl = self.get_acl(name).await?;
// Get the ACL topic
let topic = self.topic("acl");
let topic = topic.write().await;
// Delete the ACL
topic.delete(name).await?;
// Remove from cache
self.acl_cache.remove(name);
Ok(())
}
/// Gets an ACL by name
pub async fn get_acl(&mut self, name: &str) -> Result<ACL, Error> {
// Check cache first
if let Some(acl) = self.acl_cache.get(name) {
return Ok(acl.clone());
}
// Get the ACL topic
let topic = self.topic("acl");
let topic = topic.read().await;
// Get the ACL data
let acl_data = topic.get(name).await?;
// Deserialize the ACL
let acl: ACL = serde_json::from_slice(&acl_data)?;
// Update cache
self.acl_cache.insert(name.to_string(), acl.clone());
Ok(acl)
}
/// Gets or creates an ACL
async fn get_or_create_acl(&mut self, name: &str) -> Result<ACL, Error> {
match self.get_acl(name).await {
Ok(acl) => Ok(acl),
Err(_) => {
// Create a new ACL
let acl = ACL::new(name);
// Save the ACL
self.save_acl(&acl).await?;
Ok(acl)
}
}
}
/// Saves an ACL to the database
async fn save_acl(&mut self, acl: &ACL) -> Result<(), Error> {
// Get the ACL topic
let topic = self.topic("acl");
let topic = topic.write().await;
// Serialize the ACL
let acl_data = serde_json::to_vec(acl)?;
// Save the ACL
topic.set_with_acl(&acl.name, &acl_data, 0).await?;
Ok(())
}
/// Checks if a user has admin rights
async fn check_admin_rights(&mut self, pubkey: &str) -> Result<(), Error> {
// Try to get the admin ACL
match self.get_acl("admin").await {
Ok(acl) => {
// Check if the user has admin rights
if acl.has_permission(pubkey, ACLRight::Admin) {
Ok(())
} else {
Err(Error::PermissionDenied)
}
},
Err(_) => {
// If the admin ACL doesn't exist, create it with the caller as admin
let mut acl = ACL::new("admin");
acl.set_permission(pubkey, ACLRight::Admin);
// Save the ACL
self.save_acl(&acl).await?;
// Update cache
self.acl_cache.insert("admin".to_string(), acl);
Ok(())
}
}
}
/// Checks if a caller is the circle owner
fn is_circle_owner(&self, caller_pubkey: &str) -> bool {
// In a real implementation, this would check against the circle's owner
// For now, we'll use a simple check based on the circle ID
// This should be replaced with proper circle ownership verification
let circle_owner_file = Path::new(&self.base_path).join("owner");
if circle_owner_file.exists() {
if let Ok(owner) = std::fs::read_to_string(circle_owner_file) {
return owner.trim() == caller_pubkey;
}
}
// If no owner file exists, check if this is the first admin operation
self.acl_cache.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
// Tests will be added here
}

View File

@@ -0,0 +1,43 @@
use acldb::{Server, Error};
use std::env;
use log::{info, error, LevelFilter};
#[actix_web::main]
async fn main() -> Result<(), Error> {
// Initialize logger
env_logger::Builder::new()
.filter_level(LevelFilter::Info)
.init();
info!("Starting ACLDB server...");
// Parse command line arguments
let args: Vec<String> = env::args().collect();
let host = args.get(1).map_or("127.0.0.1".to_string(), |s| s.clone());
let port = args.get(2)
.map_or(8080, |s| s.parse::<u16>().unwrap_or(8080));
// Create server configuration
let config = acldb::server::ServerConfig {
host,
port,
};
// Create and start server
info!("Server listening on {}:{}", config.host, config.port);
info!("API documentation available at http://{}:{}/redoc", config.host, config.port);
let server = Server::new(config);
// Start the server
match server.start().await {
Ok(_) => {
info!("Server stopped");
Ok(())
}
Err(e) => {
error!("Server error: {}", e);
Err(Error::IOError(e))
}
}
}

258
_archive/acldb/src/rpc.rs Normal file
View File

@@ -0,0 +1,258 @@
use serde::{Serialize, Deserialize};
use crate::error::Error;
use crate::acl::ACLRight;
use std::collections::HashMap;
use utoipa::ToSchema;
/// RPC request structure
///
/// This structure represents an RPC request to the ACLDB API.
/// It contains the method name, parameters, and a signature for authentication.
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct RpcRequest {
/// Method name
pub method: String,
/// JSON-encoded arguments
pub params: serde_json::Value,
/// Signature of the JSON data
pub signature: String,
}
/// RPC response structure
///
/// This structure represents the response from an RPC request.
/// It contains either a result or an error message.
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct RpcResponse {
/// Result of the operation
pub result: Option<serde_json::Value>,
/// Error message if any
pub error: Option<String>,
}
/// ACL update request parameters
///
/// Parameters for updating or creating an ACL with specified permissions.
/// Used with the `acl_update` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct AclUpdateParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the ACL exists
pub circle_id: String,
/// Unique name for the ACL within the circle
pub name: String,
/// Array of public keys to grant permissions to
pub pubkeys: Vec<String>,
/// Permission level
pub right: String,
}
/// ACL remove request parameters
///
/// Parameters for removing specific public keys from an existing ACL.
/// Used with the `acl_remove` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct AclRemoveParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the ACL exists
pub circle_id: String,
/// Name of the ACL to modify
pub name: String,
/// Array of public keys to remove from the ACL
pub pubkeys: Vec<String>,
}
/// ACL delete request parameters
///
/// Parameters for deleting an entire ACL.
/// Used with the `acl_del` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct AclDelParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the ACL exists
pub circle_id: String,
/// Name of the ACL to delete
pub name: String,
}
/// Set request parameters
///
/// Parameters for storing data with optional ACL protection.
/// Used with the `set` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct SetParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the data belongs
pub circle_id: String,
/// String identifier for the database category
pub topic: String,
/// Optional string key for the record
pub key: Option<String>,
/// Optional numeric ID for direct access
pub id: Option<u32>,
/// Base64-encoded data to store
pub value: String,
/// ID of the ACL to protect this record (0 for public access)
pub acl_id: Option<u32>,
}
/// Delete request parameters
///
/// Parameters for deleting data with ACL verification.
/// Used with the `del` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct DelParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the data belongs
pub circle_id: String,
/// String identifier for the database category
pub topic: String,
/// Optional string key for the record
pub key: Option<String>,
/// Optional numeric ID for direct access
pub id: Option<u32>,
}
/// Get request parameters
///
/// Parameters for retrieving data with ACL verification.
/// Used with the `get` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct GetParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the data belongs
pub circle_id: String,
/// String identifier for the database category
pub topic: String,
/// Optional string key for the record
pub key: Option<String>,
/// Optional numeric ID for direct access
pub id: Option<u32>,
}
/// Prefix request parameters
///
/// Parameters for searching for keys with a specific prefix.
/// Used with the `prefix` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct PrefixParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the data belongs
pub circle_id: String,
/// String identifier for the database category
pub topic: String,
/// Prefix to search for
pub prefix: String,
}
/// RPC interface for handling client requests
pub struct RpcInterface {
/// Map of method names to handler functions
handlers: HashMap<String, Box<dyn Fn(serde_json::Value) -> Result<serde_json::Value, Error> + Send + Sync>>,
}
impl RpcInterface {
/// Creates a new RPC interface
pub fn new() -> Self {
RpcInterface {
handlers: HashMap::new(),
}
}
/// Registers a handler for a method
pub fn register<F>(&mut self, method: &str, handler: F)
where
F: Fn(serde_json::Value) -> Result<serde_json::Value, Error> + Send + Sync + 'static,
{
self.handlers.insert(method.to_string(), Box::new(handler));
}
/// Handles an RPC request
pub fn handle(&self, request: RpcRequest) -> RpcResponse {
// Verify the signature
if let Err(err) = self.verify_signature(&request) {
return RpcResponse {
result: None,
error: Some(err.to_string()),
};
}
// Extract the caller's public key from the signature
let _caller_pubkey = self.extract_pubkey(&request.signature).unwrap_or_default();
// Call the appropriate handler
match self.handlers.get(&request.method) {
Some(handler) => {
match handler(request.params) {
Ok(result) => RpcResponse {
result: Some(result),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
}
None => RpcResponse {
result: None,
error: Some(format!("Method not found: {}", request.method)),
},
}
}
/// Verifies the signature of an RPC request
fn verify_signature(&self, request: &RpcRequest) -> Result<(), Error> {
// In a real implementation, this would verify the cryptographic signature
// For now, we'll just check that the signature is not empty
if request.signature.is_empty() {
return Err(Error::SignatureError("Empty signature".to_string()));
}
Ok(())
}
/// Extracts the public key from a signature
fn extract_pubkey(&self, _signature: &str) -> Result<String, Error> {
// In a real implementation, this would extract the public key from the signature
// For now, we'll just return a placeholder
Ok("extracted_pubkey".to_string())
}
/// Parses a string to an ACLRight enum
pub fn parse_acl_right(right_str: &str) -> Result<ACLRight, Error> {
match right_str.to_lowercase().as_str() {
"read" => Ok(ACLRight::Read),
"write" => Ok(ACLRight::Write),
"delete" => Ok(ACLRight::Delete),
"execute" => Ok(ACLRight::Execute),
"admin" => Ok(ACLRight::Admin),
_ => Err(Error::InvalidRequest(format!("Invalid ACL right: {}", right_str))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_acl_right() {
let rpc = RpcInterface::new();
assert_eq!(RpcInterface::parse_acl_right("read").unwrap(), ACLRight::Read);
assert_eq!(RpcInterface::parse_acl_right("write").unwrap(), ACLRight::Write);
assert_eq!(RpcInterface::parse_acl_right("delete").unwrap(), ACLRight::Delete);
assert_eq!(RpcInterface::parse_acl_right("execute").unwrap(), ACLRight::Execute);
assert_eq!(RpcInterface::parse_acl_right("admin").unwrap(), ACLRight::Admin);
assert!(RpcInterface::parse_acl_right("invalid").is_err());
}
}

View File

@@ -0,0 +1,520 @@
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use actix_web::middleware::Logger;
use actix_cors::Cors;
use std::collections::HashMap;
use std::sync::Arc;
use std::future;
use tokio::sync::mpsc;
use tokio::sync::RwLock;
use serde_json::json;
use log::{error, info};
use utoipa::OpenApi;
use utoipa_redoc::{Redoc, Servable};
use crate::ACLDB;
use crate::rpc::{RpcInterface, RpcRequest, RpcResponse, AclUpdateParams, AclRemoveParams, AclDelParams, SetParams, DelParams, GetParams, PrefixParams};
use crate::error::Error;
use crate::utils::base64_decode;
use std::collections::VecDeque;
use tokio::task;
use tokio::time::{sleep, Duration};
/// Server configuration
#[derive(Clone)]
pub struct ServerConfig {
/// Host address
pub host: String,
/// Port number
pub port: u16,
}
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
}
}
}
/// Request queue for a circle
struct CircleQueue {
/// Queue of pending requests
queue: VecDeque<(RpcRequest, mpsc::Sender<RpcResponse>)>,
/// Flag to indicate if a worker is currently processing this queue
is_processing: bool,
}
impl CircleQueue {
/// Creates a new circle queue
fn new() -> Self {
CircleQueue {
queue: VecDeque::new(),
is_processing: false,
}
}
/// Adds a request to the queue and starts processing if needed
async fn add_request(
&mut self,
request: RpcRequest,
response_sender: mpsc::Sender<RpcResponse>,
rpc_interface: Arc<RpcInterface>,
acldb_factory: Arc<ACLDBFactory>,
) {
// Add the request to the queue
self.queue.push_back((request.clone(), response_sender));
// If no worker is processing this queue, start one
if !self.is_processing {
self.is_processing = true;
// Clone what we need for the worker
let rpc = Arc::clone(&rpc_interface);
let factory = Arc::clone(&acldb_factory);
let mut queue = self.queue.clone();
// Spawn a worker task
task::spawn(async move {
// Process all requests in the queue
while let Some((req, sender)) = queue.pop_front() {
// Process the request
let response = process_request(&req, &rpc, &factory).await;
// Send the response
if let Err(err) = sender.send(response).await {
error!("Failed to send response: {}", err);
}
// Small delay to prevent CPU hogging
sleep(Duration::from_millis(1)).await;
}
});
}
}
}
/// Factory for creating ACLDB instances
pub struct ACLDBFactory {
/// Map of circle IDs to ACLDB instances
dbs: RwLock<HashMap<String, Arc<RwLock<ACLDB>>>>,
}
impl ACLDBFactory {
/// Creates a new ACLDBFactory
pub fn new() -> Self {
ACLDBFactory {
dbs: RwLock::new(HashMap::new()),
}
}
/// Gets or creates an ACLDB instance for a circle
pub async fn get_or_create(&self, circle_id: &str) -> Result<Arc<RwLock<ACLDB>>, Error> {
// Try to get an existing instance
let dbs = self.dbs.read().await;
if let Some(db) = dbs.get(circle_id) {
return Ok(Arc::clone(db));
}
drop(dbs); // Release the read lock
// Create a new instance
let db = Arc::new(RwLock::new(ACLDB::new(circle_id)?));
// Store it in the map
let mut dbs = self.dbs.write().await;
dbs.insert(circle_id.to_string(), Arc::clone(&db));
Ok(db)
}
}
/// Server for handling RPC requests
#[derive(Clone)]
pub struct Server {
/// Server configuration
config: ServerConfig,
/// RPC interface
rpc: Arc<RpcInterface>,
/// Map of circle IDs to request queues
queues: Arc<RwLock<HashMap<String, CircleQueue>>>,
/// Factory for creating ACLDB instances
acldb_factory: Arc<ACLDBFactory>,
}
impl Server {
/// Creates a new server
pub fn new(config: ServerConfig) -> Self {
let rpc = Arc::new(RpcInterface::new());
let queues = Arc::new(RwLock::new(HashMap::new()));
let acldb_factory = Arc::new(ACLDBFactory::new());
Server {
config,
rpc,
queues,
acldb_factory,
}
}
/// Starts the server
pub async fn start(&self) -> std::io::Result<()> {
let server = self.clone();
// Register RPC handlers
self.register_handlers();
info!("Starting ACLDB server on {}:{}", self.config.host, self.config.port);
info!("API documentation available at: http://{}:{}/redoc", self.config.host, self.config.port);
// Start the HTTP server
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(server.clone()))
.wrap(Logger::default())
.wrap(
Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
)
.route("/rpc", web::post().to(handle_rpc))
.route("/health", web::get().to(health_check))
.service(
Redoc::with_url("/redoc", serde_json::to_value(ApiDoc::openapi()).unwrap())
)
})
.bind(format!("{0}:{1}", self.config.host, self.config.port))?
.run()
.await
}
/// Registers RPC handlers
fn register_handlers(&self) {
// Nothing to do here - handlers are now processed dynamically
}
/// Adds a request to the queue for a circle
async fn add_to_queue(&self, circle_id: &str, request: RpcRequest) -> mpsc::Receiver<RpcResponse> {
let (response_sender, response_receiver) = mpsc::channel(1);
// Get or create the queue for this circle
let mut queues = self.queues.write().await;
if !queues.contains_key(circle_id) {
queues.insert(circle_id.to_string(), CircleQueue::new());
}
// Get a mutable reference to the queue
if let Some(queue) = queues.get_mut(circle_id) {
// Add the request to the queue
queue.add_request(
request,
response_sender,
Arc::clone(&self.rpc),
Arc::clone(&self.acldb_factory)
).await;
}
response_receiver
}
}
/// Extracts the circle ID from an RPC request
fn extract_circle_id(request: &web::Json<RpcRequest>) -> Result<String, Error> {
// Extract from different parameter types based on the method
match request.method.as_str() {
"aclupdate" => {
let params: AclUpdateParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"aclremove" => {
let params: AclRemoveParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"acldel" => {
let params: AclDelParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"set" => {
let params: SetParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"del" => {
let params: DelParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"get" => {
let params: GetParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"prefix" => {
let params: PrefixParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
_ => Err(Error::InvalidRequest(format!("Unknown method: {}", request.method))),
}
}
/// API documentation schema
#[derive(OpenApi)]
#[openapi(
paths(
handle_rpc,
health_check
),
components(
schemas(RpcRequest, RpcResponse, AclUpdateParams, AclRemoveParams, AclDelParams, SetParams, DelParams, GetParams, PrefixParams)
),
tags(
(name = "acldb", description = "ACLDB API - Access Control Database"),
(name = "acl", description = "Access Control List Management"),
(name = "data", description = "Data Operations")
),
info(
title = "ACLDB API",
version = "1.0.0",
description = "API for managing access control lists and data operations in HeroDB. This API provides functionality for ACL management and data operations with access control.",
contact(
name = "HeroDB Team",
url = "https://ourworld.tf"
)
)
)]
struct ApiDoc;
/// Handler for RPC requests
///
/// This endpoint handles all RPC requests to the ACLDB API.
/// Supported methods include:
/// - `acl_update`: Update or create an ACL with specified permissions
/// - `acl_remove`: Remove specific public keys from an existing ACL
/// - `acl_del`: Delete an entire ACL
/// - `set`: Store data with optional ACL protection
/// - `get`: Retrieve data with ACL verification
/// - `del`: Delete data with ACL verification
/// - `prefix`: Search for keys with a specific prefix
#[utoipa::path(
post,
path = "/rpc",
request_body = RpcRequest,
responses(
(status = 200, description = "RPC request processed successfully", body = RpcResponse),
(status = 400, description = "Invalid request parameters", body = RpcResponse),
(status = 401, description = "Permission denied", body = RpcResponse),
(status = 404, description = "Resource not found", body = RpcResponse),
(status = 500, description = "Server error", body = RpcResponse)
),
tag = "acldb"
)]
async fn handle_rpc(
server: web::Data<Server>,
request: web::Json<RpcRequest>,
) -> impl Responder {
// Extract the circle ID from the request
let circle_id = match extract_circle_id(&request) {
Ok(id) => id,
Err(err) => {
return HttpResponse::BadRequest().json(RpcResponse {
result: None,
error: Some(err.to_string()),
});
}
};
// Add the request to the queue for this circle
let mut response_receiver = server.add_to_queue(&circle_id, request.0.clone()).await;
// Wait for the response
match response_receiver.recv().await {
Some(response) => HttpResponse::Ok().json(response),
None => HttpResponse::InternalServerError().json(RpcResponse {
result: None,
error: Some("Failed to get response".to_string()),
}),
}
}
/// Process an RPC request
async fn process_request(
request: &RpcRequest,
_rpc_interface: &Arc<RpcInterface>,
acldb_factory: &Arc<ACLDBFactory>
) -> RpcResponse {
match request.method.as_str() {
"aclupdate" => {
match serde_json::from_value::<AclUpdateParams>(request.params.clone()) {
Ok(params) => {
match RpcInterface::parse_acl_right(&params.right) {
Ok(right) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_update(&params.caller_pubkey, &params.name, &params.pubkeys, right).await {
Ok(_) => RpcResponse {
result: Some(json!({"success": true})),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(format!("Invalid parameters: {}", err)),
},
}
},
"aclremove" => {
match serde_json::from_value::<AclRemoveParams>(request.params.clone()) {
Ok(params) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_remove(&params.caller_pubkey, &params.name, &params.pubkeys).await {
Ok(_) => RpcResponse {
result: Some(json!({"success": true})),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(format!("Invalid parameters: {}", err)),
},
}
},
"acldel" => {
match serde_json::from_value::<AclDelParams>(request.params.clone()) {
Ok(params) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_del(&params.caller_pubkey, &params.name).await {
Ok(_) => RpcResponse {
result: Some(json!({"success": true})),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(format!("Invalid parameters: {}", err)),
},
}
},
"set" => {
match serde_json::from_value::<SetParams>(request.params.clone()) {
Ok(params) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
let topic = db.topic(&params.topic);
match base64_decode(&params.value) {
Ok(value) => {
let acl_id = params.acl_id.unwrap_or(0);
let result = if let Some(key) = params.key {
let topic = topic.write().await;
topic.set_with_acl(&key, &value, acl_id).await
} else if let Some(id) = params.id {
let topic = topic.write().await;
topic.set_with_acl(&id.to_string(), &value, acl_id).await
} else {
// Return a future that resolves to an error for consistency
future::ready(Err(Error::InvalidRequest("Either key or id must be provided".to_string()))).await
};
match result {
Ok(id) => RpcResponse {
result: Some(json!({"id": id})),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(format!("Invalid parameters: {}", err)),
},
}
},
_ => RpcResponse {
result: None,
error: Some(format!("Unknown method: {}", request.method)),
},
}
}
/// Handler for health check
///
/// This endpoint provides a simple health check to verify the server is running.
#[utoipa::path(
get,
path = "/health",
responses(
(status = 200, description = "Server is healthy", body = String)
),
tag = "acldb"
)]
async fn health_check() -> impl Responder {
HttpResponse::Ok().json(json!({"status": "ok"}))
}
#[cfg(test)]
mod tests {
// Tests will be added here
}

344
_archive/acldb/src/topic.rs Normal file
View File

@@ -0,0 +1,344 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use ourdb::{OurDB, OurDBSetArgs};
use tst::TST;
use crate::error::Error;
use crate::acl::{ACL, ACLRight};
/// ACLDBTopic represents a database instance for a specific topic within a circle
pub struct ACLDBTopic {
/// Circle ID
circle_id: String,
/// Topic name
topic: String,
/// OurDB instance
db: Arc<RwLock<OurDB>>,
/// TST instance for key-to-id mapping
tst: Arc<RwLock<TST>>,
}
impl ACLDBTopic {
/// Creates a new ACLDBTopic instance
pub fn new(circle_id: String, topic: String, db: Arc<RwLock<OurDB>>, tst: Arc<RwLock<TST>>) -> Self {
ACLDBTopic {
circle_id,
topic,
db,
tst,
}
}
/// Sets a value in the database with optional ACL protection
pub async fn set(&self, key: &str, value: &[u8]) -> Result<u32, Error> {
self.set_with_acl(key, value, 0).await // 0 means no ACL protection
}
/// Sets a value in the database with ACL protection
pub async fn set_with_acl(&self, key: &str, value: &[u8], acl_id: u32) -> Result<u32, Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Check if the key already exists in TST
let id = {
// First try to get the ID from TST
let mut id_opt = None;
{
let mut tst = self.tst.write().await;
if let Ok(id_bytes) = tst.list(&tst_key) {
if !id_bytes.is_empty() {
let id_str = String::from_utf8_lossy(&id_bytes[0].as_bytes());
if let Ok(parsed_id) = id_str.parse::<u32>() {
id_opt = Some(parsed_id);
}
}
}
}
// If not found, get a new ID
match id_opt {
Some(id) => id,
None => {
let mut db = self.db.write().await;
db.get_next_id()?
}
}
};
// Prepare the data with ACL ID prefix if needed
let data = if acl_id > 0 {
// Add ACL ID as the first 4 bytes
let mut acl_data = acl_id.to_be_bytes().to_vec();
acl_data.extend_from_slice(value);
acl_data
} else {
value.to_vec()
};
// Store the data in OurDB
{
let mut db = self.db.write().await;
db.set(OurDBSetArgs {
id: Some(id),
data: &data,
})?;
}
// Store the ID in TST
{
let mut tst = self.tst.write().await;
tst.set(&tst_key, id.to_string().into_bytes())?;
}
Ok(id)
}
/// Gets a value from the database
pub async fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Get the ID from TST
let id = {
let mut tst = self.tst.write().await;
let keys = tst.list(&tst_key)?;
if keys.is_empty() {
return Err(Error::NotFound);
}
let id_str = &keys[0];
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
};
// Get the data from OurDB
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (acl_id_bytes, actual_data) = data.split_at(4);
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
if acl_id > 0 {
// This record is ACL-protected, but we're not checking permissions here
// The permission check should be done at a higher level
return Ok(actual_data.to_vec());
}
}
Ok(data)
}
/// Gets a value from the database with permission check
pub async fn get_with_permission(&self, key: &str, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<Vec<u8>, Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Get the ID from TST
let id = {
let mut tst = self.tst.write().await;
let keys = tst.list(&tst_key)?;
if keys.is_empty() {
return Err(Error::NotFound);
}
let id_str = &keys[0];
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
};
// Get the data from OurDB
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (acl_id_bytes, actual_data) = data.split_at(4);
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
if acl_id > 0 {
// This record is ACL-protected, check permissions
let acl_name = format!("acl_{}", acl_id);
// Find the ACL in the parent ACLs
let has_permission = parent_acls.iter()
.find(|acl| acl.name == acl_name)
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Read))
.unwrap_or(false);
if !has_permission {
return Err(Error::PermissionDenied);
}
return Ok(actual_data.to_vec());
}
}
Ok(data)
}
/// Gets a value by ID from the database
pub async fn get_by_id(&self, id: u32) -> Result<Vec<u8>, Error> {
// Get the data from OurDB
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (_, actual_data) = data.split_at(4);
return Ok(actual_data.to_vec());
}
Ok(data)
}
/// Gets a value by ID from the database with permission check
pub async fn get_by_id_with_permission(&self, id: u32, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<Vec<u8>, Error> {
// Get the data from OurDB
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (acl_id_bytes, actual_data) = data.split_at(4);
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
if acl_id > 0 {
// This record is ACL-protected, check permissions
let acl_name = format!("acl_{}", acl_id);
// Find the ACL in the parent ACLs
let has_permission = parent_acls.iter()
.find(|acl| acl.name == acl_name)
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Read))
.unwrap_or(false);
if !has_permission {
return Err(Error::PermissionDenied);
}
return Ok(actual_data.to_vec());
}
}
Ok(data)
}
/// Deletes a value from the database
pub async fn delete(&self, key: &str) -> Result<(), Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Get the ID from TST
let id = {
let mut tst = self.tst.write().await;
let keys = tst.list(&tst_key)?;
if keys.is_empty() {
return Err(Error::NotFound);
}
let id_str = &keys[0];
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
};
// Delete from OurDB
{
let mut db = self.db.write().await;
db.delete(id)?;
}
// Delete from TST
{
let mut tst = self.tst.write().await;
tst.delete(&tst_key)?;
}
Ok(())
}
/// Deletes a value from the database with permission check
pub async fn delete_with_permission(&self, key: &str, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<(), Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Get the ID from TST
let id = {
let mut tst = self.tst.write().await;
let keys = tst.list(&tst_key)?;
if keys.is_empty() {
return Err(Error::NotFound);
}
let id_str = &keys[0];
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
};
// Get the data to check ACL
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (acl_id_bytes, _) = data.split_at(4);
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
if acl_id > 0 {
// This record is ACL-protected, check permissions
let acl_name = format!("acl_{}", acl_id);
// Find the ACL in the parent ACLs
let has_permission = parent_acls.iter()
.find(|acl| acl.name == acl_name)
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Delete))
.unwrap_or(false);
if !has_permission {
return Err(Error::PermissionDenied);
}
}
}
// Delete from OurDB
{
let mut db = self.db.write().await;
db.delete(id)?
};
// Delete from TST
{
let mut tst = self.tst.write().await;
tst.delete(&tst_key)?
};
Ok(())
}
/// Gets all keys with a given prefix
pub async fn prefix(&self, prefix: &str) -> Result<Vec<String>, Error> {
// Create the TST prefix
let tst_prefix = format!("{}{}", self.topic, prefix);
// Get all keys with the prefix
let keys = {
let mut tst = self.tst.write().await;
tst.list(&tst_prefix)?
};
// Remove the topic prefix from the keys
let topic_prefix = format!("{}", self.topic);
let keys = keys.into_iter()
.map(|key| key.strip_prefix(&topic_prefix).unwrap_or(&key).to_string())
.collect();
Ok(keys)
}
}
#[cfg(test)]
mod tests {
// Tests will be added here
}

View File

@@ -0,0 +1,52 @@
use crate::error::Error;
/// Decodes a base64 string to bytes
pub fn base64_decode(input: &str) -> Result<Vec<u8>, Error> {
base64::decode(input).map_err(|e| Error::InvalidRequest(format!("Invalid base64: {}", e)))
}
/// Encodes bytes to a base64 string
pub fn base64_encode(input: &[u8]) -> String {
base64::encode(input)
}
/// Generates a SHA-256 hash of the input
pub fn sha256_hash(input: &[u8]) -> Vec<u8> {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(input);
hasher.finalize().to_vec()
}
/// Converts a hash to a hex string
pub fn to_hex(bytes: &[u8]) -> String {
hex::encode(bytes)
}
/// Validates a signature against a message and public key
pub fn validate_signature(_message: &[u8], _signature: &str, _pubkey: &str) -> Result<bool, Error> {
// In a real implementation, this would validate the cryptographic signature
// For now, we'll just return true
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base64() {
let original = b"Hello, world!";
let encoded = base64_encode(original);
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_sha256() {
let input = b"test";
let hash = sha256_hash(input);
let hex = to_hex(&hash);
assert_eq!(hex, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
}
}

View File

@@ -0,0 +1,304 @@
{
"openapi": "3.0.0",
"info": {
"title": "ACLDB API",
"description": "API for the ACLDB module which implements an Access Control List layer on top of the existing ourdb and tst databases.",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Local development server"
}
],
"paths": {
"/rpc": {
"post": {
"summary": "RPC endpoint",
"description": "Handles all RPC requests to the ACLDB system",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RpcRequest"
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RpcResponse"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/health": {
"get": {
"summary": "Health check",
"description": "Returns the health status of the server",
"responses": {
"200": {
"description": "Server is healthy",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "ok"
}
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"RpcRequest": {
"type": "object",
"required": ["method", "params", "signature"],
"properties": {
"method": {
"type": "string",
"description": "The name of the method to call",
"example": "set"
},
"params": {
"type": "object",
"description": "The parameters for the method"
},
"signature": {
"type": "string",
"description": "Cryptographic signature of the request"
}
}
},
"RpcResponse": {
"type": "object",
"properties": {
"result": {
"type": "object",
"description": "The result of the method call if successful"
},
"error": {
"type": "string",
"description": "Error message if the method call failed"
}
}
},
"ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string",
"description": "Error message"
}
}
},
"AclUpdateParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "name", "pubkeys", "right"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the ACL exists"
},
"name": {
"type": "string",
"description": "Unique name for the ACL within the circle"
},
"pubkeys": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of public keys to grant permissions to"
},
"right": {
"type": "string",
"description": "Permission level (read/write/delete/execute/admin)",
"enum": ["read", "write", "delete", "execute", "admin"]
}
}
},
"AclRemoveParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "name", "pubkeys"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the ACL exists"
},
"name": {
"type": "string",
"description": "Name of the ACL to modify"
},
"pubkeys": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of public keys to remove from the ACL"
}
}
},
"AclDelParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "name"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the ACL exists"
},
"name": {
"type": "string",
"description": "Name of the ACL to delete"
}
}
},
"SetParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "topic", "value"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the data belongs"
},
"topic": {
"type": "string",
"description": "String identifier for the database category"
},
"key": {
"type": "string",
"description": "Optional string key for the record"
},
"id": {
"type": "integer",
"description": "Optional numeric ID for direct access"
},
"value": {
"type": "string",
"description": "Base64-encoded data to store"
},
"acl_id": {
"type": "integer",
"description": "ID of the ACL to protect this record (0 for public access)"
}
}
},
"DelParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "topic"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the data belongs"
},
"topic": {
"type": "string",
"description": "String identifier for the database category"
},
"key": {
"type": "string",
"description": "Optional string key for the record"
},
"id": {
"type": "integer",
"description": "Optional numeric ID for direct access"
}
}
},
"GetParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "topic"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the data belongs"
},
"topic": {
"type": "string",
"description": "String identifier for the database category"
},
"key": {
"type": "string",
"description": "Optional string key for the record"
},
"id": {
"type": "integer",
"description": "Optional numeric ID for direct access"
}
}
},
"PrefixParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "topic", "prefix"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the data belongs"
},
"topic": {
"type": "string",
"description": "String identifier for the database category"
},
"prefix": {
"type": "string",
"description": "Prefix to search for"
}
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ACLDB API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" />
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@4.5.0/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@4.5.0/favicon-16x16.png" sizes="16x16" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "/openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
window.ui = ui;
};
</script>
</body>
</html>