Skip to content

Commit 86e8ecb

Browse files
pashabitzConvex, Inc.
authored andcommitted
CX-6295 backend access token authorization wiring (#25236)
GitOrigin-RevId: 7832cb488329e89c9eab5dfaa10827ebc89daf42
1 parent 584295e commit 86e8ecb

File tree

11 files changed

+130
-2
lines changed

11 files changed

+130
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/application/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use std::{
2020

2121
use anyhow::Context;
2222
use authentication::{
23+
application_auth::ApplicationAuth,
2324
validate_id_token,
2425
Auth0IdToken,
2526
};
@@ -411,6 +412,7 @@ pub struct Application<RT: Runtime> {
411412
log_visibility: Arc<dyn LogVisibility<RT>>,
412413
module_cache: ModuleCache<RT>,
413414
system_env_var_names: HashSet<EnvVarName>,
415+
app_auth: Arc<ApplicationAuth>,
414416
}
415417

416418
impl<RT: Runtime> Clone for Application<RT> {
@@ -443,6 +445,7 @@ impl<RT: Runtime> Clone for Application<RT> {
443445
log_visibility: self.log_visibility.clone(),
444446
module_cache: self.module_cache.clone(),
445447
system_env_var_names: self.system_env_var_names.clone(),
448+
app_auth: self.app_auth.clone(),
446449
}
447450
}
448451
}
@@ -472,6 +475,7 @@ impl<RT: Runtime> Application<RT> {
472475
log_visibility: Arc<dyn LogVisibility<RT>>,
473476
snapshot_import_pause_client: PauseClient,
474477
scheduled_jobs_pause_client: PauseClient,
478+
app_auth: Arc<ApplicationAuth>,
475479
) -> anyhow::Result<Self> {
476480
let module_cache =
477481
ModuleCache::new(runtime.clone(), database.clone(), modules_storage.clone()).await;
@@ -600,6 +604,7 @@ impl<RT: Runtime> Application<RT> {
600604
log_visibility,
601605
module_cache,
602606
system_env_var_names: system_env_vars.into_keys().collect(),
607+
app_auth,
603608
})
604609
}
605610

@@ -718,6 +723,10 @@ impl<RT: Runtime> Application<RT> {
718723
self.database.latest_snapshot()
719724
}
720725

726+
pub fn app_auth(&self) -> Arc<ApplicationAuth> {
727+
self.app_auth.clone()
728+
}
729+
721730
pub async fn search_with_compiled_query(
722731
&self,
723732
index_id: IndexId,

crates/application/src/test_helpers.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ use std::{
77
};
88

99
use async_trait::async_trait;
10+
use authentication::{
11+
access_token_auth::NullAccessTokenAuth,
12+
application_auth::ApplicationAuth,
13+
};
1014
use cmd_util::env::config_test;
1115
use common::{
1216
bootstrap_model::index::database_index::IndexedFields,
@@ -238,6 +242,10 @@ impl<RT: Runtime> ApplicationTestExt<RT> for Application<RT> {
238242
Arc::new(AllowLogging),
239243
snapshot_import_pause_client,
240244
args.scheduled_jobs_pause_client,
245+
Arc::new(ApplicationAuth::new(
246+
kb.clone(),
247+
Arc::new(NullAccessTokenAuth),
248+
)),
241249
)
242250
.await?;
243251

crates/authentication/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ doctest = false
1010

1111
[dependencies]
1212
anyhow = { workspace = true }
13+
async-trait = { workspace = true }
1314
base64 = { workspace = true }
1415
biscuit = { workspace = true }
1516
chrono = { workspace = true }
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use async_trait::async_trait;
2+
use keybroker::Identity;
3+
4+
/// Logic to check authorization based on Access Token
5+
#[async_trait]
6+
pub trait AccessTokenAuth: Send + Sync {
7+
async fn is_authorized(
8+
&self,
9+
instance_name: &str,
10+
access_token: &str,
11+
) -> anyhow::Result<Identity>;
12+
}
13+
pub struct NullAccessTokenAuth;
14+
15+
#[async_trait]
16+
impl AccessTokenAuth for NullAccessTokenAuth {
17+
async fn is_authorized(
18+
&self,
19+
_instance_name: &str,
20+
_access_token: &str,
21+
) -> anyhow::Result<Identity> {
22+
anyhow::bail!("Access token authorization is not supported")
23+
}
24+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use std::sync::Arc;
2+
3+
use keybroker::{
4+
Identity,
5+
KeyBroker,
6+
};
7+
8+
use crate::access_token_auth::AccessTokenAuth;
9+
10+
pub struct ApplicationAuth {
11+
key_broker: KeyBroker,
12+
access_token_auth: Arc<dyn AccessTokenAuth>,
13+
}
14+
15+
// Encapsulates auth logic supporting both legacy Deploy Keys and new Convex
16+
// Access tokens
17+
impl ApplicationAuth {
18+
pub fn new(key_broker: KeyBroker, access_token_auth: Arc<dyn AccessTokenAuth>) -> Self {
19+
Self {
20+
key_broker,
21+
access_token_auth,
22+
}
23+
}
24+
25+
pub async fn check_key(
26+
&self,
27+
admin_key_or_access_token: String,
28+
instance_name: String,
29+
) -> anyhow::Result<Identity> {
30+
if admin_key_or_access_token.contains('|')
31+
|| self
32+
.key_broker
33+
.is_encrypted_admin_key(&admin_key_or_access_token)
34+
{
35+
// assume this is a legacy Deploy Key
36+
// This is either a pipe-delimited deployment specific key
37+
// or an encrypted admin key.
38+
// The latter is used by smoke tests.
39+
self.key_broker.check_admin_key(&admin_key_or_access_token)
40+
} else {
41+
// assume this is an Access Token
42+
// Access Tokens are base64 encoded strings and do not have pipes
43+
// in them
44+
self.access_token_auth
45+
.is_authorized(&instance_name, &admin_key_or_access_token)
46+
.await
47+
}
48+
}
49+
}

crates/authentication/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ use serde::{
4646
use sync_types::AuthenticationToken;
4747
use url::Url;
4848

49+
pub mod access_token_auth;
50+
pub mod application_auth;
51+
4952
/// Issuer for API access tokens
5053
pub static CONVEX_AUTH_URL: LazyLock<Url> =
5154
LazyLock::new(|| Url::parse("https://auth.convex.dev/").unwrap());

crates/common/src/http/fetch.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ impl FetchClient for StaticFetchClient {
185185

186186
pub enum InternalFetchPurpose {
187187
UsageTracking,
188+
AccessTokenAuth,
188189
}
189190

190191
#[cfg(test)]

crates/keybroker/src/broker.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,18 @@ impl AdminIdentity {
480480
key,
481481
})
482482
}
483+
484+
pub fn new_for_access_token(
485+
instance_name: String,
486+
member_id: MemberId,
487+
access_token: String,
488+
) -> Self {
489+
Self {
490+
instance_name,
491+
member_id,
492+
key: access_token,
493+
}
494+
}
483495
}
484496

485497
#[cfg(any(test, feature = "testing"))]
@@ -606,6 +618,16 @@ impl KeyBroker {
606618
)
607619
}
608620

621+
pub fn is_encrypted_admin_key(&self, key: &str) -> bool {
622+
let (_, encrypted_part) = split_admin_key(key)
623+
.map(|(name, key)| (Some(remove_type_prefix_from_instance_name(name)), key))
624+
.unwrap_or((None, key));
625+
let admin_key: Result<AdminKeyProto, _> = self
626+
.encryptor
627+
.decode_proto(ADMIN_KEY_VERSION, encrypted_part);
628+
admin_key.is_ok()
629+
}
630+
609631
pub fn check_admin_key(&self, key: &str) -> anyhow::Result<Identity> {
610632
let (instance_name, encrypted_part) = split_admin_key(key)
611633
.map(|(name, key)| (Some(remove_type_prefix_from_instance_name(name)), key))

crates/local_backend/src/deploy_config.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,11 @@ pub async fn push_config_handler(
390390
.into_iter()
391391
.map(|m| m.try_into())
392392
.collect::<anyhow::Result<Vec<_>>>()?;
393+
393394
let identity = application
394-
.key_broker()
395-
.check_admin_key(&config.admin_key)
395+
.app_auth()
396+
.check_key(config.admin_key, application.instance_name())
397+
.await
396398
.context("bad admin key error")?;
397399

398400
let udf_server_version = Version::parse(&config.udf_server_version).context(

0 commit comments

Comments
 (0)