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 ManageKeys,
374}
375
376impl Action {
377 #[must_use]
379 pub fn is_write(self) -> bool {
380 matches!(self, Self::Transact)
381 }
382
383 #[must_use]
385 pub fn is_admin(self) -> bool {
386 matches!(
387 self,
388 Self::CreateDatabase
389 | Self::DeleteDatabase
390 | Self::ForkDatabase
391 | Self::GarbageCollect
392 | Self::ManageIndex
393 | Self::ManageKeys
394 )
395 }
396}
397
398#[derive(Clone, Debug, PartialEq, Eq)]
400pub struct Access {
401 pub action: Action,
403 pub database: Option<String>,
405}
406
407impl Access {
408 #[must_use]
410 pub fn on(action: Action, database: impl Into<String>) -> Self {
411 Self {
412 action,
413 database: Some(database.into()),
414 }
415 }
416
417 #[must_use]
419 pub fn catalog(action: Action) -> Self {
420 Self {
421 action,
422 database: None,
423 }
424 }
425}
426
427pub trait ViewFilter: Send + Sync + fmt::Debug {
435 fn attribute_visible(&self, attribute: &str) -> bool;
438}
439
440#[derive(Clone, Debug)]
442pub struct AttributeAllowlist {
443 allowed: BTreeSet<String>,
444}
445
446impl AttributeAllowlist {
447 pub fn new(attributes: impl IntoIterator<Item = impl Into<String>>) -> Self {
449 Self {
450 allowed: attributes.into_iter().map(Into::into).collect(),
451 }
452 }
453}
454
455impl ViewFilter for AttributeAllowlist {
456 fn attribute_visible(&self, attribute: &str) -> bool {
457 self.allowed.contains(attribute)
458 }
459}
460
461#[derive(Clone)]
463pub enum Decision {
464 Allow,
466 AllowFiltered(Arc<dyn ViewFilter>),
468 Deny(String),
470}
471
472impl fmt::Debug for Decision {
473 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
474 match self {
475 Self::Allow => formatter.write_str("Allow"),
476 Self::AllowFiltered(filter) => write!(formatter, "AllowFiltered({filter:?})"),
477 Self::Deny(reason) => write!(formatter, "Deny({reason:?})"),
478 }
479 }
480}
481
482#[tonic::async_trait]
493pub trait Authorizer: Send + Sync + 'static {
494 async fn authorize(&self, principal: &Principal, access: &Access) -> Decision;
496}
497
498#[derive(Clone, Debug, Default)]
501pub struct AllowAll;
502
503#[tonic::async_trait]
504impl Authorizer for AllowAll {
505 async fn authorize(&self, _principal: &Principal, _access: &Access) -> Decision {
506 Decision::Allow
507 }
508}
509
510#[derive(Clone, Debug)]
512pub struct Grant {
513 pub actions: BTreeSet<ActionClass>,
515 pub databases: BTreeSet<String>,
517 pub view: Option<Arc<dyn ViewFilter>>,
519}
520
521#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
523pub enum ActionClass {
524 Read,
526 Write,
528 Admin,
530}
531
532impl ActionClass {
533 #[must_use]
535 pub fn of(action: Action) -> Self {
536 if action.is_admin() {
537 Self::Admin
538 } else if action.is_write() {
539 Self::Write
540 } else {
541 Self::Read
542 }
543 }
544}
545
546impl Grant {
547 #[must_use]
549 pub fn new(
550 actions: impl IntoIterator<Item = ActionClass>,
551 databases: impl IntoIterator<Item = impl Into<String>>,
552 ) -> Self {
553 Self {
554 actions: actions.into_iter().collect(),
555 databases: databases.into_iter().map(Into::into).collect(),
556 view: None,
557 }
558 }
559
560 #[must_use]
562 pub fn with_view(mut self, view: Arc<dyn ViewFilter>) -> Self {
563 self.view = Some(view);
564 self
565 }
566
567 fn covers(&self, access: &Access) -> bool {
568 let action_ok =
569 self.actions.is_empty() || self.actions.contains(&ActionClass::of(access.action));
570 let database_ok = self.databases.is_empty()
571 || access
572 .database
573 .as_ref()
574 .is_some_and(|database| self.databases.contains(database));
575 action_ok && database_ok
576 }
577}
578
579#[derive(Clone, Default)]
583pub struct PolicyAuthorizer {
584 roles: BTreeMap<String, Vec<Grant>>,
585}
586
587impl PolicyAuthorizer {
588 #[must_use]
590 pub fn new() -> Self {
591 Self::default()
592 }
593
594 #[must_use]
596 pub fn grant(mut self, role: impl Into<String>, grant: Grant) -> Self {
597 self.roles.entry(role.into()).or_default().push(grant);
598 self
599 }
600}
601
602#[tonic::async_trait]
603impl Authorizer for PolicyAuthorizer {
604 async fn authorize(&self, principal: &Principal, access: &Access) -> Decision {
605 let mut matched = false;
606 let mut view: Option<Arc<dyn ViewFilter>> = None;
607 for role in &principal.roles {
608 let Some(grants) = self.roles.get(role) else {
609 continue;
610 };
611 for grant in grants {
612 if grant.covers(access) {
613 matched = true;
614 match &grant.view {
618 Some(filter) if view.is_none() => view = Some(Arc::clone(filter)),
619 Some(_) => {}
620 None => return Decision::Allow,
621 }
622 }
623 }
624 }
625 if !matched {
626 return Decision::Deny(format!(
627 "principal {:?} has no grant for {:?}",
628 principal.subject, access
629 ));
630 }
631 match view {
632 Some(filter) => Decision::AllowFiltered(filter),
633 None => Decision::Allow,
634 }
635 }
636}
637
638#[derive(Clone)]
648pub struct Guard {
649 identity: Arc<dyn IdentityProvider>,
650 authorizer: Arc<dyn Authorizer>,
651 allow_anonymous: bool,
652}
653
654impl Guard {
655 #[must_use]
657 pub fn new(identity: Arc<dyn IdentityProvider>, authorizer: Arc<dyn Authorizer>) -> Self {
658 Self {
659 identity,
660 authorizer,
661 allow_anonymous: false,
662 }
663 }
664
665 #[must_use]
668 pub fn allow_anonymous(mut self, allow: bool) -> Self {
669 self.allow_anonymous = allow;
670 self
671 }
672
673 #[must_use]
676 pub fn disabled() -> Self {
677 Self::new(Arc::new(AllowAnonymous), Arc::new(AllowAll)).allow_anonymous(true)
678 }
679
680 #[must_use]
683 pub fn is_disabled(&self) -> bool {
684 self.identity.name() == "anonymous"
685 }
686
687 pub fn authenticate(&self, credentials: &Credentials) -> Result<Principal, AuthError> {
694 match self.identity.authenticate(credentials)? {
695 Some(principal) => Ok(principal),
696 None if self.allow_anonymous => Ok(Principal::anonymous()),
697 None if credentials.is_empty() => Err(AuthError::Unauthenticated(
698 "authentication is required".to_owned(),
699 )),
700 None => Err(AuthError::Unauthenticated(
701 "no provider accepted the presented credentials".to_owned(),
702 )),
703 }
704 }
705
706 pub async fn authorize(
714 &self,
715 principal: &Principal,
716 access: &Access,
717 ) -> Result<Option<Arc<dyn ViewFilter>>, AuthError> {
718 match self.authorizer.authorize(principal, access).await {
719 Decision::Allow => Ok(None),
720 Decision::AllowFiltered(filter) => Ok(Some(filter)),
721 Decision::Deny(reason) => Err(AuthError::Forbidden(reason)),
722 }
723 }
724}
725
726#[derive(Clone)]
732pub struct IdentityInterceptor {
733 guard: Guard,
734}
735
736impl IdentityInterceptor {
737 #[must_use]
739 pub fn new(guard: Guard) -> Self {
740 Self { guard }
741 }
742}
743
744impl Interceptor for IdentityInterceptor {
745 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
746 let credentials = Credentials::from_metadata(request.metadata());
747 let principal = self
748 .guard
749 .authenticate(&credentials)
750 .map_err(|error| error.to_status())?;
751 request.extensions_mut().insert(principal);
752 Ok(request)
753 }
754}
755
756#[must_use]
759pub fn principal<T>(request: &Request<T>) -> Principal {
760 request
761 .extensions()
762 .get::<Principal>()
763 .cloned()
764 .unwrap_or_else(Principal::anonymous)
765}
766
767#[cfg(test)]
768mod tests {
769 use super::*;
770
771 fn creds(bearer: &str) -> Credentials {
772 Credentials {
773 bearer: Some(bearer.to_owned()),
774 client_cert_subject: None,
775 }
776 }
777
778 #[tokio::test]
779 async fn disabled_guard_allows_anonymous_everything() {
780 let guard = Guard::disabled();
781 assert!(guard.is_disabled());
782 let principal = guard.authenticate(&Credentials::default()).unwrap();
783 assert!(principal.is_anonymous());
784 assert!(
785 guard
786 .authorize(&principal, &Access::on(Action::Transact, "people"))
787 .await
788 .unwrap()
789 .is_none()
790 );
791 }
792
793 #[test]
794 fn static_tokens_map_to_distinct_principals() {
795 let provider = StaticTokens::new()
796 .with(
797 "reader-secret",
798 Principal::new("static-token", "reader").with_role("reader"),
799 )
800 .with(
801 "writer-secret",
802 Principal::new("static-token", "writer").with_role("writer"),
803 );
804
805 let reader = provider.authenticate(&creds("reader-secret")).unwrap();
806 assert_eq!(reader.unwrap().subject, "reader");
807 let writer = provider.authenticate(&creds("writer-secret")).unwrap();
808 assert!(writer.unwrap().has_role("writer"));
809
810 assert!(
813 provider
814 .authenticate(&Credentials::default())
815 .unwrap()
816 .is_none()
817 );
818 assert!(provider.authenticate(&creds("bogus")).unwrap().is_none());
819 }
820
821 struct FakeJwt;
822 impl TokenVerifier for FakeJwt {
823 fn verify(&self, token: &str) -> Result<Principal, AuthError> {
824 let mut parts = token.split(':');
826 match (parts.next(), parts.next(), parts.next()) {
827 (Some("oidc"), Some(sub), Some(tenant)) => Ok(Principal::new("oidc", sub)
828 .with_role("reader")
829 .with_claim("tenant", tenant)),
830 _ => Err(AuthError::Unauthenticated("bad token".to_owned())),
831 }
832 }
833 }
834
835 #[test]
836 fn external_token_verifier_seam_produces_principal() {
837 let provider = ExternalTokens::new("oidc", FakeJwt);
838 let principal = provider
839 .authenticate(&creds("oidc:alice:acme"))
840 .unwrap()
841 .unwrap();
842 assert_eq!(principal.provider, "oidc");
843 assert_eq!(principal.claim("tenant"), Some("acme"));
844 assert!(matches!(
845 provider.authenticate(&creds("garbage")),
846 Err(AuthError::Unauthenticated(_))
847 ));
848 }
849
850 #[test]
851 fn composite_provider_tries_in_order_and_abstains() {
852 let statics = StaticTokens::new().with(
853 "svc-secret",
854 Principal::new("static-token", "svc").with_role("writer"),
855 );
856 let provider = CompositeProvider::new(vec![
857 Arc::new(statics),
858 Arc::new(ExternalTokens::new("oidc", FakeJwt)),
859 ]);
860
861 assert_eq!(
863 provider
864 .authenticate(&creds("svc-secret"))
865 .unwrap()
866 .unwrap()
867 .subject,
868 "svc"
869 );
870 assert_eq!(
872 provider
873 .authenticate(&creds("oidc:bob:beta"))
874 .unwrap()
875 .unwrap()
876 .provider,
877 "oidc"
878 );
879 assert!(
881 provider
882 .authenticate(&Credentials::default())
883 .unwrap()
884 .is_none()
885 );
886
887 let strict = Guard::new(Arc::new(provider.clone()), Arc::new(AllowAll));
890 assert!(matches!(
891 strict.authenticate(&Credentials::default()),
892 Err(AuthError::Unauthenticated(_))
893 ));
894 let open = Guard::new(Arc::new(provider), Arc::new(AllowAll)).allow_anonymous(true);
895 assert!(
896 open.authenticate(&Credentials::default())
897 .unwrap()
898 .is_anonymous()
899 );
900 }
901
902 fn sample_policy() -> PolicyAuthorizer {
903 PolicyAuthorizer::new()
904 .grant("reader", Grant::new([ActionClass::Read], ["people"]))
905 .grant(
906 "writer",
907 Grant::new([ActionClass::Read, ActionClass::Write], ["people"]),
908 )
909 .grant(
910 "admin",
911 Grant::new([ActionClass::Admin], Vec::<String>::new()),
912 )
913 }
914
915 #[tokio::test]
916 async fn policy_authorizer_enforces_actions_and_databases() {
917 let policy = sample_policy();
918 let reader = Principal::new("t", "r").with_role("reader");
919 let writer = Principal::new("t", "w").with_role("writer");
920 let admin = Principal::new("t", "a").with_role("admin");
921
922 assert!(matches!(
924 policy
925 .authorize(&reader, &Access::on(Action::Query, "people"))
926 .await,
927 Decision::Allow
928 ));
929 assert!(matches!(
930 policy
931 .authorize(&reader, &Access::on(Action::Transact, "people"))
932 .await,
933 Decision::Deny(_)
934 ));
935 assert!(matches!(
936 policy
937 .authorize(&reader, &Access::on(Action::Query, "secrets"))
938 .await,
939 Decision::Deny(_)
940 ));
941
942 assert!(matches!(
944 policy
945 .authorize(&writer, &Access::on(Action::Transact, "people"))
946 .await,
947 Decision::Allow
948 ));
949 assert!(matches!(
950 policy
951 .authorize(&admin, &Access::catalog(Action::CreateDatabase))
952 .await,
953 Decision::Allow
954 ));
955 assert!(matches!(
957 policy
958 .authorize(&admin, &Access::on(Action::Query, "people"))
959 .await,
960 Decision::Deny(_)
961 ));
962 }
963
964 #[tokio::test]
965 async fn per_principal_view_filter_gives_different_views() {
966 let acme_view: Arc<dyn ViewFilter> = Arc::new(AttributeAllowlist::new([
968 ":person/name",
969 ":person/acme-note",
970 ]));
971 let beta_view: Arc<dyn ViewFilter> = Arc::new(AttributeAllowlist::new([
972 ":person/name",
973 ":person/beta-note",
974 ]));
975 let policy = PolicyAuthorizer::new()
976 .grant(
977 "acme",
978 Grant::new([ActionClass::Read], ["people"]).with_view(acme_view),
979 )
980 .grant(
981 "beta",
982 Grant::new([ActionClass::Read], ["people"]).with_view(beta_view),
983 );
984 let guard = Guard::new(Arc::new(AllowAnonymous), Arc::new(policy));
985
986 let acme = Principal::new("oidc", "alice").with_role("acme");
987 let beta = Principal::new("oidc", "bob").with_role("beta");
988 let acme_filter = guard
989 .authorize(&acme, &Access::on(Action::Query, "people"))
990 .await
991 .unwrap()
992 .expect("acme is filtered");
993 let beta_filter = guard
994 .authorize(&beta, &Access::on(Action::Query, "people"))
995 .await
996 .unwrap()
997 .expect("beta is filtered");
998
999 assert!(acme_filter.attribute_visible(":person/name"));
1000 assert!(acme_filter.attribute_visible(":person/acme-note"));
1001 assert!(!acme_filter.attribute_visible(":person/beta-note"));
1002
1003 assert!(beta_filter.attribute_visible(":person/beta-note"));
1004 assert!(!beta_filter.attribute_visible(":person/acme-note"));
1005 }
1006
1007 #[test]
1008 fn interceptor_attaches_principal_to_extensions() {
1009 let guard = Guard::new(
1010 Arc::new(StaticTokens::new().with(
1011 "reader-secret",
1012 Principal::new("static-token", "reader").with_role("reader"),
1013 )),
1014 Arc::new(AllowAll),
1015 );
1016 let mut interceptor = IdentityInterceptor::new(guard);
1017
1018 let mut request = Request::new(());
1019 request
1020 .metadata_mut()
1021 .insert("authorization", "Bearer reader-secret".parse().unwrap());
1022 let request = interceptor.call(request).unwrap();
1023 assert_eq!(principal(&request).subject, "reader");
1024
1025 let mut bad = Request::new(());
1027 bad.metadata_mut()
1028 .insert("authorization", "Bearer nope".parse().unwrap());
1029 assert_eq!(
1030 interceptor.call(bad).unwrap_err().code(),
1031 tonic::Code::Unauthenticated
1032 );
1033 }
1034
1035 #[tokio::test]
1036 async fn one_guard_serves_two_tenants_with_different_authority() {
1037 let provider = CompositeProvider::new(vec![Arc::new(
1039 StaticTokens::new()
1040 .with(
1041 "acme-writer",
1042 Principal::new("static-token", "acme-svc").with_role("acme"),
1043 )
1044 .with(
1045 "beta-reader",
1046 Principal::new("static-token", "beta-user").with_role("beta"),
1047 ),
1048 )]);
1049 let policy = PolicyAuthorizer::new()
1050 .grant(
1051 "acme",
1052 Grant::new([ActionClass::Read, ActionClass::Write], ["acme-db"]),
1053 )
1054 .grant("beta", Grant::new([ActionClass::Read], ["beta-db"]));
1055 let guard = Guard::new(Arc::new(provider), Arc::new(policy));
1056
1057 let acme = guard.authenticate(&creds("acme-writer")).unwrap();
1058 let beta = guard.authenticate(&creds("beta-reader")).unwrap();
1059
1060 assert!(
1062 guard
1063 .authorize(&acme, &Access::on(Action::Transact, "acme-db"))
1064 .await
1065 .is_ok()
1066 );
1067 assert!(
1068 guard
1069 .authorize(&beta, &Access::on(Action::Query, "acme-db"))
1070 .await
1071 .is_err()
1072 );
1073 assert!(
1074 guard
1075 .authorize(&beta, &Access::on(Action::Query, "beta-db"))
1076 .await
1077 .is_ok()
1078 );
1079 assert!(
1081 guard
1082 .authorize(&beta, &Access::on(Action::Transact, "beta-db"))
1083 .await
1084 .is_err()
1085 );
1086 }
1087
1088 struct FakeOracle {
1092 allowed: BTreeSet<(String, Action)>,
1093 }
1094
1095 #[tonic::async_trait]
1096 impl Authorizer for FakeOracle {
1097 async fn authorize(&self, principal: &Principal, access: &Access) -> Decision {
1098 tokio::task::yield_now().await;
1100 let key = (principal.subject.clone(), access.action);
1101 if self.allowed.contains(&key) {
1102 Decision::Allow
1103 } else {
1104 Decision::Deny("oracle check failed".to_owned())
1105 }
1106 }
1107 }
1108
1109 #[tokio::test]
1110 async fn external_async_oracle_authorizer() {
1111 let oracle = FakeOracle {
1112 allowed: [("alice".to_owned(), Action::Query)].into_iter().collect(),
1113 };
1114 let guard = Guard::new(Arc::new(AllowAnonymous), Arc::new(oracle));
1115 let alice = Principal::new("oidc", "alice");
1116 let bob = Principal::new("oidc", "bob");
1117
1118 assert!(
1119 guard
1120 .authorize(&alice, &Access::on(Action::Query, "people"))
1121 .await
1122 .is_ok()
1123 );
1124 assert!(
1125 guard
1126 .authorize(&alice, &Access::on(Action::Transact, "people"))
1127 .await
1128 .is_err()
1129 );
1130 assert!(
1131 guard
1132 .authorize(&bob, &Access::on(Action::Query, "people"))
1133 .await
1134 .is_err()
1135 );
1136 }
1137}