Public release release-a513a752ee52
Source commit: a513a752ee528b3c50400dde5d86e24c694e35b5 Public tree identity: sha256:ae10e20386250d9dac200a07ef69e1944402e8cb0f9c37a249587595638b3be0
This commit is contained in:
commit
b9d3e7681a
221 changed files with 81138 additions and 0 deletions
194
crates/clusterflux-coordinator/src/sessions.rs
Normal file
194
crates/clusterflux-coordinator/src/sessions.rs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use clusterflux_core::{Actor, AuthContext, CredentialKind, Digest, ProjectId, TenantId, UserId};
|
||||
|
||||
use crate::{CliSessionRecord, Coordinator, CoordinatorError, CredentialRecord};
|
||||
|
||||
impl Coordinator {
|
||||
pub fn issue_cli_session(
|
||||
&mut self,
|
||||
tenant: TenantId,
|
||||
project: ProjectId,
|
||||
user: UserId,
|
||||
session_secret: &str,
|
||||
expires_at_epoch_seconds: Option<u64>,
|
||||
) -> CliSessionRecord {
|
||||
self.upsert_tenant(tenant.clone());
|
||||
self.upsert_user(
|
||||
tenant.clone(),
|
||||
user.clone(),
|
||||
CredentialKind::CliDeviceSession,
|
||||
);
|
||||
let session_digest = Digest::sha256(session_secret);
|
||||
let record = CliSessionRecord {
|
||||
session_digest: session_digest.clone(),
|
||||
tenant: tenant.clone(),
|
||||
project: project.clone(),
|
||||
user: user.clone(),
|
||||
credential_kind: CredentialKind::CliDeviceSession,
|
||||
expires_at_epoch_seconds,
|
||||
revoked: false,
|
||||
};
|
||||
self.durable
|
||||
.cli_sessions
|
||||
.insert(session_digest.clone(), record.clone());
|
||||
let credential_subject = format!("cli-session:{}", session_digest.as_str());
|
||||
self.durable.credentials.insert(
|
||||
credential_subject.clone(),
|
||||
CredentialRecord {
|
||||
subject: credential_subject,
|
||||
tenant,
|
||||
project: Some(project),
|
||||
kind: CredentialKind::CliDeviceSession,
|
||||
public_key_fingerprint: None,
|
||||
},
|
||||
);
|
||||
record
|
||||
}
|
||||
|
||||
pub fn authenticate_cli_session(
|
||||
&self,
|
||||
session_secret: &str,
|
||||
) -> Result<AuthContext, CoordinatorError> {
|
||||
self.authenticate_cli_session_at_with_tenant_policy(
|
||||
session_secret,
|
||||
unix_timestamp_seconds(),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authenticate_cli_session_for_status(
|
||||
&self,
|
||||
session_secret: &str,
|
||||
) -> Result<AuthContext, CoordinatorError> {
|
||||
self.authenticate_cli_session_at_with_tenant_policy(
|
||||
session_secret,
|
||||
unix_timestamp_seconds(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authenticate_cli_session_at(
|
||||
&self,
|
||||
session_secret: &str,
|
||||
now_epoch_seconds: u64,
|
||||
) -> Result<AuthContext, CoordinatorError> {
|
||||
self.authenticate_cli_session_at_with_tenant_policy(session_secret, now_epoch_seconds, true)
|
||||
}
|
||||
|
||||
fn authenticate_cli_session_at_with_tenant_policy(
|
||||
&self,
|
||||
session_secret: &str,
|
||||
now_epoch_seconds: u64,
|
||||
require_active_tenant: bool,
|
||||
) -> Result<AuthContext, CoordinatorError> {
|
||||
if session_secret.trim().is_empty() {
|
||||
return Err(CoordinatorError::Unauthorized(
|
||||
"CLI session credential is missing".to_owned(),
|
||||
));
|
||||
}
|
||||
let session_digest = Digest::sha256(session_secret);
|
||||
let record = self
|
||||
.durable
|
||||
.cli_sessions
|
||||
.get(&session_digest)
|
||||
.ok_or_else(|| {
|
||||
CoordinatorError::Unauthorized(
|
||||
"CLI session credential is not recognized".to_owned(),
|
||||
)
|
||||
})?;
|
||||
if record.revoked {
|
||||
return Err(CoordinatorError::Unauthorized(
|
||||
"CLI session credential has been revoked".to_owned(),
|
||||
));
|
||||
}
|
||||
if record
|
||||
.expires_at_epoch_seconds
|
||||
.is_some_and(|expires_at| expires_at <= now_epoch_seconds)
|
||||
{
|
||||
return Err(CoordinatorError::Unauthorized(
|
||||
"CLI session credential has expired; run clusterflux login --browser again"
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
if record.credential_kind != CredentialKind::CliDeviceSession {
|
||||
return Err(CoordinatorError::Unauthorized(
|
||||
"credential is not a CLI session".to_owned(),
|
||||
));
|
||||
}
|
||||
if require_active_tenant {
|
||||
self.ensure_tenant_active(&record.tenant)?;
|
||||
}
|
||||
Ok(AuthContext {
|
||||
tenant: record.tenant.clone(),
|
||||
project: record.project.clone(),
|
||||
actor: Actor::User(record.user.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn revoke_cli_session(
|
||||
&mut self,
|
||||
session_secret: &str,
|
||||
) -> Result<CliSessionRecord, CoordinatorError> {
|
||||
self.authenticate_cli_session(session_secret)?;
|
||||
let session_digest = Digest::sha256(session_secret);
|
||||
let record = self
|
||||
.durable
|
||||
.cli_sessions
|
||||
.get_mut(&session_digest)
|
||||
.expect("authenticated session must exist");
|
||||
record.revoked = true;
|
||||
Ok(record.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn unix_timestamp_seconds() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::InMemoryDurableStore;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn repeated_logins_for_one_user_create_distinct_session_credentials() {
|
||||
let store = InMemoryDurableStore::default();
|
||||
let mut coordinator = Coordinator::boot(&store, 1);
|
||||
let tenant = TenantId::from("tenant");
|
||||
let user = UserId::from("user");
|
||||
|
||||
coordinator.issue_cli_session(
|
||||
tenant.clone(),
|
||||
ProjectId::from("project-one"),
|
||||
user.clone(),
|
||||
"session-one",
|
||||
None,
|
||||
);
|
||||
coordinator.issue_cli_session(
|
||||
tenant,
|
||||
ProjectId::from("project-two"),
|
||||
user,
|
||||
"session-two",
|
||||
None,
|
||||
);
|
||||
|
||||
let subjects = coordinator
|
||||
.durable
|
||||
.credentials
|
||||
.values()
|
||||
.map(|credential| credential.subject.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(coordinator.durable.cli_sessions.len(), 2);
|
||||
assert_eq!(subjects.len(), 2);
|
||||
assert!(subjects
|
||||
.iter()
|
||||
.all(|subject| subject.starts_with("cli-session:sha256:")));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue