blob: 2b57ae60fa5cdab861842299d6471d5d53de95ef [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.
*/
use maka_agent_facts::access::ManagedPrincipalKind;
use maka_config::{
ConfigurationStore,
access::{AccessCreateMode, AccessCredential, CredentialState},
};
use maka_event_log::root::{RootNamespaces, RootOwner};
use std::sync::Arc;
fn credential(id: &str, digit: &str, pending: bool) -> AccessCredential {
AccessCredential {
credential_id: id.into(),
credential_hash: digit.repeat(64),
principal_id: "owner".into(),
principal_kind: ManagedPrincipalKind::RemoteOwner,
grants: vec![
"host.status".into(),
"access.credential.finalize".into(),
"future.operation".into(),
],
can_publish_client_capabilities: true,
can_use_host_paths: false,
created_at: "2026-09-12T00:00:00.000Z".into(),
capability_owner: None,
state: if pending {
CredentialState::Pending {
expires_at: 100,
bind_client_instance: true,
}
} else {
CredentialState::Active {
client_instance_id: None,
}
},
}
}
async fn setup() -> (tempfile::TempDir, RootNamespaces, ConfigurationStore) {
let temp = tempfile::tempdir().unwrap();
let namespaces = RootNamespaces {
ownership: temp.path().join("owners"),
control: temp.path().join("control"),
};
let owner = Arc::new(RootOwner::create(&temp.path().join("root"), &namespaces).unwrap());
let store = ConfigurationStore::for_root(owner).await.unwrap();
(temp, namespaces, store)
}
#[tokio::test]
async fn racing_finalizers_bind_once_and_owner_snapshot_survives_revocation_and_reopen() {
let (temp, namespaces, store) = setup().await;
let catalog = store.catalog().await.unwrap();
store
.create_access_credential(credential("old", "a", false), AccessCreateMode::Issue, None)
.await
.unwrap();
let active = store
.finalize_access_credential("old".into(), "cannot-bind".into(), None, 0)
.await
.unwrap();
assert!(!active.value.reconnect_required);
assert!(
!store
.has_active_bound_client_identity("owner".into(), "cannot-bind".into())
.await
.unwrap()
);
let mut provider = credential("provider", "c", false);
provider.principal_kind = ManagedPrincipalKind::CapabilityProvider;
provider.principal_id = "provider".into();
assert!(
store
.create_access_credential(
provider.clone(),
AccessCreateMode::Issue,
Some("old".into())
)
.await
.is_err()
);
store
.create_access_credential(
credential("candidate", "b", true),
AccessCreateMode::Prepare,
None,
)
.await
.unwrap();
let (first, second) = tokio::join!(
store.finalize_access_credential("candidate".into(), "client-a".into(), None, 99),
store.finalize_access_credential("candidate".into(), "client-b".into(), None, 99)
);
assert_ne!(first.is_ok(), second.is_ok());
let winner = if first.is_ok() {
"client-a"
} else {
"client-b"
};
let change = first.or(second).unwrap();
assert!(change.value.reconnect_required);
assert_eq!(change.revoked, ["old"]);
assert!(
!store
.finalize_access_credential("candidate".into(), winner.into(), Some(winner.into()), 101)
.await
.unwrap()
.value
.reconnect_required
);
assert!(
store
.finalize_access_credential("candidate".into(), winner.into(), None, 101)
.await
.unwrap()
.value
.reconnect_required
);
assert!(
store
.has_active_bound_client_identity("owner".into(), winner.into())
.await
.unwrap()
);
let created = store
.create_access_credential(provider, AccessCreateMode::Issue, Some("candidate".into()))
.await
.unwrap()
.value;
assert_eq!(
created
.capability_owner
.as_ref()
.unwrap()
.client_instance_id,
winner
);
assert!(
store
.revoke_access_credential("candidate".into(), "2026-09-12T01:00:00Z".into())
.await
.unwrap()
.value
);
assert!(
!store
.has_active_bound_client_identity("owner".into(), winner.into())
.await
.unwrap()
);
assert_eq!(store.catalog().await.unwrap(), catalog);
store.close().await.unwrap();
let reopened = ConfigurationStore::for_root(Arc::new(
RootOwner::open(&temp.path().join("root"), &namespaces).unwrap(),
))
.await
.unwrap();
let authenticated = reopened
.authenticate_access_credential("c".repeat(64), 500)
.await
.unwrap()
.unwrap();
assert_eq!(
serde_json::to_value(authenticated).unwrap(),
serde_json::to_value(created).unwrap()
);
assert!(
reopened
.authenticate_access_credential("b".repeat(64), 500)
.await
.unwrap()
.is_none()
);
reopened.close().await.unwrap();
}
#[tokio::test]
async fn expiry_revoke_and_replacement_are_atomic_and_report_removed_credentials() {
let (_temp, _namespaces, store) = setup().await;
store
.create_access_credential(
credential("active", "a", false),
AccessCreateMode::Issue,
None,
)
.await
.unwrap();
store
.create_access_credential(credential("p1", "b", true), AccessCreateMode::Prepare, None)
.await
.unwrap();
let replacement = store
.create_access_credential(credential("p2", "c", true), AccessCreateMode::Prepare, None)
.await
.unwrap();
assert_eq!(replacement.revoked, ["p1"]);
assert_eq!(
store.next_access_credential_expiry().await.unwrap(),
Some(100)
);
assert!(
store
.authenticate_access_credential("c".repeat(64), 99)
.await
.unwrap()
.is_some()
);
assert!(
store
.authenticate_access_credential("c".repeat(64), 100)
.await
.unwrap()
.is_none()
);
assert!(
store
.finalize_access_credential("p2".into(), "client".into(), None, 100)
.await
.is_err()
);
assert_eq!(
store.active_access_credentials().await.unwrap()[0].credential_id,
"active"
);
assert_eq!(store.expire_access_credentials(100).await.unwrap(), ["p2"]);
store
.create_access_credential(credential("p3", "d", true), AccessCreateMode::Prepare, None)
.await
.unwrap();
let revoked = store
.revoke_access_credential("active".into(), "2026-09-12T01:00:00Z".into())
.await
.unwrap();
assert!(revoked.value);
assert_eq!(revoked.revoked, ["active", "p3"]);
assert!(
!store
.revoke_access_credential("active".into(), "again".into())
.await
.unwrap()
.value
);
// The revoked record remains: neither its identity nor hash can be reissued.
assert!(
store
.create_access_credential(
credential("active", "a", false),
AccessCreateMode::Issue,
None
)
.await
.is_err()
);
store
.create_access_credential(credential("new", "e", false), AccessCreateMode::Issue, None)
.await
.unwrap();
store
.create_access_credential(credential("p4", "f", true), AccessCreateMode::Prepare, None)
.await
.unwrap();
let replaced = store
.create_access_credential(
credential("replacement", "1", false),
AccessCreateMode::Replace,
None,
)
.await
.unwrap();
assert_eq!(replaced.revoked, ["new", "p4"]);
let mut bad = credential("invalid", "2", true);
bad.principal_kind = ManagedPrincipalKind::CapabilityProvider;
assert!(
store
.create_access_credential(bad, AccessCreateMode::Prepare, None)
.await
.is_err()
);
let mut old_shape = serde_json::to_value(credential("legacy", "3", false)).unwrap();
old_shape.as_object_mut().unwrap().remove("state");
assert!(serde_json::from_value::<AccessCredential>(old_shape).is_err());
store.close().await.unwrap();
}