From 8b336741f3dc59cd2952f05902c712bc38ec37f8 Mon Sep 17 00:00:00 2001 From: dawn Date: Thu, 15 Oct 2026 16:27:15 +0300 Subject: [PATCH] knot2: check who can push before accepting a push Signed-off-by: dawn --- knot2/crates/knot-acl/src/lib.rs | 2 +- knot2/crates/knot-acl/src/repos.rs | 39 ++++++++++++--- knot2/crates/knot-pack/src/lib.rs | 6 +-- knot2/crates/knot-receive/src/lib.rs | 15 +++--- knot2/crates/knot-ssh/src/exec.rs | 14 +++--- knot2/crates/knot-xrpc/src/branches.rs | 6 +-- knot2/crates/knot-xrpc/src/lfs.rs | 66 +++++++++++++------------- knot2/crates/knot-xrpc/src/lib.rs | 19 ++++---- knot2/crates/knot-xrpc/src/mint.rs | 2 +- knot2/crates/knot-xrpc/src/receive.rs | 37 +++++++-------- knot2/crates/knot-xrpc/src/tests.rs | 9 ++-- 11 files changed, 118 insertions(+), 97 deletions(-) diff --git a/knot2/crates/knot-acl/src/lib.rs b/knot2/crates/knot-acl/src/lib.rs index 89e038f16..947283094 100644 --- a/knot2/crates/knot-acl/src/lib.rs +++ b/knot2/crates/knot-acl/src/lib.rs @@ -10,7 +10,7 @@ pub use knot_types::Decision; mod repos; -pub use repos::{ReadGrant, Repos, push_grant, read_grant, read_grant_by_path}; +pub use repos::{PushGrant, ReadGrant, Repos, push_grant, read_grant, read_grant_by_path}; pub trait Acl { fn is_admin(&self, who: &AccountDid) -> bool; diff --git a/knot2/crates/knot-acl/src/repos.rs b/knot2/crates/knot-acl/src/repos.rs index 8b24b85c4..111a0348d 100644 --- a/knot2/crates/knot-acl/src/repos.rs +++ b/knot2/crates/knot-acl/src/repos.rs @@ -1,8 +1,8 @@ use std::path::{Path, PathBuf}; use knot_git::{GitError, Layout, Repo}; -use knot_index::{CollaborationConsent, Index, Resolved}; -use knot_types::{ClonePath, KnotId, ObjectFormat, OwnerDid, RepoDid}; +use knot_index::{Index, Resolved}; +use knot_types::{AccountDid, ClonePath, KnotId, ObjectFormat, OwnerDid, RepoDid}; use crate::{Acl, can_push}; @@ -54,6 +54,12 @@ impl Repos { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReadGrant(RepoDid); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PushGrant { + read: ReadGrant, + pusher: AccountDid, +} + impl ReadGrant { // only for the knot's own background work and never for a request pub fn internal(repo: RepoDid) -> Self { @@ -69,6 +75,20 @@ impl ReadGrant { } } +impl PushGrant { + pub fn read(&self) -> &ReadGrant { + &self.read + } + + pub fn repo(&self) -> &RepoDid { + self.read.repo() + } + + pub fn pusher(&self) -> &AccountDid { + &self.pusher + } +} + pub fn read_grant(index: &Index, repo: RepoDid) -> Resolved> { index .owner_of(&repo) @@ -87,8 +107,15 @@ pub fn read_grant_by_path( } } -pub fn push_grant(acl: &impl Acl, consent: &CollaborationConsent<'_>) -> Option { - can_push(acl, consent) - .is_allowed() - .then(|| ReadGrant(consent.repo().clone())) +pub fn push_grant( + acl: &impl Acl, + index: &Index, + read: &ReadGrant, + pusher: &AccountDid, +) -> Option { + let consent = index.collaboration_consent(read.repo(), pusher); + can_push(acl, &consent).is_allowed().then(|| PushGrant { + read: read.clone(), + pusher: pusher.clone(), + }) } diff --git a/knot2/crates/knot-pack/src/lib.rs b/knot2/crates/knot-pack/src/lib.rs index 23410f439..fd088fc56 100644 --- a/knot2/crates/knot-pack/src/lib.rs +++ b/knot2/crates/knot-pack/src/lib.rs @@ -328,7 +328,7 @@ pub use knot_edge::SocketPeer; pub trait ReceiveAdvertiser: Send + Sync + 'static { fn advertise( &self, - repo: RepoDid, + grant: ReadGrant, peer: knot_edge::SocketPeer, headers: HeaderMap, ) -> std::pin::Pin + Send + '_>>; @@ -577,9 +577,7 @@ async fn read_advertisement( )) } Some("git-receive-pack") => match &state.receive { - Some(advertiser) => Ok(advertiser - .advertise(grant.into_repo(), peer, headers.clone()) - .await), + Some(advertiser) => Ok(advertiser.advertise(grant, peer, headers.clone()).await), None => Err(PackError::PushOverSsh), }, _ => Err(PackError::UnsupportedService), diff --git a/knot2/crates/knot-receive/src/lib.rs b/knot2/crates/knot-receive/src/lib.rs index af73060e9..536324256 100644 --- a/knot2/crates/knot-receive/src/lib.rs +++ b/knot2/crates/knot-receive/src/lib.rs @@ -46,7 +46,7 @@ use std::cell::RefCell; use std::sync::Arc; use std::time::Duration; -use knot_acl::{ReadGrant, Repos}; +use knot_acl::{PushGrant, Repos}; use knot_atproto::Atproto; use knot_cob::CobHome; use knot_git::{RefUpdate, Repo}; @@ -86,11 +86,10 @@ pub trait RefSurface: Send + Sync { pub struct Push<'a, H: HttpTransport, C: Clock> { pub repos: &'a Repos, - pub grant: &'a ReadGrant, + pub grant: &'a PushGrant, pub received: ReceivedPack, pub limits: PackLimits, pub knot_actor: ActorId, - pub committer: AccountDid, pub index: &'a Index, pub atproto: &'a Atproto, pub resolve_slots: &'a ResolveSlots, @@ -108,7 +107,6 @@ pub async fn land(push: Push<'_, H, C>) -> Result(push: Push<'_, H, C>) -> Result Result<(Repo, ReceiveOutcome, Applied), PackError> { @@ -184,7 +183,7 @@ pub async fn land(push: Push<'_, H, C>) -> Result(push: Push<'_, H, C>) -> Result { - project.project(&repo, &did, &applied, &committer, &push_options) - } + Some(project) => project.project(&repo, &did, &applied, &pusher, &push_options), None => Vec::new(), }; (messages.collect::>(), replies) diff --git a/knot2/crates/knot-ssh/src/exec.rs b/knot2/crates/knot-ssh/src/exec.rs index a7b411a02..6daba67cb 100644 --- a/knot2/crates/knot-ssh/src/exec.rs +++ b/knot2/crates/knot-ssh/src/exec.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use futures::StreamExt; -use knot_acl::{KnotAcl, ReadGrant, push_grant, read_grant, read_grant_by_path}; +use knot_acl::{KnotAcl, PushGrant, ReadGrant, push_grant, read_grant, read_grant_by_path}; use knot_index::Resolved; use knot_lfs::TransferOp; use knot_pack::{PackError, PackLimits, RepoLookup}; @@ -642,8 +642,8 @@ async fn serve_receive( return; } - let (committer, pushing) = match authorize_push(&state, &credential, &grant, peer).await { - PushAuth::Allowed(did, pushing) => (did, pushing), + let pushing = match authorize_push(&state, &credential, &grant, peer).await { + PushAuth::Allowed(pushing) => pushing, PushAuth::Refused { reason, message } => { tracing::warn!(repo = repo_did.as_str(), reason, "ssh push denied"); return fail(channel, &message).await; @@ -703,7 +703,6 @@ async fn serve_receive( received: body, limits: state.limits, knot_actor: state.knot_actor.clone(), - committer, index: &state.index, atproto: &state.atproto, resolve_slots: &state.slots.resolve, @@ -780,7 +779,7 @@ enum PusherLookup { } enum PushAuth { - Allowed(AccountDid, ReadGrant), + Allowed(PushGrant), Refused { reason: &'static str, message: String, @@ -801,10 +800,9 @@ async fn authorize_push( let repo = grant.repo(); match resolve_pusher(state, credential, repo, peer).await { PusherLookup::Matched(did) => { - let consent = state.index.collaboration_consent(repo, &did); let acl = KnotAcl::new(&state.admins, state.admission, &state.index); - match push_grant(&acl, &consent) { - Some(pushing) => PushAuth::Allowed(did, pushing), + match push_grant(&acl, &state.index, grant, &did) { + Some(pushing) => PushAuth::Allowed(pushing), None => PushAuth::Refused { reason: "unauthorized", message: state.catalog.ssh.push_denied.text(), diff --git a/knot2/crates/knot-xrpc/src/branches.rs b/knot2/crates/knot-xrpc/src/branches.rs index 3c87b8527..d6ae44ea8 100644 --- a/knot2/crates/knot-xrpc/src/branches.rs +++ b/knot2/crates/knot-xrpc/src/branches.rs @@ -100,11 +100,11 @@ pub(crate) async fn delete_branch( let actor = state.authenticate_write(&headers, &method).await?; let input: DeleteBranchInput = decode(&body)?; let hosted = require_hosted(&state, input.repo)?; - let pushing = crate::authorize_push(&state, &actor, hosted.repo(), BRANCH_DENIED).await?; + let pushing = crate::authorize_push(&state, &actor, &hosted, BRANCH_DENIED).await?; let refname = input.branch.head_ref(); let repos = state.repos.clone(); - let target = pushing.clone(); + let target = pushing.read().clone(); let update = run_blocking(move || { let refs = RepoRefs::shared(repos.open(&target)?); let store: &dyn RefStore = &refs; @@ -129,7 +129,7 @@ pub(crate) async fn delete_branch( }) .await?; - crate::refrecords::sync_after(&state, &pushing, update).await; + crate::refrecords::sync_after(&state, pushing.read(), update).await; Ok(ok_empty()) } diff --git a/knot2/crates/knot-xrpc/src/lfs.rs b/knot2/crates/knot-xrpc/src/lfs.rs index e0242c537..514b142e3 100644 --- a/knot2/crates/knot-xrpc/src/lfs.rs +++ b/knot2/crates/knot-xrpc/src/lfs.rs @@ -154,20 +154,17 @@ fn lfs_error(error: crate::XrpcError) -> Box { fn resolve_did( state: &XrpcState, segment: &crate::RepoDidSegment, -) -> Result> { - crate::resolve_repo_did(state, segment) - .map(ReadGrant::into_repo) - .map_err(lfs_error) +) -> Result> { + crate::resolve_repo_did(state, segment).map_err(lfs_error) } async fn resolve_named( state: &XrpcState, owner: &crate::OwnerSegment, name: &crate::RepoNameSegment, -) -> Result> { +) -> Result> { crate::resolve_repo_named(state, owner, name) .await - .map(ReadGrant::into_repo) .map_err(lfs_error) } @@ -201,13 +198,13 @@ async fn batch_named( headers: HeaderMap, body: Result, ) -> Response { - let repo = match resolve_named(&state, &did, &name).await { - Ok(repo) => repo, + let grant = match resolve_named(&state, &did, &name).await { + Ok(grant) => grant, Err(response) => return *response, }; serve_batch( &state, - repo, + grant, &format!("{}/{}", did.as_str(), name.as_str()), peer, &headers, @@ -223,21 +220,22 @@ async fn batch_did( headers: HeaderMap, body: Result, ) -> Response { - let repo = match resolve_did(&state, &did) { - Ok(repo) => repo, + let grant = match resolve_did(&state, &did) { + Ok(grant) => grant, Err(response) => return *response, }; - serve_batch(&state, repo, did.as_str(), peer, &headers, body).await + serve_batch(&state, grant, did.as_str(), peer, &headers, body).await } async fn serve_batch( state: &XrpcState, - repo: RepoDid, + grant: ReadGrant, path_prefix: &str, peer: SocketPeer, headers: &HeaderMap, body: Result, ) -> Response { + let repo = grant.repo(); let lfs = &state.lfs; let body = match body { Ok(body) => body, @@ -280,11 +278,11 @@ async fn serve_batch( let base = &state.knot_service_url; let objects: Result, Box> = match request.operation { BatchOperation::Upload => { - match crate::receive::authorized_pusher(state, peer, headers, &repo, PUSH_DENIED) + match crate::receive::authorized_pusher(state, peer, headers, &grant, PUSH_DENIED) .await .map_err(|error| Box::new(challenge(error))) { - Ok(_actor) => match probe_all(lfs, &repo, &request.objects).await { + Ok(_pushing) => match probe_all(lfs, repo, &request.objects).await { Ok(stored) => request .objects .iter() @@ -296,7 +294,7 @@ async fn serve_batch( Err(response) => return *response, } } - BatchOperation::Download => match probe_all(lfs, &repo, &request.objects).await { + BatchOperation::Download => match probe_all(lfs, repo, &request.objects).await { Ok(stored) => request .objects .iter() @@ -470,11 +468,11 @@ async fn object_named( Path(RepoObjectParams { did, name, oid }): Path, request: Request, ) -> Response { - let repo = match resolve_named(&state, &did, &name).await { - Ok(repo) => repo, + let grant = match resolve_named(&state, &did, &name).await { + Ok(grant) => grant, Err(response) => return *response, }; - serve_object(&state, repo, &oid, request).await + serve_object(&state, grant, &oid, request).await } async fn object_did( @@ -482,19 +480,20 @@ async fn object_did( Path(DidObjectParams { did, oid }): Path, request: Request, ) -> Response { - let repo = match resolve_did(&state, &did) { - Ok(repo) => repo, + let grant = match resolve_did(&state, &did) { + Ok(grant) => grant, Err(response) => return *response, }; - serve_object(&state, repo, &oid, request).await + serve_object(&state, grant, &oid, request).await } async fn serve_object( state: &XrpcState, - repo: RepoDid, + grant: ReadGrant, oid_raw: &OidSegment, mut request: Request, ) -> Response { + let repo = grant.repo(); let lfs = &state.lfs; let Ok(oid) = LfsOid::new(oid_raw.as_str()) else { return fail(StatusCode::NOT_FOUND, "object not found"); @@ -571,11 +570,11 @@ async fn object_upload_named( peer: SocketPeer, request: Request, ) -> Response { - let repo = match resolve_named(&state, &did, &name).await { - Ok(repo) => repo, + let grant = match resolve_named(&state, &did, &name).await { + Ok(grant) => grant, Err(response) => return *response, }; - serve_object_upload(&state, repo, &oid, peer, request).await + serve_object_upload(&state, grant, &oid, peer, request).await } async fn object_upload_did( @@ -584,26 +583,27 @@ async fn object_upload_did( peer: SocketPeer, request: Request, ) -> Response { - let repo = match resolve_did(&state, &did) { - Ok(repo) => repo, + let grant = match resolve_did(&state, &did) { + Ok(grant) => grant, Err(response) => return *response, }; - serve_object_upload(&state, repo, &oid, peer, request).await + serve_object_upload(&state, grant, &oid, peer, request).await } async fn serve_object_upload( state: &XrpcState, - repo: RepoDid, + grant: ReadGrant, oid_raw: &OidSegment, peer: SocketPeer, request: Request, ) -> Response { + let repo = grant.repo(); let lfs = &state.lfs; let Ok(oid) = LfsOid::new(oid_raw.as_str()) else { return fail(StatusCode::NOT_FOUND, "object not found"); }; let denied = - crate::receive::authorized_pusher(state, peer, request.headers(), &repo, PUSH_DENIED) + crate::receive::authorized_pusher(state, peer, request.headers(), &grant, PUSH_DENIED) .await .map_err(|error| Box::new(challenge(error))); if let Err(response) = denied { @@ -617,7 +617,7 @@ async fn serve_object_upload( }; let permit = match lfs.handle.admission.admit(size) { Ok(permit) => permit, - Err(error) => return store_fault(&repo, &oid, error), + Err(error) => return store_fault(repo, &oid, error), }; let store = Arc::clone(&lfs.handle.store); let target = repo.clone(); @@ -648,7 +648,7 @@ async fn serve_object_upload( ); StatusCode::OK.into_response() } - Ok(Err(error)) => store_fault(&repo, &oid, error), + Ok(Err(error)) => store_fault(repo, &oid, error), Err(_) => fail(StatusCode::INTERNAL_SERVER_ERROR, "upload task died"), } } diff --git a/knot2/crates/knot-xrpc/src/lib.rs b/knot2/crates/knot-xrpc/src/lib.rs index 3c29afdc4..087378748 100644 --- a/knot2/crates/knot-xrpc/src/lib.rs +++ b/knot2/crates/knot-xrpc/src/lib.rs @@ -73,7 +73,7 @@ use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::json; -use knot_acl::{ReadGrant, Repos}; +use knot_acl::{PushGrant, ReadGrant, Repos}; use knot_atproto::{Atproto, AtprotoError, ServiceJwt}; use crate::legacy_admin::LegacyAdminSecret; @@ -718,13 +718,13 @@ pub(crate) async fn current_member( pub(crate) async fn authorize_push( state: &XrpcState, actor: &AccountDid, - repo: &RepoDid, + read: &ReadGrant, denied: &str, -) -> Result { - project_collaborators(state, repo).await?; - let consent = state.index.collaboration_consent(repo, actor); +) -> Result { + project_collaborators(state, read.repo()).await?; let acl = knot_acl::KnotAcl::new(&state.admins, state.admission, &state.index); - knot_acl::push_grant(&acl, &consent).ok_or_else(|| XrpcError::forbidden(denied)) + knot_acl::push_grant(&acl, &state.index, read, actor) + .ok_or_else(|| XrpcError::forbidden(denied)) } pub(crate) async fn authorize_settings( @@ -756,15 +756,14 @@ pub(crate) async fn authenticate_and_authorize_push( state: &XrpcState, socket: SocketPeer, headers: &HeaderMap, - repo: &RepoDid, + read: &ReadGrant, denied: &str, -) -> Result<(AccountDid, ReadGrant), XrpcError> { +) -> Result { let peer = state.proxy_trust.client_peer(headers, socket.ip()); let guard = admit_pre_auth(state, peer)?; let actor = state.authenticate_push(headers).await?; guard.refund(); - let grant = authorize_push(state, &actor, repo, denied).await?; - Ok((actor, grant)) + authorize_push(state, &actor, read, denied).await } pub(crate) async fn run_blocking(task: F) -> Result diff --git a/knot2/crates/knot-xrpc/src/mint.rs b/knot2/crates/knot-xrpc/src/mint.rs index 9e3a49fbd..6e5917e52 100644 --- a/knot2/crates/knot-xrpc/src/mint.rs +++ b/knot2/crates/knot-xrpc/src/mint.rs @@ -51,7 +51,7 @@ pub(crate) async fn mint_token( let actor = state .authenticate_write_for_repo(&headers, &method, repo) .await?; - authorize_push(&state, &actor, repo, MINT_DENIED).await?; + authorize_push(&state, &actor, &hosted, MINT_DENIED).await?; let now = state.now(); let expires_at = RepoTokenExpiry::mint(exp, now) diff --git a/knot2/crates/knot-xrpc/src/receive.rs b/knot2/crates/knot-xrpc/src/receive.rs index 7c545a3b2..0dce16826 100644 --- a/knot2/crates/knot-xrpc/src/receive.rs +++ b/knot2/crates/knot-xrpc/src/receive.rs @@ -10,12 +10,12 @@ use axum::http::{HeaderMap, HeaderValue, StatusCode, header}; use axum::response::{IntoResponse, Response}; use axum::routing::post; use futures::TryStreamExt; -use knot_acl::ReadGrant; +use knot_acl::{PushGrant, ReadGrant}; use knot_messages::{ErrorKey, HttpMessages}; use knot_pack::{PackError, PackReceiver, ReceivedPack, SocketPeer}; use knot_receive::{Push, land}; use knot_runtime::{Clock, HttpTransport}; -use knot_types::{AccountDid, ActorId, ObjectFormat, RepoDid}; +use knot_types::{ActorId, ObjectFormat}; use tokio_util::io::{StreamReader, SyncIoBridge}; use crate::{XrpcError, XrpcState, authenticate_and_authorize_push, run_blocking}; @@ -47,33 +47,33 @@ struct ReceiveGate { impl knot_pack::ReceiveAdvertiser for ReceiveGate { fn advertise( &self, - repo: RepoDid, + grant: ReadGrant, peer: SocketPeer, headers: HeaderMap, ) -> Pin + Send + '_>> { - Box::pin(async move { serve_advertisement(&self.state, repo, peer, &headers).await }) + Box::pin(async move { serve_advertisement(&self.state, grant, peer, &headers).await }) } } async fn serve_advertisement( state: &Arc>, - repo: RepoDid, + grant: ReadGrant, peer: SocketPeer, headers: &HeaderMap, ) -> Response { let denied = state.catalog.http.push_denied.text(); - let pushing = match authorized_pusher(state, peer, headers, &repo, &denied) + let pushing = match authorized_pusher(state, peer, headers, &grant, &denied) .await .map_err(|error| Box::new(challenge(error))) { - Ok((_, pushing)) => pushing, + Ok(pushing) => pushing, Err(response) => return *response, }; let repos = state.repos.clone(); let missing = state.catalog.http.repo_not_found.text(); let advert = run_blocking(move || { let repo = repos - .open(&pushing) + .open(pushing.read()) .map_err(|_| XrpcError::not_found(missing))?; knot_pack::advertise_receive(&repo).map_err(map_pack) }) @@ -95,7 +95,7 @@ async fn receive_named( Ok(grant) => grant, Err(error) => return error.into_response(), }; - serve_receive(&state, grant.into_repo(), peer, &headers, body).await + serve_receive(&state, grant, peer, &headers, body).await } async fn receive_did( @@ -109,22 +109,22 @@ async fn receive_did( Ok(grant) => grant, Err(error) => return error.into_response(), }; - serve_receive(&state, grant.into_repo(), peer, &headers, body).await + serve_receive(&state, grant, peer, &headers, body).await } async fn serve_receive( state: &Arc>, - repo_did: RepoDid, + grant: ReadGrant, peer: SocketPeer, headers: &HeaderMap, body: Body, ) -> Response { let denied = state.catalog.http.push_denied.text(); - let (pusher, pushing) = match authorized_pusher(state, peer, headers, &repo_did, &denied) + let pushing = match authorized_pusher(state, peer, headers, &grant, &denied) .await .map_err(|error| Box::new(challenge(error))) { - Ok(authorized) => authorized, + Ok(pushing) => pushing, Err(response) => return *response, }; @@ -135,7 +135,7 @@ async fn serve_receive( let format = { let repos = state.repos.clone(); - let target = pushing.clone(); + let target = pushing.read().clone(); let missing = state.catalog.http.repo_not_found.text(); match run_blocking(move || { repos @@ -184,7 +184,6 @@ async fn serve_receive( received, limits: state.pack_limits, knot_actor, - committer: pusher.clone(), index: &state.index, atproto: &state.atproto, resolve_slots: &state.slots.resolve, @@ -200,7 +199,7 @@ async fn serve_receive( match landed { Ok(framed) => git_response(RESULT, framed), Err(error) => { - tracing::warn!(repo = repo_did.as_str(), %error, "HTTP receive-pack failed"); + tracing::warn!(repo = grant.repo().as_str(), %error, "HTTP receive-pack failed"); error.into_response() } } @@ -240,10 +239,10 @@ pub(crate) async fn authorized_pusher( state: &XrpcState, peer: SocketPeer, headers: &HeaderMap, - repo: &RepoDid, + read: &ReadGrant, denied: &str, -) -> Result<(AccountDid, ReadGrant), XrpcError> { - authenticate_and_authorize_push(state, peer, headers, repo, denied).await +) -> Result { + authenticate_and_authorize_push(state, peer, headers, read, denied).await } fn challenge(error: XrpcError) -> Response { diff --git a/knot2/crates/knot-xrpc/src/tests.rs b/knot2/crates/knot-xrpc/src/tests.rs index 8f24f5d24..99c9e2dac 100644 --- a/knot2/crates/knot-xrpc/src/tests.rs +++ b/knot2/crates/knot-xrpc/src/tests.rs @@ -5815,9 +5815,12 @@ mod grant_lists { async fn allowed(&self) -> bool { let state = &self.world.state; match self.repo() { - Some(repo) => crate::authorize_push(state, &self.subject, repo, DENIED) - .await - .map(drop), + Some(repo) => { + let read = knot_acl::ReadGrant::internal(repo.clone()); + crate::authorize_push(state, &self.subject, &read, DENIED) + .await + .map(drop) + } None => crate::authorize_create(state, &self.subject, DENIED).await, } .is_ok() -- knotmirror 0.0.1