blob: b3e24dae6c60ebbf2c8d87cec84cb0cf69913f01 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Credentials grant the exact published application source and endpoint set.
use super::{Host, Result};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use maka_agent_facts::access::ManagedPrincipalKind;
use maka_config::access::{AccessCreateMode, AccessCredential, CredentialState};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
pub(super) enum Authority {
Local,
Remote { hash: String },
}
impl Authority {
pub(super) fn local(&self) -> Result<()> {
matches!(self, Self::Local)
.then_some(())
.ok_or("Host control requires the local owner".into())
}
}
#[derive(PartialEq, Eq)]
pub(super) enum Principal {
Local,
Credential(String),
}
/// Intended only for the importing client; never logged or formatted as Debug.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Pairing {
pub root_id: String,
pub credential_id: String,
pub credential: String,
}
pub(super) fn now() -> Result<u64> {
u64::try_from(time::OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000)
.map_err(|_| "clock precedes Unix epoch".into())
}
fn timestamp() -> Result<String> {
time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.map_err(|error| error.to_string())
}
impl Host {
pub(super) async fn credential(&self, hash: &str) -> Result<AccessCredential> {
self.access
.authenticate_access_credential(hash.into(), now()?)
.await
.map_err(|error| error.to_string())?
.filter(|value| {
value.principal_kind == ManagedPrincipalKind::RemoteOwner
&& value.grants.contains(&*self.access_grant.lock().unwrap())
})
.ok_or("credential does not grant this application source".into())
}
/// Call while holding access_gate through admission to close revocation races.
pub(super) async fn identify(
&self,
authority: &Authority,
client: Option<&str>,
) -> Result<Principal> {
match authority {
Authority::Local => Ok(Principal::Local),
Authority::Remote { hash } => {
let credential = self.credential(hash).await?;
let client = client
.filter(|value| !value.is_empty())
.ok_or("remote client identity required")?;
if !matches!(credential.state, CredentialState::Active { .. })
|| credential.client_instance_id() != Some(client)
{
return Err(
"finalize pairing with this client identity before opening a session"
.into(),
);
}
Ok(Principal::Credential(credential.credential_id))
}
}
}
pub(super) fn revoked(&self, credentials: &[String]) -> Result<()> {
for session in self.sessions.lock().unwrap().values() {
if let Principal::Credential(id) = &session.principal
&& credentials.contains(id)
{
session.peer.replace(vec![])?;
session.transport.request_close();
}
}
Ok(())
}
pub async fn prepare_access(&self, principal: String) -> Result<Pairing> {
let _gate = self.access_gate.lock().await;
if self.stop.is_cancelled() {
return Err("Host is closing".into());
}
let expired = self
.access
.expire_access_credentials(now()?)
.await
.map_err(|error| error.to_string())?;
self.revoked(&expired)?;
let mut bytes = [0; 32];
getrandom::fill(&mut bytes).map_err(|_| "cannot generate access credential")?;
let secret = format!("maka_rh_{}", URL_SAFE_NO_PAD.encode(bytes));
let credential_id = Uuid::new_v4().to_string();
let credential = AccessCredential {
credential_id: credential_id.clone(),
credential_hash: hex::encode(Sha256::digest(secret.as_bytes())),
principal_id: principal,
principal_kind: ManagedPrincipalKind::RemoteOwner,
grants: vec![
self.access_grant.lock().unwrap().clone(),
"access.credential.finalize".into(),
],
can_publish_client_capabilities: false,
can_use_host_paths: false,
created_at: timestamp()?,
state: CredentialState::Pending {
expires_at: now()?
.checked_add(15 * 60 * 1000)
.ok_or("credential expiry overflow")?,
bind_client_instance: true,
},
capability_owner: None,
};
let changed = self
.access
.create_access_credential(credential, AccessCreateMode::Prepare, None)
.await
.map_err(|error| error.to_string())?;
self.revoked(&changed.revoked)?;
Ok(Pairing {
root_id: self.source.state_domain.clone(),
credential_id,
credential: secret,
})
}
pub async fn revoke_access(&self, credential: String) -> Result<bool> {
let _gate = self.access_gate.lock().await;
let changed = self
.access
.revoke_access_credential(credential, timestamp()?)
.await
.map_err(|error| error.to_string())?;
self.revoked(&changed.revoked)?;
Ok(changed.value)
}
pub(super) async fn finalize_access(
&self,
authority: &Authority,
client: Option<String>,
) -> Result<()> {
let Authority::Remote { hash } = authority else {
return Err("pairing requires the remote credential".into());
};
let _gate = self.access_gate.lock().await;
let credential = self.credential(hash).await?;
let changed = self
.access
.finalize_access_credential(
credential.credential_id.clone(),
client.ok_or("client identity required")?,
credential.client_instance_id().map(str::to_owned),
now()?,
)
.await
.map_err(|error| error.to_string())?;
self.revoked(&changed.revoked)
}
}