feat(cubesys/cubecrypt): R4 challenge-response auth + R5/R6 wiring
- cubecrypt/src/auth.rs: HMAC-SHA256 signed HELLO (sign_hello/verify_hello), random_nonce_hex entropy source (plan R4) - cube-server: --auth-key enables CHALLENGE/HELLO handshake; auth_handshake is a module-level free fn (run(self) consumes self, so the thread closure can only reach the captured psk). Resolves the earlier E0425 compile failure - cubec.rs client: --auth-key builds a signed HELLO frame - commands.rs: audit op constants + read-gating hooks wired into admit_read - audit.rs: per-tenant append-only audit log in a Null-cube range (plan R6) - clippy -D warnings clean; full ./check gate ALL CHECKS PASSED (61 tests)
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
//! survives restarts. Each `exec` takes and returns an `Arc<ConcurrentStore>`
|
||||
//! so the server can hand a cloned handle to each worker thread.
|
||||
|
||||
use crate::audit::{Audit, OP_DELETE, OP_GRANT, OP_OPEN, OP_READ, OP_REVOKE, OP_SEAL, OP_WRITE};
|
||||
use crate::grants::{grant, grant_allows, perms_from_str, revoke, Owner, Perm};
|
||||
use crate::store::ConcurrentStore;
|
||||
use crate::tenant::{TenantIdentity, TenantSession};
|
||||
@@ -72,6 +73,11 @@ pub struct Session {
|
||||
/// behaviour, so pre-existing `cubec`/stress.sh flows keep working until
|
||||
/// the operator opts in via the daemon's `--require-identity` policy).
|
||||
enforce_owner: bool,
|
||||
/// Audit trail (plan R6). `Some` when the daemon enabled it on this
|
||||
/// connection; every op is then appended (permitted or denied) so an
|
||||
/// operator can later review who touched what. `None` keeps the library
|
||||
/// REPL/tests silent (no audit record churn) unless explicitly enabled.
|
||||
audit: Option<Audit>,
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
@@ -90,6 +96,7 @@ impl Session {
|
||||
identity: None,
|
||||
txn: None,
|
||||
enforce_owner: false,
|
||||
audit: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +109,7 @@ impl Session {
|
||||
identity: None,
|
||||
txn: None,
|
||||
enforce_owner: false,
|
||||
audit: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +126,25 @@ impl Session {
|
||||
identity: ts.identity(),
|
||||
txn: None,
|
||||
enforce_owner: false,
|
||||
audit: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable the audit trail for this session (plan R6). Called by the daemon
|
||||
/// once per connection after construction.
|
||||
pub fn enable_audit(&mut self) {
|
||||
self.audit = Some(Audit::new(self.store.clone()));
|
||||
}
|
||||
|
||||
/// Append an audit entry if auditing is enabled. No-op otherwise.
|
||||
fn audit_now(&self, op: u8, coord: Czyx, ok: bool) {
|
||||
if let Some(a) = &self.audit {
|
||||
let owner = self
|
||||
.identity
|
||||
.as_ref()
|
||||
.map(|i| i.owner_local.as_str())
|
||||
.unwrap_or("<anonymous>");
|
||||
a.append(op, coord, owner, ok);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,6 +399,23 @@ impl Session {
|
||||
Ok("ok: transaction rolled back".to_string())
|
||||
}
|
||||
"stats" => Ok(self.stats()),
|
||||
"audit" => {
|
||||
// Plan R6: dump the append-only audit trail for this session's
|
||||
// store. Each line is a JSON object; `ok:false` rows are denied
|
||||
// attempts (the interesting ones for intrusion review).
|
||||
let log = match &self.audit {
|
||||
Some(a) => a.dump(),
|
||||
None => return Err(
|
||||
"audit: audit trail is not enabled on this session (daemon --enable-audit)"
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
if log.trim().is_empty() {
|
||||
Ok("audit -> (no entries)".to_string())
|
||||
} else {
|
||||
Ok(format!("audit ->\n{log}"))
|
||||
}
|
||||
}
|
||||
"query" => {
|
||||
let dt = it
|
||||
.next()
|
||||
@@ -412,8 +456,10 @@ impl Session {
|
||||
let coord = scratch_code_coord(path, Kind::Fn, name, &ops)?;
|
||||
// Task 6: reject overwriting a record owned by a different owner.
|
||||
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
|
||||
self.audit_now(OP_WRITE, coord, false);
|
||||
return Err(msg);
|
||||
}
|
||||
self.audit_now(OP_WRITE, coord, true);
|
||||
let owner = self.identity.as_ref().map(|i| i.owner_local.as_str());
|
||||
let value = {
|
||||
let mut scratch = CubeStore::new(HashBackend::new());
|
||||
@@ -452,8 +498,10 @@ impl Session {
|
||||
let name = path.rsplit('/').next().unwrap_or(path);
|
||||
let coord = scratch_code_coord(path, Kind::Fn, name, &code)?;
|
||||
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
|
||||
self.audit_now(OP_WRITE, coord, false);
|
||||
return Err(msg);
|
||||
}
|
||||
self.audit_now(OP_WRITE, coord, true);
|
||||
let owner = self.identity.as_ref().map(|i| i.owner_local.as_str());
|
||||
let value = {
|
||||
let mut scratch = CubeStore::new(HashBackend::new());
|
||||
@@ -483,8 +531,10 @@ impl Session {
|
||||
// Task 6: reject deleting a record owned by a different owner
|
||||
// (unless unowned, which any identity may take over).
|
||||
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
|
||||
self.audit_now(OP_DELETE, coord, false);
|
||||
return Err(msg);
|
||||
}
|
||||
self.audit_now(OP_DELETE, coord, true);
|
||||
if let Some(txn) = self.txn.as_mut() {
|
||||
txn.ops.push(TxnOp { coord, put: None });
|
||||
return Ok(format!("buffered del {path} — commit to apply"));
|
||||
@@ -537,6 +587,7 @@ impl Session {
|
||||
};
|
||||
let seq = grant(&self.store, &granter, &grantee, perms, scope)
|
||||
.map_err(|e| format!("grant: {e}"))?;
|
||||
self.audit_now(OP_GRANT, scope.unwrap_or(Czyx::new(0, 0, 0, 0)), true);
|
||||
let scope_s = match scope {
|
||||
Some(c) => c.pack_u32().to_string(),
|
||||
None => "global".to_string(),
|
||||
@@ -582,6 +633,7 @@ impl Session {
|
||||
remote: gr,
|
||||
};
|
||||
let removed = revoke(&self.store, &granter, &grantee, scope);
|
||||
self.audit_now(OP_REVOKE, scope.unwrap_or(Czyx::new(0, 0, 0, 0)), true);
|
||||
Ok(format!("ok: revoked {removed} grant(s)"))
|
||||
}
|
||||
"run" => {
|
||||
@@ -590,8 +642,10 @@ impl Session {
|
||||
// R5: reads (this one also *executes*) honor the owner/grantee
|
||||
// read gate, same as writes honor the mutate gate.
|
||||
if let Some(msg) = self.admit_read(_coord) {
|
||||
self.audit_now(OP_READ, _coord, false);
|
||||
return Err(msg);
|
||||
}
|
||||
self.audit_now(OP_READ, _coord, true);
|
||||
let sn = txn_snapshot(self);
|
||||
let cell = crate::load_code_cell(&sn, path).map_err(|e| e.to_string())?;
|
||||
let mut vm = Vm::new(sn);
|
||||
@@ -622,8 +676,10 @@ impl Session {
|
||||
// R5: metadata reads honor the read gate.
|
||||
let coord = crate::path_to_czyx(path).map_err(|e| e.to_string())?;
|
||||
if let Some(msg) = self.admit_read(coord) {
|
||||
self.audit_now(OP_READ, coord, false);
|
||||
return Err(msg);
|
||||
}
|
||||
self.audit_now(OP_READ, coord, true);
|
||||
let sn = txn_snapshot(self);
|
||||
let fs = cubefs::CubeFs::new(sn);
|
||||
let a = fs
|
||||
@@ -659,6 +715,8 @@ impl Session {
|
||||
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
|
||||
return Err(msg);
|
||||
}
|
||||
// Audit the destructive op that is about to run (seal vs open).
|
||||
self.audit_now(if cmd == "seal" { OP_SEAL } else { OP_OPEN }, coord, true);
|
||||
|
||||
if store.get_record(&kc).is_none() {
|
||||
store.put_raw(kc, b"demo-key-material-32-bytes-long!!".to_vec());
|
||||
|
||||
Reference in New Issue
Block a user