Add identity and target authorization package

OIDC validation, principal mapping, remora:v1 roles, and current-authz
filtering with kind acceptance on Keycloak and ZITADEL.
This commit is contained in:
ms2k 2026-07-16 18:27:42 +09:00
commit bf028161ae

View file

@ -0,0 +1,289 @@
use remora_domain::{BucketName, OpaqueId, StorageScope, Target, TargetPrefix, TargetStoreId};
use remora_identity::{
AuthorizedResource, DefaultTarget, IdentityError, ManagementOperation, ResourceVisibility,
TargetOverride, can_target, decode_roles, encode_admin_role, encode_storage_role,
filter_visible, may_manage, may_request_execution_grant_revocation, parse_default_target,
principal_id_for, resolve_authorized_target, resolve_target, resource_visibility,
roles_from_names,
};
fn scope(store: &str, bucket: &str, prefix: &str) -> StorageScope {
StorageScope {
store: TargetStoreId::parse(store).unwrap(),
bucket: BucketName::parse(bucket).unwrap(),
prefix: TargetPrefix::parse(prefix, ["_remora"]).unwrap(),
}
}
fn target(store: &str, bucket: &str, prefix: &str) -> Target {
Target {
store: TargetStoreId::parse(store).unwrap(),
bucket: BucketName::parse(bucket).unwrap(),
prefix: TargetPrefix::parse(prefix, ["_remora"]).unwrap(),
}
}
fn principal(
label: &str,
administrator: bool,
scopes: Vec<StorageScope>,
) -> remora_identity::Principal {
remora_identity::Principal {
id: OpaqueId::parse(format!("principal-{label}-aaaa")).unwrap(),
issuer: format!("https://issuer.example/{label}"),
subject: format!("subject-{label}"),
administrator,
scopes,
}
}
#[test]
fn scope_segment_matching_rejects_textual_prefix_coincidence() {
let owner = principal(
"owner",
false,
vec![scope("target-main", "target-bucket", "research")],
);
assert!(can_target(
&owner,
&target("target-main", "target-bucket", "research/data")
));
assert!(!can_target(
&owner,
&target("target-main", "target-bucket", "researchers")
));
assert!(!can_target(
&owner,
&target("target-main", "target-bucket", "researc")
));
}
#[test]
fn unicode_prefix_nfc_canonicalization_and_reserved_segments() {
let defaults = parse_default_target("target-main", "target-bucket", "", &["_remora"]).unwrap();
let resolved = resolve_target(
&defaults,
&TargetOverride {
bucket: None,
prefix: Some("cafe\u{0301}/models".into()),
},
&["_remora"],
)
.unwrap();
assert_eq!(resolved.prefix.canonical(), "caf\u{e9}/models");
let reserved = resolve_target(
&defaults,
&TargetOverride {
bucket: None,
prefix: Some("_remora/private".into()),
},
&["_remora"],
);
assert!(reserved.is_err());
let category_c = resolve_target(
&defaults,
&TargetOverride {
bucket: None,
prefix: Some("private/\u{e000}".into()),
},
&["_remora"],
);
assert!(category_c.is_err());
}
#[test]
fn default_target_applies_bucket_and_prefix_overrides() {
let defaults = DefaultTarget {
store: TargetStoreId::parse("target-main").unwrap(),
bucket: BucketName::parse("default-bucket").unwrap(),
prefix: TargetPrefix::parse("defaults", []).unwrap(),
};
let full_default = resolve_target(&defaults, &TargetOverride::default(), &[]).unwrap();
assert_eq!(full_default.bucket.as_str(), "default-bucket");
assert_eq!(full_default.prefix.canonical(), "defaults");
let overridden = resolve_target(
&defaults,
&TargetOverride {
bucket: Some("other-bucket".into()),
prefix: Some("models/current/".into()),
},
&[],
)
.unwrap();
assert_eq!(overridden.store.as_str(), "target-main");
assert_eq!(overridden.bucket.as_str(), "other-bucket");
assert_eq!(overridden.prefix.canonical(), "models/current");
}
#[test]
fn resolve_authorized_target_requires_covering_scope() {
let defaults = DefaultTarget {
store: TargetStoreId::parse("target-main").unwrap(),
bucket: BucketName::parse("target-bucket").unwrap(),
prefix: TargetPrefix::parse("defaults", []).unwrap(),
};
let scoped = principal(
"scoped",
false,
vec![scope("target-main", "target-bucket", "models")],
);
let ok = resolve_authorized_target(
&scoped,
&defaults,
&TargetOverride {
bucket: None,
prefix: Some("models/current".into()),
},
&[],
)
.unwrap();
assert_eq!(ok.prefix.canonical(), "models/current");
let denied = resolve_authorized_target(
&scoped,
&defaults,
&TargetOverride {
bucket: None,
prefix: Some("other".into()),
},
&[],
);
assert!(matches!(denied, Err(IdentityError::UnauthorizedTarget)));
let admin = principal("admin", true, vec![]);
let admin_ok = resolve_authorized_target(
&admin,
&defaults,
&TargetOverride {
bucket: Some("any-bucket".into()),
prefix: Some("anywhere".into()),
},
&[],
)
.unwrap();
assert_eq!(admin_ok.bucket.as_str(), "any-bucket");
}
#[test]
fn revocation_of_roles_removes_current_management_not_admin_revoke_for_owners() {
let target = target("target-main", "target-bucket", "models/current");
let resource = AuthorizedResource {
owner: OpaqueId::parse("principal-owner-aaaa").unwrap(),
target: target.clone(),
};
let with_scope = principal(
"owner",
false,
vec![scope("target-main", "target-bucket", "models")],
);
assert!(may_manage(
&with_scope,
&resource,
ManagementOperation::Cancel
));
assert!(!may_request_execution_grant_revocation(&with_scope));
let after_role_loss = principal("owner", false, vec![]);
assert!(!may_manage(
&after_role_loss,
&resource,
ManagementOperation::Read
));
assert_eq!(
resource_visibility(&after_role_loss, Some(&resource), ManagementOperation::Read),
ResourceVisibility::NotFound
);
let admin = principal("admin", true, vec![]);
assert!(may_manage(
&admin,
&resource,
ManagementOperation::HardDelete
));
assert!(may_request_execution_grant_revocation(&admin));
}
#[test]
fn missing_and_unauthorized_resources_are_indistinguishable() {
let owner = principal(
"owner",
false,
vec![scope("target-main", "target-bucket", "")],
);
let other = principal(
"other",
false,
vec![scope("target-main", "target-bucket", "")],
);
let resource = AuthorizedResource {
owner: owner.id.clone(),
target: target("target-main", "target-bucket", "models"),
};
let missing = resource_visibility(&owner, None, ManagementOperation::Read);
let unauthorized = resource_visibility(&other, Some(&resource), ManagementOperation::Read);
assert_eq!(missing, ResourceVisibility::NotFound);
assert_eq!(unauthorized, ResourceVisibility::NotFound);
assert_eq!(missing, unauthorized);
let visible = resource_visibility(&owner, Some(&resource), ManagementOperation::Read);
assert_eq!(visible, ResourceVisibility::Visible);
}
#[test]
fn list_filtering_hides_unauthorized_existence() {
let owner = principal(
"owner",
false,
vec![scope("target-main", "target-bucket", "models")],
);
let mine = AuthorizedResource {
owner: owner.id.clone(),
target: target("target-main", "target-bucket", "models/a"),
};
let outside = AuthorizedResource {
owner: owner.id.clone(),
target: target("target-main", "target-bucket", "other"),
};
let peer = AuthorizedResource {
owner: OpaqueId::parse("principal-peer-aaaaaa").unwrap(),
target: target("target-main", "target-bucket", "models/b"),
};
let visible = filter_visible(&owner, [&mine, &outside, &peer]);
assert_eq!(visible, vec![&mine]);
}
#[test]
fn principal_ids_are_stable_for_issuer_subject_pairs() {
let first = principal_id_for("https://issuer.example", "user-1").unwrap();
let second = principal_id_for("https://issuer.example", "user-1").unwrap();
let other = principal_id_for("https://issuer.example", "user-2").unwrap();
assert_eq!(first, second);
assert_ne!(first, other);
}
#[test]
fn role_encoding_round_trip_and_foreign_roles_ignored() {
let storage =
encode_storage_role(&scope("target-main", "target-bucket", "models/current")).unwrap();
let decoded = decode_roles(
[encode_admin_role().as_str(), "test-user", storage.as_str()],
&["_remora"],
)
.unwrap();
assert!(decoded.administrator);
assert_eq!(decoded.scopes.len(), 1);
assert!(decoded.scopes[0].covers(&target(
"target-main",
"target-bucket",
"models/current/file"
)));
let roles = roles_from_names(&[remora_identity::ADMIN_ROLE], &[]).unwrap();
assert!(roles.administrator);
let bare = roles_from_names(&["admin"], &[]).unwrap();
assert!(!bare.administrator);
}