Add SSH cert flows and user service installer
This commit is contained in:
parent
26f81ff1ef
commit
f302342b1c
21 changed files with 2158 additions and 14 deletions
|
|
@ -118,6 +118,34 @@ impl Store {
|
|||
state_json TEXT NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ssh_cert_requests (
|
||||
request_id TEXT PRIMARY KEY,
|
||||
requester_node TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
public_key_fingerprint TEXT NOT NULL,
|
||||
cert_kind TEXT NOT NULL,
|
||||
principals_json TEXT NOT NULL,
|
||||
requested_validity TEXT,
|
||||
renewal_of TEXT,
|
||||
reason TEXT,
|
||||
status TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ssh_certificates (
|
||||
cert_id TEXT PRIMARY KEY,
|
||||
request_id TEXT NOT NULL,
|
||||
certificate TEXT NOT NULL,
|
||||
certificate_fingerprint TEXT NOT NULL,
|
||||
imported_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ssh_revocations (
|
||||
revocation_id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
published INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', '1');
|
||||
"#,
|
||||
)?;
|
||||
|
|
@ -200,6 +228,170 @@ impl Store {
|
|||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn insert_ssh_cert_request(
|
||||
&self,
|
||||
request: &StoredSshCertRequest,
|
||||
) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO ssh_cert_requests(
|
||||
request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
||||
principals_json, requested_validity, renewal_of, reason, status, created_at_ms
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"#,
|
||||
params![
|
||||
request.request_id,
|
||||
request.requester_node,
|
||||
request.public_key,
|
||||
request.public_key_fingerprint,
|
||||
request.cert_kind,
|
||||
serde_json::to_string(&request.principals)?,
|
||||
request.requested_validity,
|
||||
request.renewal_of,
|
||||
request.reason,
|
||||
request.status,
|
||||
request.created_at_ms
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_ssh_cert_request(
|
||||
&self,
|
||||
request_id: &str,
|
||||
) -> Result<Option<StoredSshCertRequest>, StoreError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
r#"SELECT request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
||||
principals_json, requested_validity, renewal_of, reason, status, created_at_ms
|
||||
FROM ssh_cert_requests WHERE request_id = ?1"#,
|
||||
)?;
|
||||
let mut rows = stmt.query(params![request_id])?;
|
||||
if let Some(row) = rows.next()? {
|
||||
Ok(Some(stored_ssh_cert_request_from_row(row)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ssh_cert_request_status(
|
||||
&self,
|
||||
request_id: &str,
|
||||
status: &str,
|
||||
) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
"UPDATE ssh_cert_requests SET status = ?2 WHERE request_id = ?1",
|
||||
params![request_id, status],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_ssh_cert_requests(&self) -> Result<Vec<StoredSshCertRequest>, StoreError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
r#"SELECT request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
||||
principals_json, requested_validity, renewal_of, reason, status, created_at_ms
|
||||
FROM ssh_cert_requests ORDER BY created_at_ms, request_id"#,
|
||||
)?;
|
||||
let rows = stmt.query_map([], stored_ssh_cert_request_from_row)?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn insert_ssh_certificate(
|
||||
&self,
|
||||
certificate: &StoredSshCertificate,
|
||||
) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO ssh_certificates(
|
||||
cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5)"#,
|
||||
params![
|
||||
certificate.cert_id,
|
||||
certificate.request_id,
|
||||
certificate.certificate,
|
||||
certificate.certificate_fingerprint,
|
||||
certificate.imported_at_ms
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_ssh_certificates(&self) -> Result<Vec<StoredSshCertificate>, StoreError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
r#"SELECT cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
|
||||
FROM ssh_certificates ORDER BY imported_at_ms, cert_id"#,
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(StoredSshCertificate {
|
||||
cert_id: row.get(0)?,
|
||||
request_id: row.get(1)?,
|
||||
certificate: row.get(2)?,
|
||||
certificate_fingerprint: row.get(3)?,
|
||||
imported_at_ms: row.get(4)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn insert_ssh_revocation(
|
||||
&self,
|
||||
revocation: &StoredSshRevocation,
|
||||
) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO ssh_revocations(
|
||||
revocation_id, kind, target, reason, created_at_ms, published
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
|
||||
params![
|
||||
revocation.revocation_id,
|
||||
revocation.kind,
|
||||
revocation.target,
|
||||
revocation.reason,
|
||||
revocation.created_at_ms,
|
||||
if revocation.published { 1_i64 } else { 0_i64 }
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_ssh_revocations(&self) -> Result<Vec<StoredSshRevocation>, StoreError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
r#"SELECT revocation_id, kind, target, reason, created_at_ms, published
|
||||
FROM ssh_revocations ORDER BY created_at_ms, revocation_id"#,
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(StoredSshRevocation {
|
||||
revocation_id: row.get(0)?,
|
||||
kind: row.get(1)?,
|
||||
target: row.get(2)?,
|
||||
reason: row.get(3)?,
|
||||
created_at_ms: row.get(4)?,
|
||||
published: row.get::<_, i64>(5)? != 0,
|
||||
})
|
||||
})?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(StoreError::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_ssh_cert_request_from_row(
|
||||
row: &rusqlite::Row<'_>,
|
||||
) -> Result<StoredSshCertRequest, rusqlite::Error> {
|
||||
let principals_json: String = row.get(5)?;
|
||||
let principals = serde_json::from_str(&principals_json).map_err(|error| {
|
||||
rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(error))
|
||||
})?;
|
||||
Ok(StoredSshCertRequest {
|
||||
request_id: row.get(0)?,
|
||||
requester_node: row.get(1)?,
|
||||
public_key: row.get(2)?,
|
||||
public_key_fingerprint: row.get(3)?,
|
||||
cert_kind: row.get(4)?,
|
||||
principals,
|
||||
requested_validity: row.get(6)?,
|
||||
renewal_of: row.get(7)?,
|
||||
reason: row.get(8)?,
|
||||
status: row.get(9)?,
|
||||
created_at_ms: row.get(10)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -217,6 +409,40 @@ pub struct CasObject {
|
|||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredSshCertRequest {
|
||||
pub request_id: String,
|
||||
pub requester_node: String,
|
||||
pub public_key: String,
|
||||
pub public_key_fingerprint: String,
|
||||
pub cert_kind: String,
|
||||
pub principals: Vec<String>,
|
||||
pub requested_validity: Option<String>,
|
||||
pub renewal_of: Option<String>,
|
||||
pub reason: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredSshCertificate {
|
||||
pub cert_id: String,
|
||||
pub request_id: String,
|
||||
pub certificate: String,
|
||||
pub certificate_fingerprint: String,
|
||||
pub imported_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredSshRevocation {
|
||||
pub revocation_id: String,
|
||||
pub kind: String,
|
||||
pub target: String,
|
||||
pub reason: Option<String>,
|
||||
pub created_at_ms: i64,
|
||||
pub published: bool,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn now_ms() -> i64 {
|
||||
let now = std::time::SystemTime::now()
|
||||
|
|
@ -237,4 +463,47 @@ mod tests {
|
|||
let resources = store.list_resources().expect("resources");
|
||||
assert!(resources.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_cert_request_and_revocation_roundtrip() {
|
||||
let store = Store::open_memory().expect("open");
|
||||
let request = StoredSshCertRequest {
|
||||
request_id: "ssh-cert-request:1".to_owned(),
|
||||
requester_node: "node:laptop".to_owned(),
|
||||
public_key: "ssh-ed25519 AAAA test".to_owned(),
|
||||
public_key_fingerprint: "ssh:blake3:test".to_owned(),
|
||||
cert_kind: "user".to_owned(),
|
||||
principals: vec!["eric".to_owned()],
|
||||
requested_validity: Some("+52w".to_owned()),
|
||||
renewal_of: None,
|
||||
reason: Some("renewal".to_owned()),
|
||||
status: "pending".to_owned(),
|
||||
created_at_ms: 1,
|
||||
};
|
||||
store
|
||||
.insert_ssh_cert_request(&request)
|
||||
.expect("insert request");
|
||||
assert_eq!(
|
||||
store
|
||||
.get_ssh_cert_request("ssh-cert-request:1")
|
||||
.expect("get request"),
|
||||
Some(request.clone())
|
||||
);
|
||||
|
||||
let revocation = StoredSshRevocation {
|
||||
revocation_id: "ssh-revocation:1".to_owned(),
|
||||
kind: "public-key".to_owned(),
|
||||
target: "ssh:blake3:test".to_owned(),
|
||||
reason: Some("lost key".to_owned()),
|
||||
created_at_ms: 2,
|
||||
published: true,
|
||||
};
|
||||
store
|
||||
.insert_ssh_revocation(&revocation)
|
||||
.expect("insert revocation");
|
||||
assert_eq!(
|
||||
store.list_ssh_revocations().expect("list revocations"),
|
||||
vec![revocation]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue