1use std::collections::{BTreeMap, BTreeSet};
39use std::fmt;
40use std::sync::Arc;
41
42use tonic::metadata::MetadataMap;
43use tonic::service::Interceptor;
44use tonic::{Request, Status};
45
46#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct Principal {
55 pub subject: String,
58 pub provider: String,
60 pub roles: BTreeSet<String>,
62 pub claims: BTreeMap<String, String>,
64}
65
66impl Principal {
67 #[must_use]
70 pub fn anonymous() -> Self {
71 Self {
72 subject: "anonymous".to_owned(),
73 provider: "anonymous".to_owned(),
74 roles: BTreeSet::new(),
75 claims: BTreeMap::new(),
76 }
77 }
78
79 #[must_use]
81 pub fn new(provider: impl Into<String>, subject: impl Into<String>) -> Self {
82 Self {
83 subject: subject.into(),
84 provider: provider.into(),
85 roles: BTreeSet::new(),
86 claims: BTreeMap::new(),
87 }
88 }
89
90 #[must_use]
92 pub fn with_role(mut self, role: impl Into<String>) -> Self {
93 self.roles.insert(role.into());
94 self
95 }
96
97 #[must_use]
99 pub fn with_claim(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
100 self.claims.insert(key.into(), value.into());
101 self
102 }
103
104 #[must_use]
106 pub fn is_anonymous(&self) -> bool {
107 self.provider == "anonymous"
108 }
109
110 #[must_use]
112 pub fn has_role(&self, role: &str) -> bool {
113 self.roles.contains(role)
114 }
115
116 #[must_use]
118 pub fn claim(&self, key: &str) -> Option<&str> {
119 self.claims.get(key).map(String::as_str)
120 }
121}
122
123#[derive(Clone, Debug, Default)]
131pub struct Credentials {
132 pub bearer: Option<String>,
134 pub client_cert_subject: Option<String>,
136}
137
138impl Credentials {
139 #[must_use]
141 pub fn from_metadata(metadata: &MetadataMap) -> Self {
142 let bearer = metadata
143 .get("authorization")
144 .and_then(|value| value.to_str().ok())
145 .and_then(|value| value.strip_prefix("Bearer "))
146 .map(str::to_owned);
147 Self {
148 bearer,
149 client_cert_subject: None,
150 }
151 }
152
153 #[must_use]
155 pub fn is_empty(&self) -> bool {
156 self.bearer.is_none() && self.client_cert_subject.is_none()
157 }
158}
159
160#[derive(Clone, Debug, thiserror::Error)]
162pub enum AuthError {
163 #[error("unauthenticated: {0}")]
165 Unauthenticated(String),
166 #[error("forbidden: {0}")]
168 Forbidden(String),
169}
170
171impl AuthError {
172 #[must_use]
174 pub fn to_status(&self) -> Status {
175 match self {
176 Self::Unauthenticated(message) => Status::unauthenticated(message.clone()),
177 Self::Forbidden(message) => Status::permission_denied(message.clone()),
178 }
179 }
180}
181
182pub trait IdentityProvider: Send + Sync + 'static {
190 fn name(&self) -> &str;
192
193 fn authenticate(&self, credentials: &Credentials) -> Result<Option<Principal>, AuthError>;
199}
200
201#[derive(Clone, Debug, Default)]
204pub struct AllowAnonymous;
205
206impl IdentityProvider for AllowAnonymous {
207 #[allow(clippy::unnecessary_literal_bound)]
208 fn name(&self) -> &str {
209 "anonymous"
210 }
211
212 fn authenticate(&self, _credentials: &Credentials) -> Result<Option<Principal>, AuthError> {
213 Ok(Some(Principal::anonymous()))
214 }
215}
216
217#[derive(Clone, Debug, Default)]
224pub struct StaticTokens {
225 tokens: BTreeMap<String, Principal>,
226}
227
228impl StaticTokens {
229 #[must_use]
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 #[must_use]
237 pub fn with(mut self, token: impl Into<String>, principal: Principal) -> Self {
238 self.tokens.insert(token.into(), principal);
239 self
240 }
241}
242
243impl IdentityProvider for StaticTokens {
244 #[allow(clippy::unnecessary_literal_bound)]
245 fn name(&self) -> &str {
246 "static-token"
247 }
248
249 fn authenticate(&self, credentials: &Credentials) -> Result<Option<Principal>, AuthError> {
250 Ok(credentials
254 .bearer
255 .as_ref()
256 .and_then(|token| self.tokens.get(token).cloned()))
257 }
258}
259
260pub trait TokenVerifier: Send + Sync + 'static {
268 fn verify(&self, token: &str) -> Result<Principal, AuthError>;
274}
275
276pub struct ExternalTokens<V: TokenVerifier> {
278 name: String,
279 verifier: V,
280}
281
282impl<V: TokenVerifier> ExternalTokens<V> {
283 pub fn new(name: impl Into<String>, verifier: V) -> Self {
285 Self {
286 name: name.into(),
287 verifier,
288 }
289 }
290}
291
292impl<V: TokenVerifier> IdentityProvider for ExternalTokens<V> {
293 fn name(&self) -> &str {
294 &self.name
295 }
296
297 fn authenticate(&self, credentials: &Credentials) -> Result<Option<Principal>, AuthError> {
298 match &credentials.bearer {
299 None => Ok(None),
300 Some(token) => self.verifier.verify(token).map(Some),
301 }
302 }
303}
304
305#[derive(Clone)]
311pub struct CompositeProvider {
312 providers: Vec<Arc<dyn IdentityProvider>>,
313}
314
315impl CompositeProvider {
316 #[must_use]
318 pub fn new(providers: Vec<Arc<dyn IdentityProvider>>) -> Self {
319 Self { providers }
320 }
321}
322
323impl IdentityProvider for CompositeProvider {
324 #[allow(clippy::unnecessary_literal_bound)]
325 fn name(&self) -> &str {
326 "composite"
327 }
328
329 fn authenticate(&self, credentials: &Credentials) -> Result<Option<Principal>, AuthError> {
330 for provider in &self.providers {
331 if let Some(principal) = provider.authenticate(credentials)? {
332 return Ok(Some(principal));
333 }
334 }
335 Ok(None)
336 }
337}
338
339#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
345pub enum Action {
346 Query,
348 Pull,
350 Datoms,
352 TxRange,
354 Subscribe,
356 Inspect,
358 Transact,
360 CreateDatabase,
362 DeleteDatabase,
364 ForkDatabase,
366 ListDatabases,
368 GarbageCollect,
370 ManageIndex,
372}
373
374impl Action {
375 #[must_use]
377 pub fn is_write(self) -> bool {
378 matches!(self, Self::Transact)
379 }
380
381 #[must_use]
383 pub fn is_admin(self) -> bool {
384 matches!(
385 self,
386 Self::CreateDatabase
387 | Self::DeleteDatabase
388 | Self::ForkDatabase
389 | Self::GarbageCollect
390 | Self::ManageIndex
391 )
392 }
393}
394
395#[derive(Clone, Debug, PartialEq, Eq)]
397pub struct Access {
398 pub action: Action,
400 pub database: Option<String>,
402}
403
404impl Access {
405 #[must_use]
407 pub fn on(action: Action, database: impl Into<String>) -> Self {
408 Self {
409 action,
410 database: Some(database.into()),
411 }
412 }
413
414 #[must_use]
416 pub fn catalog(action: Action) -> Self {
417 Self {
418 action,
419 database: None,
420 }
421 }
422}
423
424pub trait ViewFilter: Send + Sync + fmt::Debug {
432 fn attribute_visible(&self, attribute: &str) -> bool;
435}
436
437#[derive(Clone, Debug)]
439pub struct AttributeAllowlist {
440 allowed: BTreeSet<String>,
441}
442
443impl AttributeAllowlist {
444 pub fn new(attributes: impl IntoIterator<Item = impl Into<String>>) -> Self {
446 Self {
447 allowed: attributes.into_iter().map(Into::into).collect(),
448 }
449 }
450}
451
452impl ViewFilter for AttributeAllowlist {
453 fn attribute_visible(&self, attribute: &str) -> bool {
454 self.allowed.contains(attribute)
455 }
456}
457
458#[derive(Clone)]
460pub enum Decision {
461 Allow,
463 AllowFiltered(Arc<dyn ViewFilter>),
465 Deny(String),
467}
468
469impl fmt::Debug for Decision {
470 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
471 match self {
472 Self::Allow => formatter.write_str("Allow"),
473 Self::AllowFiltered(filter) => write!(formatter, "AllowFiltered({filter:?})"),
474 Self::Deny(reason) => write!(formatter, "Deny({reason:?})"),
475 }
476 }
477}
478
479#[tonic::async_trait]
490pub trait Authorizer: Send + Sync + 'static {
491 async fn authorize(&self, principal: &Principal, access: &Access) -> Decision;
493}
494
495#[derive(Clone, Debug, Default)]
498pub struct AllowAll;
499
500#[tonic::async_trait]
501impl Authorizer for AllowAll {
502 async fn authorize(&self, _principal: &Principal, _access: &Access) -> Decision {
503 Decision::Allow
504 }
505}
506
507#[derive(Clone, Debug)]
509pub struct Grant {
510 pub actions: BTreeSet<ActionClass>,
512 pub databases: BTreeSet<String>,
514 pub view: Option<Arc<dyn ViewFilter>>,
516}
517
518#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
520pub enum ActionClass {
521 Read,
523 Write,
525 Admin,
527}
528
529impl ActionClass {
530 #[must_use]
532 pub fn of(action: Action) -> Self {
533 if action.is_admin() {
534 Self::Admin
535 } else if action.is_write() {
536 Self::Write
537 } else {
538 Self::Read
539 }
540 }
541}
542
543impl Grant {
544 #[must_use]
546 pub fn new(
547 actions: impl IntoIterator<Item = ActionClass>,
548 databases: impl IntoIterator<Item = impl Into<String>>,
549 ) -> Self {
550 Self {
551 actions: actions.into_iter().collect(),
552 databases: databases.into_iter().map(Into::into).collect(),
553 view: None,
554 }
555 }
556
557 #[must_use]
559 pub fn with_view(mut self, view: Arc<dyn ViewFilter>) -> Self {
560 self.view = Some(view);
561 self
562 }
563
564 fn covers(&self, access: &Access) -> bool {
565 let action_ok =
566 self.actions.is_empty() || self.actions.contains(&ActionClass::of(access.action));
567 let database_ok = self.databases.is_empty()
568 || access
569 .database
570 .as_ref()
571 .is_some_and(|database| self.databases.contains(database));
572 action_ok && database_ok
573 }
574}
575
576#[derive(Clone, Default)]
580pub struct PolicyAuthorizer {
581 roles: BTreeMap<String, Vec<Grant>>,
582}
583
584impl PolicyAuthorizer {
585 #[must_use]
587 pub fn new() -> Self {
588 Self::default()
589 }
590
591 #[must_use]
593 pub fn grant(mut self, role: impl Into<String>, grant: Grant) -> Self {
594 self.roles.entry(role.into()).or_default().push(grant);
595 self
596 }
597}
598
599#[tonic::async_trait]
600impl Authorizer for PolicyAuthorizer {
601 async fn authorize(&self, principal: &Principal, access: &Access) -> Decision {
602 let mut matched = false;
603 let mut view: Option<Arc<dyn ViewFilter>> = None;
604 for role in &principal.roles {
605 let Some(grants) = self.roles.get(role) else {
606 continue;
607 };
608 for grant in grants {
609 if grant.covers(access) {
610 matched = true;
611 match &grant.view {
615 Some(filter) if view.is_none() => view = Some(Arc::clone(filter)),
616 Some(_) => {}
617 None => return Decision::Allow,
618 }
619 }
620 }
621 }
622 if !matched {
623 return Decision::Deny(format!(
624 "principal {:?} has no grant for {:?}",
625 principal.subject, access
626 ));
627 }
628 match view {
629 Some(filter) => Decision::AllowFiltered(filter),
630 None => Decision::Allow,
631 }
632 }
633}
634
635#[derive(Clone)]
645pub struct Guard {
646 identity: Arc<dyn IdentityProvider>,
647 authorizer: Arc<dyn Authorizer>,
648 allow_anonymous: bool,
649}
650
651impl Guard {
652 #[must_use]
654 pub fn new(identity: Arc<dyn IdentityProvider>, authorizer: Arc<dyn Authorizer>) -> Self {
655 Self {
656 identity,
657 authorizer,
658 allow_anonymous: false,
659 }
660 }
661
662 #[must_use]
665 pub fn allow_anonymous(mut self, allow: bool) -> Self {
666 self.allow_anonymous = allow;
667 self
668 }
669
670 #[must_use]
673 pub fn disabled() -> Self {
674 Self::new(Arc::new(AllowAnonymous), Arc::new(AllowAll)).allow_anonymous(true)
675 }
676
677 #[must_use]
680 pub fn is_disabled(&self) -> bool {
681 self.identity.name() == "anonymous"
682 }
683
684 pub fn authenticate(&self, credentials: &Credentials) -> Result<Principal, AuthError> {
691 match self.identity.authenticate(credentials)? {
692 Some(principal) => Ok(principal),
693 None if self.allow_anonymous => Ok(Principal::anonymous()),
694 None if credentials.is_empty() => Err(AuthError::Unauthenticated(
695 "authentication is required".to_owned(),
696 )),
697 None => Err(AuthError::Unauthenticated(
698 "no provider accepted the presented credentials".to_owned(),
699 )),
700 }
701 }
702
703 pub async fn authorize(
711 &self,
712 principal: &Principal,
713 access: &Access,
714 ) -> Result<Option<Arc<dyn ViewFilter>>, AuthError> {
715 match self.authorizer.authorize(principal, access).await {
716 Decision::Allow => Ok(None),
717 Decision::AllowFiltered(filter) => Ok(Some(filter)),
718 Decision::Deny(reason) => Err(AuthError::Forbidden(reason)),
719 }
720 }
721}
722
723#[derive(Clone)]
729pub struct IdentityInterceptor {
730 guard: Guard,
731}
732
733impl IdentityInterceptor {
734 #[must_use]
736 pub fn new(guard: Guard) -> Self {
737 Self { guard }
738 }
739}
740
741impl Interceptor for IdentityInterceptor {
742 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
743 let credentials = Credentials::from_metadata(request.metadata());
744 let principal = self
745 .guard
746 .authenticate(&credentials)
747 .map_err(|error| error.to_status())?;
748 request.extensions_mut().insert(principal);
749 Ok(request)
750 }
751}
752
753#[must_use]
756pub fn principal<T>(request: &Request<T>) -> Principal {
757 request
758 .extensions()
759 .get::<Principal>()
760 .cloned()
761 .unwrap_or_else(Principal::anonymous)
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767
768 fn creds(bearer: &str) -> Credentials {
769 Credentials {
770 bearer: Some(bearer.to_owned()),
771 client_cert_subject: None,
772 }
773 }
774
775 #[tokio::test]
776 async fn disabled_guard_allows_anonymous_everything() {
777 let guard = Guard::disabled();
778 assert!(guard.is_disabled());
779 let principal = guard.authenticate(&Credentials::default()).unwrap();
780 assert!(principal.is_anonymous());
781 assert!(
782 guard
783 .authorize(&principal, &Access::on(Action::Transact, "people"))
784 .await
785 .unwrap()
786 .is_none()
787 );
788 }
789
790 #[test]
791 fn static_tokens_map_to_distinct_principals() {
792 let provider = StaticTokens::new()
793 .with(
794 "reader-secret",
795 Principal::new("static-token", "reader").with_role("reader"),
796 )
797 .with(
798 "writer-secret",
799 Principal::new("static-token", "writer").with_role("writer"),
800 );
801
802 let reader = provider.authenticate(&creds("reader-secret")).unwrap();
803 assert_eq!(reader.unwrap().subject, "reader");
804 let writer = provider.authenticate(&creds("writer-secret")).unwrap();
805 assert!(writer.unwrap().has_role("writer"));
806
807 assert!(
810 provider
811 .authenticate(&Credentials::default())
812 .unwrap()
813 .is_none()
814 );
815 assert!(provider.authenticate(&creds("bogus")).unwrap().is_none());
816 }
817
818 struct FakeJwt;
819 impl TokenVerifier for FakeJwt {
820 fn verify(&self, token: &str) -> Result<Principal, AuthError> {
821 let mut parts = token.split(':');
823 match (parts.next(), parts.next(), parts.next()) {
824 (Some("oidc"), Some(sub), Some(tenant)) => Ok(Principal::new("oidc", sub)
825 .with_role("reader")
826 .with_claim("tenant", tenant)),
827 _ => Err(AuthError::Unauthenticated("bad token".to_owned())),
828 }
829 }
830 }
831
832 #[test]
833 fn external_token_verifier_seam_produces_principal() {
834 let provider = ExternalTokens::new("oidc", FakeJwt);
835 let principal = provider
836 .authenticate(&creds("oidc:alice:acme"))
837 .unwrap()
838 .unwrap();
839 assert_eq!(principal.provider, "oidc");
840 assert_eq!(principal.claim("tenant"), Some("acme"));
841 assert!(matches!(
842 provider.authenticate(&creds("garbage")),
843 Err(AuthError::Unauthenticated(_))
844 ));
845 }
846
847 #[test]
848 fn composite_provider_tries_in_order_and_abstains() {
849 let statics = StaticTokens::new().with(
850 "svc-secret",
851 Principal::new("static-token", "svc").with_role("writer"),
852 );
853 let provider = CompositeProvider::new(vec![
854 Arc::new(statics),
855 Arc::new(ExternalTokens::new("oidc", FakeJwt)),
856 ]);
857
858 assert_eq!(
860 provider
861 .authenticate(&creds("svc-secret"))
862 .unwrap()
863 .unwrap()
864 .subject,
865 "svc"
866 );
867 assert_eq!(
869 provider
870 .authenticate(&creds("oidc:bob:beta"))
871 .unwrap()
872 .unwrap()
873 .provider,
874 "oidc"
875 );
876 assert!(
878 provider
879 .authenticate(&Credentials::default())
880 .unwrap()
881 .is_none()
882 );
883
884 let strict = Guard::new(Arc::new(provider.clone()), Arc::new(AllowAll));
887 assert!(matches!(
888 strict.authenticate(&Credentials::default()),
889 Err(AuthError::Unauthenticated(_))
890 ));
891 let open = Guard::new(Arc::new(provider), Arc::new(AllowAll)).allow_anonymous(true);
892 assert!(
893 open.authenticate(&Credentials::default())
894 .unwrap()
895 .is_anonymous()
896 );
897 }
898
899 fn sample_policy() -> PolicyAuthorizer {
900 PolicyAuthorizer::new()
901 .grant("reader", Grant::new([ActionClass::Read], ["people"]))
902 .grant(
903 "writer",
904 Grant::new([ActionClass::Read, ActionClass::Write], ["people"]),
905 )
906 .grant(
907 "admin",
908 Grant::new([ActionClass::Admin], Vec::<String>::new()),
909 )
910 }
911
912 #[tokio::test]
913 async fn policy_authorizer_enforces_actions_and_databases() {
914 let policy = sample_policy();
915 let reader = Principal::new("t", "r").with_role("reader");
916 let writer = Principal::new("t", "w").with_role("writer");
917 let admin = Principal::new("t", "a").with_role("admin");
918
919 assert!(matches!(
921 policy
922 .authorize(&reader, &Access::on(Action::Query, "people"))
923 .await,
924 Decision::Allow
925 ));
926 assert!(matches!(
927 policy
928 .authorize(&reader, &Access::on(Action::Transact, "people"))
929 .await,
930 Decision::Deny(_)
931 ));
932 assert!(matches!(
933 policy
934 .authorize(&reader, &Access::on(Action::Query, "secrets"))
935 .await,
936 Decision::Deny(_)
937 ));
938
939 assert!(matches!(
941 policy
942 .authorize(&writer, &Access::on(Action::Transact, "people"))
943 .await,
944 Decision::Allow
945 ));
946 assert!(matches!(
947 policy
948 .authorize(&admin, &Access::catalog(Action::CreateDatabase))
949 .await,
950 Decision::Allow
951 ));
952 assert!(matches!(
954 policy
955 .authorize(&admin, &Access::on(Action::Query, "people"))
956 .await,
957 Decision::Deny(_)
958 ));
959 }
960
961 #[tokio::test]
962 async fn per_principal_view_filter_gives_different_views() {
963 let acme_view: Arc<dyn ViewFilter> = Arc::new(AttributeAllowlist::new([
965 ":person/name",
966 ":person/acme-note",
967 ]));
968 let beta_view: Arc<dyn ViewFilter> = Arc::new(AttributeAllowlist::new([
969 ":person/name",
970 ":person/beta-note",
971 ]));
972 let policy = PolicyAuthorizer::new()
973 .grant(
974 "acme",
975 Grant::new([ActionClass::Read], ["people"]).with_view(acme_view),
976 )
977 .grant(
978 "beta",
979 Grant::new([ActionClass::Read], ["people"]).with_view(beta_view),
980 );
981 let guard = Guard::new(Arc::new(AllowAnonymous), Arc::new(policy));
982
983 let acme = Principal::new("oidc", "alice").with_role("acme");
984 let beta = Principal::new("oidc", "bob").with_role("beta");
985 let acme_filter = guard
986 .authorize(&acme, &Access::on(Action::Query, "people"))
987 .await
988 .unwrap()
989 .expect("acme is filtered");
990 let beta_filter = guard
991 .authorize(&beta, &Access::on(Action::Query, "people"))
992 .await
993 .unwrap()
994 .expect("beta is filtered");
995
996 assert!(acme_filter.attribute_visible(":person/name"));
997 assert!(acme_filter.attribute_visible(":person/acme-note"));
998 assert!(!acme_filter.attribute_visible(":person/beta-note"));
999
1000 assert!(beta_filter.attribute_visible(":person/beta-note"));
1001 assert!(!beta_filter.attribute_visible(":person/acme-note"));
1002 }
1003
1004 #[test]
1005 fn interceptor_attaches_principal_to_extensions() {
1006 let guard = Guard::new(
1007 Arc::new(StaticTokens::new().with(
1008 "reader-secret",
1009 Principal::new("static-token", "reader").with_role("reader"),
1010 )),
1011 Arc::new(AllowAll),
1012 );
1013 let mut interceptor = IdentityInterceptor::new(guard);
1014
1015 let mut request = Request::new(());
1016 request
1017 .metadata_mut()
1018 .insert("authorization", "Bearer reader-secret".parse().unwrap());
1019 let request = interceptor.call(request).unwrap();
1020 assert_eq!(principal(&request).subject, "reader");
1021
1022 let mut bad = Request::new(());
1024 bad.metadata_mut()
1025 .insert("authorization", "Bearer nope".parse().unwrap());
1026 assert_eq!(
1027 interceptor.call(bad).unwrap_err().code(),
1028 tonic::Code::Unauthenticated
1029 );
1030 }
1031
1032 #[tokio::test]
1033 async fn one_guard_serves_two_tenants_with_different_authority() {
1034 let provider = CompositeProvider::new(vec![Arc::new(
1036 StaticTokens::new()
1037 .with(
1038 "acme-writer",
1039 Principal::new("static-token", "acme-svc").with_role("acme"),
1040 )
1041 .with(
1042 "beta-reader",
1043 Principal::new("static-token", "beta-user").with_role("beta"),
1044 ),
1045 )]);
1046 let policy = PolicyAuthorizer::new()
1047 .grant(
1048 "acme",
1049 Grant::new([ActionClass::Read, ActionClass::Write], ["acme-db"]),
1050 )
1051 .grant("beta", Grant::new([ActionClass::Read], ["beta-db"]));
1052 let guard = Guard::new(Arc::new(provider), Arc::new(policy));
1053
1054 let acme = guard.authenticate(&creds("acme-writer")).unwrap();
1055 let beta = guard.authenticate(&creds("beta-reader")).unwrap();
1056
1057 assert!(
1059 guard
1060 .authorize(&acme, &Access::on(Action::Transact, "acme-db"))
1061 .await
1062 .is_ok()
1063 );
1064 assert!(
1065 guard
1066 .authorize(&beta, &Access::on(Action::Query, "acme-db"))
1067 .await
1068 .is_err()
1069 );
1070 assert!(
1071 guard
1072 .authorize(&beta, &Access::on(Action::Query, "beta-db"))
1073 .await
1074 .is_ok()
1075 );
1076 assert!(
1078 guard
1079 .authorize(&beta, &Access::on(Action::Transact, "beta-db"))
1080 .await
1081 .is_err()
1082 );
1083 }
1084
1085 struct FakeOracle {
1089 allowed: BTreeSet<(String, Action)>,
1090 }
1091
1092 #[tonic::async_trait]
1093 impl Authorizer for FakeOracle {
1094 async fn authorize(&self, principal: &Principal, access: &Access) -> Decision {
1095 tokio::task::yield_now().await;
1097 let key = (principal.subject.clone(), access.action);
1098 if self.allowed.contains(&key) {
1099 Decision::Allow
1100 } else {
1101 Decision::Deny("oracle check failed".to_owned())
1102 }
1103 }
1104 }
1105
1106 #[tokio::test]
1107 async fn external_async_oracle_authorizer() {
1108 let oracle = FakeOracle {
1109 allowed: [("alice".to_owned(), Action::Query)].into_iter().collect(),
1110 };
1111 let guard = Guard::new(Arc::new(AllowAnonymous), Arc::new(oracle));
1112 let alice = Principal::new("oidc", "alice");
1113 let bob = Principal::new("oidc", "bob");
1114
1115 assert!(
1116 guard
1117 .authorize(&alice, &Access::on(Action::Query, "people"))
1118 .await
1119 .is_ok()
1120 );
1121 assert!(
1122 guard
1123 .authorize(&alice, &Access::on(Action::Transact, "people"))
1124 .await
1125 .is_err()
1126 );
1127 assert!(
1128 guard
1129 .authorize(&bob, &Access::on(Action::Query, "people"))
1130 .await
1131 .is_err()
1132 );
1133 }
1134}