use std::collections::BTreeMap;
use std::fmt;
pub use agent_registry_contract::ResourceRelations;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const SERVICE_NAME: &str = "agent-runtime-identity";
pub const AUDIENCE: &str = "agent-runtime-identity";
pub const DEFAULT_PORT: u16 = 5111;
pub const HEALTH_PATH: &str = "/health";
pub const IDENTITIES_PATH: &str = "/internal/v1/runtime-identities";
pub const LIST_IDENTITIES_PATH: &str = "/internal/v1/runtime-identities:list";
pub const IDENTITY_PATH: &str = "/internal/v1/runtime-identities/{identity_id}";
pub const ACTIVATE_PATH: &str = "/internal/v1/runtime-identities/{identity_id}:activate";
pub const SUSPEND_PATH: &str = "/internal/v1/runtime-identities/{identity_id}:suspend";
pub const RESUME_PATH: &str = "/internal/v1/runtime-identities/{identity_id}:resume";
pub const REVOKE_PATH: &str = "/internal/v1/runtime-identities/{identity_id}:revoke";
pub const PERMISSIONS_PATH: &str = "/internal/v1/runtime-identities/{identity_id}/permissions";
pub const EFFECTIVE_PERMISSIONS_PATH: &str =
"/internal/v1/runtime-identities/{identity_id}/effective-permissions";
pub const PERMISSION_CHECK_PATH: &str = "/internal/v1/runtime-permissions:check";
pub const ISSUE_CREDENTIAL_PATH: &str = "/internal/v1/runtime-identities/credentials:issue";
pub const RECOVER_CREDENTIAL_PATH: &str = "/internal/v1/runtime-identities/credentials:recover";
pub const VERIFY_CREDENTIAL_PATH: &str = "/internal/v1/runtime-credentials:verify";
pub const REVOKE_CREDENTIAL_PATH: &str = "/internal/v1/runtime-credentials:revoke";
pub const RECOVERY_CREDENTIALS_PATH: &str =
"/internal/v1/runtime-identities/{identity_id}/recovery-credentials";
pub const RECOVERY_CREDENTIAL_ACTION_PATH: &str =
"/internal/v1/runtime-identities/{identity_id}/recovery-credentials/{credential_action}";
pub const JWKS_PATH: &str = "/.well-known/runtime-identity-jwks.json";
pub const RECOVERY_CREDENTIAL_PREFIX: &str = "rti_rc_";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TenantProjectScope {
pub tenant_id: String,
pub project_id: String,
}
impl TenantProjectScope {
pub fn new(
tenant_id: impl Into<String>,
project_id: impl Into<String>,
) -> Result<Self, String> {
let value = Self {
tenant_id: tenant_id.into(),
project_id: project_id.into(),
};
validate_identifier("tenant_id", &value.tenant_id, 128)?;
validate_identifier("project_id", &value.project_id, 128)?;
Ok(value)
}
pub fn local() -> Self {
Self {
tenant_id: "local".into(),
project_id: "default".into(),
}
}
}
macro_rules! identifier {
($name:ident, $label:literal, $max:expr) => {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, String> {
let value = value.into();
validate_identifier($label, &value, $max)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
};
}
identifier!(RuntimeIdentityId, "runtime_identity_id", 255);
identifier!(PermissionGrantId, "permission_grant_id", 255);
identifier!(RecoveryCredentialId, "recovery_credential_id", 255);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CredentialCapability(String);
impl CredentialCapability {
pub fn new(value: impl Into<String>) -> Result<Self, String> {
let value = value.into();
if value.is_empty() || value.len() > 128 {
return Err("capability must contain 1..=128 bytes".into());
}
let segments = value.split(':').collect::<Vec<_>>();
if segments.len() != 3
|| segments.iter().any(|segment| {
segment.is_empty()
|| (*segment != "*"
&& !segment.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_')
}))
})
{
return Err("capability must contain three valid colon-delimited segments".into());
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub fn capability_grant_matches(granted: &str, requested: &str) -> bool {
if granted == "*" || granted == requested {
return true;
}
let mut granted_segments = granted.split(':');
let mut requested_segments = requested.split(':');
for _ in 0..3 {
let (Some(granted), Some(requested)) = (granted_segments.next(), requested_segments.next())
else {
return false;
};
if granted != "*" && granted != requested {
return false;
}
}
granted_segments.next().is_none() && requested_segments.next().is_none()
}
impl fmt::Display for CredentialCapability {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeIdentityKind {
Code,
Evaluate,
Tool,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeIdentityStatus {
Pending,
Active,
Suspended,
Revoked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeIdentity {
pub id: RuntimeIdentityId,
#[serde(default)]
pub name: String,
pub scope: TenantProjectScope,
pub kind: RuntimeIdentityKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default)]
pub relations: ResourceRelations,
pub status: RuntimeIdentityStatus,
pub revision: u64,
pub grant_revision: u64,
pub credential_generation: u64,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateRuntimeIdentityRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<RuntimeIdentityId>,
pub name: String,
pub kind: RuntimeIdentityKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default)]
pub relations: ResourceRelations,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub idempotency_key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UpdateRuntimeIdentityRequest {
pub expected_revision: u64,
pub name: String,
#[serde(default)]
pub idempotency_key: String,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateRuntimeIdentityResponse {
pub identity: RuntimeIdentity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bootstrap_token: Option<String>,
}
impl fmt::Debug for CreateRuntimeIdentityResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CreateRuntimeIdentityResponse")
.field("identity", &self.identity)
.field(
"bootstrap_token",
&self.bootstrap_token.as_ref().map(|_| "[REDACTED]"),
)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TransitionIdentityRequest {
pub expected_revision: u64,
pub idempotency_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListRuntimeIdentitiesRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<RuntimeIdentityStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<RuntimeIdentityId>,
#[serde(default = "default_page_size")]
pub limit: usize,
}
impl Default for ListRuntimeIdentitiesRequest {
fn default() -> Self {
Self {
status: None,
after: None,
limit: default_page_size(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListRuntimeIdentitiesResponse {
pub items: Vec<RuntimeIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next: Option<RuntimeIdentityId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdentityAccess {
Tenant,
User { user_id: String },
Runtime { runtime_identity_id: String },
}
impl IdentityAccess {
pub fn allows(&self, identity: &RuntimeIdentity) -> bool {
match self {
Self::Tenant => true,
Self::User { user_id } => identity.relations.user_ids.iter().any(|id| id == user_id),
Self::Runtime {
runtime_identity_id,
} => {
identity.id.as_str() == runtime_identity_id
|| identity
.relations
.runtime_ids
.iter()
.any(|id| id == runtime_identity_id)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PermissionGrantInput {
pub audience: String,
pub capabilities: Vec<CredentialCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PermissionGrant {
pub id: PermissionGrantId,
pub identity_id: RuntimeIdentityId,
pub audience: String,
pub capabilities: Vec<CredentialCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReplacePermissionsRequest {
pub expected_revision: u64,
pub grants: Vec<PermissionGrantInput>,
pub idempotency_key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PermissionsResponse {
pub identity_id: RuntimeIdentityId,
pub revision: u64,
pub grants: Vec<PermissionGrant>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EffectivePermissionsResponse {
pub identity_id: RuntimeIdentityId,
pub audience: String,
pub grant_revision: u64,
pub capabilities: Vec<CredentialCapability>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CheckPermissionRequest {
pub identity_id: RuntimeIdentityId,
pub audience: String,
pub capability: CredentialCapability,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CheckPermissionResponse {
pub allowed: bool,
pub grant_revision: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RecoveryCredentialStatus {
Active,
Revoked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RecoveryCredential {
pub id: RecoveryCredentialId,
pub identity_id: RuntimeIdentityId,
pub name: String,
pub purpose: String,
pub status: RecoveryCredentialStatus,
pub ceiling: BTreeMap<String, Vec<CredentialCapability>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub predecessor_id: Option<RecoveryCredentialId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub successor_id: Option<RecoveryCredentialId>,
pub created_by: String,
pub created_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_used_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revoked_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateRecoveryCredentialRequest {
pub name: String,
#[serde(default)]
pub purpose: String,
#[serde(default)]
pub ceiling: BTreeMap<String, Vec<CredentialCapability>>,
#[serde(default)]
pub idempotency_key: String,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateRecoveryCredentialResponse {
pub credential: RecoveryCredential,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
}
impl fmt::Debug for CreateRecoveryCredentialResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CreateRecoveryCredentialResponse")
.field("credential", &self.credential)
.field("secret", &self.secret.as_ref().map(|_| "[REDACTED]"))
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RotateRecoveryCredentialRequest {
#[serde(default)]
pub idempotency_key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RevokeRecoveryCredentialRequest {
#[serde(default)]
pub idempotency_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListRecoveryCredentialsResponse {
pub items: Vec<RecoveryCredential>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct IssueRuntimeCredentialRequest {
pub workload_proof: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proof_audience: Option<String>,
pub audience: String,
pub capabilities: Vec<CredentialCapability>,
}
impl fmt::Debug for IssueRuntimeCredentialRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IssueRuntimeCredentialRequest")
.field("workload_proof", &"[REDACTED]")
.field("proof_audience", &self.proof_audience)
.field("audience", &self.audience)
.field("capabilities", &self.capabilities)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct IssueRuntimeCredentialResponse {
pub access_token: String,
pub token_type: String,
pub expires_in_seconds: u64,
pub key_id: String,
}
impl fmt::Debug for IssueRuntimeCredentialResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IssueRuntimeCredentialResponse")
.field("access_token", &"[REDACTED]")
.field("token_type", &self.token_type)
.field("expires_in_seconds", &self.expires_in_seconds)
.field("key_id", &self.key_id)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct VerifyRuntimeCredentialRequest {
pub access_token: String,
pub audience: String,
}
impl fmt::Debug for VerifyRuntimeCredentialRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VerifyRuntimeCredentialRequest")
.field("access_token", &"[REDACTED]")
.field("audience", &self.audience)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeCredentialClaims {
pub issuer: String,
pub runtime_identity_id: RuntimeIdentityId,
pub scope: TenantProjectScope,
#[serde(default)]
pub relations: ResourceRelations,
pub audience: String,
pub capabilities: Vec<CredentialCapability>,
pub credential_generation: u64,
pub grant_revision: u64,
pub issued_at_seconds: u64,
pub expires_at_seconds: u64,
pub token_id: String,
pub key_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct VerifyRuntimeCredentialResponse {
pub active: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claims: Option<RuntimeCredentialClaims>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RevokeRuntimeCredentialRequest {
pub token_id: String,
pub expires_at_seconds: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeCredentialPublicKey {
pub kty: String,
#[serde(rename = "use")]
pub key_use: String,
pub alg: String,
pub kid: String,
pub crv: String,
pub x: String,
pub y: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeCredentialKeySet {
pub issuer: String,
pub keys: Vec<RuntimeCredentialPublicKey>,
pub cache_max_age_seconds: u64,
}
pub fn validate_audience(value: &str) -> Result<(), String> {
validate_identifier("audience", value, 128)
}
pub fn validate_runtime_name(value: &str) -> Result<String, String> {
let value = value.trim();
if value.is_empty() || value.chars().count() > 128 {
return Err("runtime name must contain 1..=128 characters".into());
}
if value.chars().any(char::is_control) {
return Err("runtime name must not contain control characters".into());
}
Ok(value.to_string())
}
pub fn validate_capabilities(values: &[CredentialCapability]) -> Result<(), String> {
if values.is_empty() || values.len() > 32 {
return Err("capabilities must contain 1..=32 entries".into());
}
let mut unique = std::collections::BTreeSet::new();
if !values.iter().all(|value| unique.insert(value.as_str())) {
return Err("capabilities must not contain duplicates".into());
}
Ok(())
}
fn validate_identifier(label: &str, value: &str, max_bytes: usize) -> Result<(), String> {
if value.is_empty() || value.len() > max_bytes {
return Err(format!("{label} must contain 1..={max_bytes} bytes"));
}
if !value.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'.' | b'-' | b'_' | b'@')
}) {
return Err(format!("{label} contains unsupported characters"));
}
Ok(())
}
fn default_page_size() -> usize {
50
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_bodies_cannot_override_scope() {
let result = serde_json::from_value::<CreateRuntimeIdentityRequest>(serde_json::json!({
"name": "runtime-test",
"kind": "code",
"tenantId": "attacker"
}));
assert!(result.is_err());
}
#[test]
fn runtime_name_is_trimmed_and_bounded() {
assert_eq!(validate_runtime_name(" code-dev ").unwrap(), "code-dev");
assert!(validate_runtime_name(" ").is_err());
assert!(validate_runtime_name("runtime\nname").is_err());
assert!(validate_runtime_name(&"r".repeat(129)).is_err());
}
#[test]
fn credentials_are_redacted_from_debug() {
let request = IssueRuntimeCredentialRequest {
workload_proof: "sensitive-bootstrap-value".into(),
proof_audience: None,
audience: "agent-model-infra".into(),
capabilities: vec![CredentialCapability::new("model-profile:echo:invoke").unwrap()],
};
assert!(!format!("{request:?}").contains("sensitive-bootstrap-value"));
}
#[test]
fn recovery_secrets_are_redacted_from_debug() {
let response = CreateRecoveryCredentialResponse {
credential: RecoveryCredential {
id: RecoveryCredentialId::new("rti_rc_abc").unwrap(),
identity_id: RuntimeIdentityId::new("rti_1").unwrap(),
name: "dev-mac".into(),
purpose: "local".into(),
status: RecoveryCredentialStatus::Active,
ceiling: BTreeMap::new(),
predecessor_id: None,
successor_id: None,
created_by: "user-1".into(),
created_at: Utc::now(),
last_used_at: None,
revoked_at: None,
},
secret: Some("rti_rc_abc.super-secret".into()),
};
assert!(!format!("{response:?}").contains("super-secret"));
}
#[test]
fn capabilities_are_bounded_and_unique() {
let capability = CredentialCapability::new("model-profile:echo:invoke").unwrap();
assert!(validate_capabilities(std::slice::from_ref(&capability)).is_ok());
assert!(validate_capabilities(&[capability.clone(), capability]).is_err());
assert!(CredentialCapability::new("model-profile:*:invoke").is_ok());
assert!(CredentialCapability::new("model-profile:echo:*").is_ok());
assert!(CredentialCapability::new("model-profile:echo:inv*oke").is_err());
}
#[test]
fn capability_grants_match_only_whole_segments() {
assert!(capability_grant_matches(
"model-profile:*:invoke",
"model-profile:deepseek:invoke"
));
assert!(capability_grant_matches(
"conversation:conv-1:*",
"conversation:conv-1:append"
));
assert!(!capability_grant_matches(
"model-profile:*:read",
"model-profile:deepseek:invoke"
));
assert!(!capability_grant_matches(
"conversation:conv-1:*",
"conversation:conv-2:append"
));
assert!(!capability_grant_matches(
"model-profile:*",
"model-profile:x"
));
}
fn sample_identity(user_id: &str, runtime_id: &str) -> RuntimeIdentity {
RuntimeIdentity {
id: RuntimeIdentityId::new(runtime_id).unwrap(),
name: "code-dev".into(),
scope: TenantProjectScope::local(),
kind: RuntimeIdentityKind::Code,
agent_id: None,
relations: ResourceRelations {
agent_ids: Vec::new(),
runtime_ids: vec![runtime_id.into()],
user_ids: vec![user_id.into()],
},
status: RuntimeIdentityStatus::Pending,
revision: 1,
grant_revision: 0,
credential_generation: 1,
metadata: BTreeMap::new(),
created_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap(),
updated_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap(),
}
}
#[test]
fn identity_access_scopes_records_to_owner_or_runtime() {
let alice = sample_identity("alice", "runtime-alice");
assert!(IdentityAccess::Tenant.allows(&alice));
assert!(
IdentityAccess::User {
user_id: "alice".into()
}
.allows(&alice)
);
assert!(
!IdentityAccess::User {
user_id: "bob".into()
}
.allows(&alice)
);
assert!(
IdentityAccess::Runtime {
runtime_identity_id: "runtime-alice".into()
}
.allows(&alice)
);
assert!(
!IdentityAccess::Runtime {
runtime_identity_id: "runtime-bob".into()
}
.allows(&alice)
);
}
}