merge branches and cleanup db
This commit is contained in:
1916
_archive/acldb/Cargo.lock
generated
Normal file
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
26
_archive/acldb/Cargo.toml
Normal 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
154
_archive/acldb/README.md
Normal 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
97
_archive/acldb/src/acl.rs
Normal 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));
|
||||
}
|
||||
}
|
47
_archive/acldb/src/error.rs
Normal file
47
_archive/acldb/src/error.rs
Normal 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
271
_archive/acldb/src/lib.rs
Normal 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
|
||||
}
|
43
_archive/acldb/src/main.rs
Normal file
43
_archive/acldb/src/main.rs
Normal 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
258
_archive/acldb/src/rpc.rs
Normal 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());
|
||||
}
|
||||
}
|
520
_archive/acldb/src/server.rs
Normal file
520
_archive/acldb/src/server.rs
Normal 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(¶ms.right) {
|
||||
Ok(right) => {
|
||||
match acldb_factory.get_or_create(¶ms.circle_id).await {
|
||||
Ok(db) => {
|
||||
let mut db = db.write().await;
|
||||
match db.acl_update(¶ms.caller_pubkey, ¶ms.name, ¶ms.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(¶ms.circle_id).await {
|
||||
Ok(db) => {
|
||||
let mut db = db.write().await;
|
||||
match db.acl_remove(¶ms.caller_pubkey, ¶ms.name, ¶ms.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(¶ms.circle_id).await {
|
||||
Ok(db) => {
|
||||
let mut db = db.write().await;
|
||||
match db.acl_del(¶ms.caller_pubkey, ¶ms.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(¶ms.circle_id).await {
|
||||
Ok(db) => {
|
||||
let mut db = db.write().await;
|
||||
let topic = db.topic(¶ms.topic);
|
||||
|
||||
match base64_decode(¶ms.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
344
_archive/acldb/src/topic.rs
Normal 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
|
||||
}
|
52
_archive/acldb/src/utils.rs
Normal file
52
_archive/acldb/src/utils.rs
Normal 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");
|
||||
}
|
||||
}
|
304
_archive/acldb/static/openapi.json
Normal file
304
_archive/acldb/static/openapi.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
_archive/acldb/static/swagger-ui.html
Normal file
54
_archive/acldb/static/swagger-ui.html
Normal 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>
|
Reference in New Issue
Block a user