mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-06 21:12:55 +00:00
[proxy] Cache GetEndpointAccessControl errors (#12571)
Related to https://github.com/neondatabase/cloud/issues/19353
This commit is contained in:
committed by
GitHub
parent
8e95455aef
commit
96bcfba79e
286
proxy/src/cache/project_info.rs
vendored
286
proxy/src/cache/project_info.rs
vendored
@@ -10,6 +10,7 @@ use tokio::time::Instant;
|
|||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::config::ProjectInfoCacheOptions;
|
use crate::config::ProjectInfoCacheOptions;
|
||||||
|
use crate::control_plane::messages::{ControlPlaneErrorMessage, Reason};
|
||||||
use crate::control_plane::{EndpointAccessControl, RoleAccessControl};
|
use crate::control_plane::{EndpointAccessControl, RoleAccessControl};
|
||||||
use crate::intern::{AccountIdInt, EndpointIdInt, ProjectIdInt, RoleNameInt};
|
use crate::intern::{AccountIdInt, EndpointIdInt, ProjectIdInt, RoleNameInt};
|
||||||
use crate::types::{EndpointId, RoleName};
|
use crate::types::{EndpointId, RoleName};
|
||||||
@@ -36,22 +37,37 @@ impl<T> Entry<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get(&self) -> Option<&T> {
|
pub(crate) fn get(&self) -> Option<&T> {
|
||||||
(self.expires_at > Instant::now()).then_some(&self.value)
|
(!self.is_expired()).then_some(&self.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_expired(&self) -> bool {
|
||||||
|
self.expires_at <= Instant::now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EndpointInfo {
|
struct EndpointInfo {
|
||||||
role_controls: HashMap<RoleNameInt, Entry<RoleAccessControl>>,
|
role_controls: HashMap<RoleNameInt, Entry<ControlPlaneResult<RoleAccessControl>>>,
|
||||||
controls: Option<Entry<EndpointAccessControl>>,
|
controls: Option<Entry<ControlPlaneResult<EndpointAccessControl>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ControlPlaneResult<T> = Result<T, Box<ControlPlaneErrorMessage>>;
|
||||||
|
|
||||||
impl EndpointInfo {
|
impl EndpointInfo {
|
||||||
pub(crate) fn get_role_secret(&self, role_name: RoleNameInt) -> Option<RoleAccessControl> {
|
pub(crate) fn get_role_secret_with_ttl(
|
||||||
self.role_controls.get(&role_name)?.get().cloned()
|
&self,
|
||||||
|
role_name: RoleNameInt,
|
||||||
|
) -> Option<(ControlPlaneResult<RoleAccessControl>, Duration)> {
|
||||||
|
let entry = self.role_controls.get(&role_name)?;
|
||||||
|
let ttl = entry.expires_at - Instant::now();
|
||||||
|
Some((entry.get()?.clone(), ttl))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_controls(&self) -> Option<EndpointAccessControl> {
|
pub(crate) fn get_controls_with_ttl(
|
||||||
self.controls.as_ref()?.get().cloned()
|
&self,
|
||||||
|
) -> Option<(ControlPlaneResult<EndpointAccessControl>, Duration)> {
|
||||||
|
let entry = self.controls.as_ref()?;
|
||||||
|
let ttl = entry.expires_at - Instant::now();
|
||||||
|
Some((entry.get()?.clone(), ttl))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn invalidate_endpoint(&mut self) {
|
pub(crate) fn invalidate_endpoint(&mut self) {
|
||||||
@@ -153,28 +169,28 @@ impl ProjectInfoCacheImpl {
|
|||||||
self.cache.get(&endpoint_id)
|
self.cache.get(&endpoint_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_role_secret(
|
pub(crate) fn get_role_secret_with_ttl(
|
||||||
&self,
|
&self,
|
||||||
endpoint_id: &EndpointId,
|
endpoint_id: &EndpointId,
|
||||||
role_name: &RoleName,
|
role_name: &RoleName,
|
||||||
) -> Option<RoleAccessControl> {
|
) -> Option<(ControlPlaneResult<RoleAccessControl>, Duration)> {
|
||||||
let role_name = RoleNameInt::get(role_name)?;
|
let role_name = RoleNameInt::get(role_name)?;
|
||||||
let endpoint_info = self.get_endpoint_cache(endpoint_id)?;
|
let endpoint_info = self.get_endpoint_cache(endpoint_id)?;
|
||||||
endpoint_info.get_role_secret(role_name)
|
endpoint_info.get_role_secret_with_ttl(role_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_endpoint_access(
|
pub(crate) fn get_endpoint_access_with_ttl(
|
||||||
&self,
|
&self,
|
||||||
endpoint_id: &EndpointId,
|
endpoint_id: &EndpointId,
|
||||||
) -> Option<EndpointAccessControl> {
|
) -> Option<(ControlPlaneResult<EndpointAccessControl>, Duration)> {
|
||||||
let endpoint_info = self.get_endpoint_cache(endpoint_id)?;
|
let endpoint_info = self.get_endpoint_cache(endpoint_id)?;
|
||||||
endpoint_info.get_controls()
|
endpoint_info.get_controls_with_ttl()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_endpoint_access(
|
pub(crate) fn insert_endpoint_access(
|
||||||
&self,
|
&self,
|
||||||
account_id: Option<AccountIdInt>,
|
account_id: Option<AccountIdInt>,
|
||||||
project_id: ProjectIdInt,
|
project_id: Option<ProjectIdInt>,
|
||||||
endpoint_id: EndpointIdInt,
|
endpoint_id: EndpointIdInt,
|
||||||
role_name: RoleNameInt,
|
role_name: RoleNameInt,
|
||||||
controls: EndpointAccessControl,
|
controls: EndpointAccessControl,
|
||||||
@@ -183,26 +199,89 @@ impl ProjectInfoCacheImpl {
|
|||||||
if let Some(account_id) = account_id {
|
if let Some(account_id) = account_id {
|
||||||
self.insert_account2endpoint(account_id, endpoint_id);
|
self.insert_account2endpoint(account_id, endpoint_id);
|
||||||
}
|
}
|
||||||
self.insert_project2endpoint(project_id, endpoint_id);
|
if let Some(project_id) = project_id {
|
||||||
|
self.insert_project2endpoint(project_id, endpoint_id);
|
||||||
|
}
|
||||||
|
|
||||||
if self.cache.len() >= self.config.size {
|
if self.cache.len() >= self.config.size {
|
||||||
// If there are too many entries, wait until the next gc cycle.
|
// If there are too many entries, wait until the next gc cycle.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let controls = Entry::new(controls, self.config.ttl);
|
debug!(
|
||||||
let role_controls = Entry::new(role_controls, self.config.ttl);
|
key = &*endpoint_id,
|
||||||
|
"created a cache entry for endpoint access"
|
||||||
|
);
|
||||||
|
|
||||||
|
let controls = Some(Entry::new(Ok(controls), self.config.ttl));
|
||||||
|
let role_controls = Entry::new(Ok(role_controls), self.config.ttl);
|
||||||
|
|
||||||
match self.cache.entry(endpoint_id) {
|
match self.cache.entry(endpoint_id) {
|
||||||
clashmap::Entry::Vacant(e) => {
|
clashmap::Entry::Vacant(e) => {
|
||||||
e.insert(EndpointInfo {
|
e.insert(EndpointInfo {
|
||||||
role_controls: HashMap::from_iter([(role_name, role_controls)]),
|
role_controls: HashMap::from_iter([(role_name, role_controls)]),
|
||||||
controls: Some(controls),
|
controls,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
clashmap::Entry::Occupied(mut e) => {
|
clashmap::Entry::Occupied(mut e) => {
|
||||||
let ep = e.get_mut();
|
let ep = e.get_mut();
|
||||||
ep.controls = Some(controls);
|
ep.controls = controls;
|
||||||
|
if ep.role_controls.len() < self.config.max_roles {
|
||||||
|
ep.role_controls.insert(role_name, role_controls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert_endpoint_access_err(
|
||||||
|
&self,
|
||||||
|
endpoint_id: EndpointIdInt,
|
||||||
|
role_name: RoleNameInt,
|
||||||
|
msg: Box<ControlPlaneErrorMessage>,
|
||||||
|
ttl: Option<Duration>,
|
||||||
|
) {
|
||||||
|
if self.cache.len() >= self.config.size {
|
||||||
|
// If there are too many entries, wait until the next gc cycle.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
key = &*endpoint_id,
|
||||||
|
"created a cache entry for an endpoint access error"
|
||||||
|
);
|
||||||
|
|
||||||
|
let ttl = ttl.unwrap_or(self.config.ttl);
|
||||||
|
|
||||||
|
let controls = if msg.get_reason() == Reason::RoleProtected {
|
||||||
|
// RoleProtected is the only role-specific error that control plane can give us.
|
||||||
|
// If a given role name does not exist, it still returns a successful response,
|
||||||
|
// just with an empty secret.
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// We can cache all the other errors in EndpointInfo.controls,
|
||||||
|
// because they don't depend on what role name we pass to control plane.
|
||||||
|
Some(Entry::new(Err(msg.clone()), ttl))
|
||||||
|
};
|
||||||
|
|
||||||
|
let role_controls = Entry::new(Err(msg), ttl);
|
||||||
|
|
||||||
|
match self.cache.entry(endpoint_id) {
|
||||||
|
clashmap::Entry::Vacant(e) => {
|
||||||
|
e.insert(EndpointInfo {
|
||||||
|
role_controls: HashMap::from_iter([(role_name, role_controls)]),
|
||||||
|
controls,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
clashmap::Entry::Occupied(mut e) => {
|
||||||
|
let ep = e.get_mut();
|
||||||
|
if let Some(entry) = &ep.controls
|
||||||
|
&& !entry.is_expired()
|
||||||
|
&& entry.value.is_ok()
|
||||||
|
{
|
||||||
|
// If we have cached non-expired, non-error controls, keep them.
|
||||||
|
} else {
|
||||||
|
ep.controls = controls;
|
||||||
|
}
|
||||||
if ep.role_controls.len() < self.config.max_roles {
|
if ep.role_controls.len() < self.config.max_roles {
|
||||||
ep.role_controls.insert(role_name, role_controls);
|
ep.role_controls.insert(role_name, role_controls);
|
||||||
}
|
}
|
||||||
@@ -245,7 +324,7 @@ impl ProjectInfoCacheImpl {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if role_controls.get().expires_at <= Instant::now() {
|
if role_controls.get().is_expired() {
|
||||||
role_controls.remove();
|
role_controls.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,13 +363,11 @@ impl ProjectInfoCacheImpl {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::control_plane::messages::EndpointRateLimitConfig;
|
use crate::control_plane::messages::{Details, EndpointRateLimitConfig, ErrorInfo, Status};
|
||||||
use crate::control_plane::{AccessBlockerFlags, AuthSecret};
|
use crate::control_plane::{AccessBlockerFlags, AuthSecret};
|
||||||
use crate::scram::ServerSecret;
|
use crate::scram::ServerSecret;
|
||||||
use crate::types::ProjectId;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_project_info_cache_settings() {
|
async fn test_project_info_cache_settings() {
|
||||||
@@ -301,9 +378,9 @@ mod tests {
|
|||||||
ttl: Duration::from_secs(1),
|
ttl: Duration::from_secs(1),
|
||||||
gc_interval: Duration::from_secs(600),
|
gc_interval: Duration::from_secs(600),
|
||||||
});
|
});
|
||||||
let project_id: ProjectId = "project".into();
|
let project_id: Option<ProjectIdInt> = Some(ProjectIdInt::from(&"project".into()));
|
||||||
let endpoint_id: EndpointId = "endpoint".into();
|
let endpoint_id: EndpointId = "endpoint".into();
|
||||||
let account_id: Option<AccountIdInt> = None;
|
let account_id = None;
|
||||||
|
|
||||||
let user1: RoleName = "user1".into();
|
let user1: RoleName = "user1".into();
|
||||||
let user2: RoleName = "user2".into();
|
let user2: RoleName = "user2".into();
|
||||||
@@ -316,7 +393,7 @@ mod tests {
|
|||||||
|
|
||||||
cache.insert_endpoint_access(
|
cache.insert_endpoint_access(
|
||||||
account_id,
|
account_id,
|
||||||
(&project_id).into(),
|
project_id,
|
||||||
(&endpoint_id).into(),
|
(&endpoint_id).into(),
|
||||||
(&user1).into(),
|
(&user1).into(),
|
||||||
EndpointAccessControl {
|
EndpointAccessControl {
|
||||||
@@ -332,7 +409,7 @@ mod tests {
|
|||||||
|
|
||||||
cache.insert_endpoint_access(
|
cache.insert_endpoint_access(
|
||||||
account_id,
|
account_id,
|
||||||
(&project_id).into(),
|
project_id,
|
||||||
(&endpoint_id).into(),
|
(&endpoint_id).into(),
|
||||||
(&user2).into(),
|
(&user2).into(),
|
||||||
EndpointAccessControl {
|
EndpointAccessControl {
|
||||||
@@ -346,11 +423,17 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
|
let (cached, ttl) = cache
|
||||||
assert_eq!(cached.secret, secret1);
|
.get_role_secret_with_ttl(&endpoint_id, &user1)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cached.unwrap().secret, secret1);
|
||||||
|
assert_eq!(ttl, cache.config.ttl);
|
||||||
|
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user2).unwrap();
|
let (cached, ttl) = cache
|
||||||
assert_eq!(cached.secret, secret2);
|
.get_role_secret_with_ttl(&endpoint_id, &user2)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cached.unwrap().secret, secret2);
|
||||||
|
assert_eq!(ttl, cache.config.ttl);
|
||||||
|
|
||||||
// Shouldn't add more than 2 roles.
|
// Shouldn't add more than 2 roles.
|
||||||
let user3: RoleName = "user3".into();
|
let user3: RoleName = "user3".into();
|
||||||
@@ -358,7 +441,7 @@ mod tests {
|
|||||||
|
|
||||||
cache.insert_endpoint_access(
|
cache.insert_endpoint_access(
|
||||||
account_id,
|
account_id,
|
||||||
(&project_id).into(),
|
project_id,
|
||||||
(&endpoint_id).into(),
|
(&endpoint_id).into(),
|
||||||
(&user3).into(),
|
(&user3).into(),
|
||||||
EndpointAccessControl {
|
EndpointAccessControl {
|
||||||
@@ -372,17 +455,144 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(cache.get_role_secret(&endpoint_id, &user3).is_none());
|
assert!(
|
||||||
|
cache
|
||||||
|
.get_role_secret_with_ttl(&endpoint_id, &user3)
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
let cached = cache.get_endpoint_access(&endpoint_id).unwrap();
|
let cached = cache
|
||||||
|
.get_endpoint_access_with_ttl(&endpoint_id)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.unwrap();
|
||||||
assert_eq!(cached.allowed_ips, allowed_ips);
|
assert_eq!(cached.allowed_ips, allowed_ips);
|
||||||
|
|
||||||
tokio::time::advance(Duration::from_secs(2)).await;
|
tokio::time::advance(Duration::from_secs(2)).await;
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user1);
|
let cached = cache.get_role_secret_with_ttl(&endpoint_id, &user1);
|
||||||
assert!(cached.is_none());
|
assert!(cached.is_none());
|
||||||
let cached = cache.get_role_secret(&endpoint_id, &user2);
|
let cached = cache.get_role_secret_with_ttl(&endpoint_id, &user2);
|
||||||
assert!(cached.is_none());
|
assert!(cached.is_none());
|
||||||
let cached = cache.get_endpoint_access(&endpoint_id);
|
let cached = cache.get_endpoint_access_with_ttl(&endpoint_id);
|
||||||
assert!(cached.is_none());
|
assert!(cached.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_caching_project_info_errors() {
|
||||||
|
let cache = ProjectInfoCacheImpl::new(ProjectInfoCacheOptions {
|
||||||
|
size: 10,
|
||||||
|
max_roles: 10,
|
||||||
|
ttl: Duration::from_secs(1),
|
||||||
|
gc_interval: Duration::from_secs(600),
|
||||||
|
});
|
||||||
|
let project_id = Some(ProjectIdInt::from(&"project".into()));
|
||||||
|
let endpoint_id: EndpointId = "endpoint".into();
|
||||||
|
let account_id = None;
|
||||||
|
|
||||||
|
let user1: RoleName = "user1".into();
|
||||||
|
let user2: RoleName = "user2".into();
|
||||||
|
let secret = Some(AuthSecret::Scram(ServerSecret::mock([1; 32])));
|
||||||
|
|
||||||
|
let role_msg = Box::new(ControlPlaneErrorMessage {
|
||||||
|
error: "role is protected and cannot be used for password-based authentication"
|
||||||
|
.to_owned()
|
||||||
|
.into_boxed_str(),
|
||||||
|
http_status_code: http::StatusCode::NOT_FOUND,
|
||||||
|
status: Some(Status {
|
||||||
|
code: "PERMISSION_DENIED".to_owned().into_boxed_str(),
|
||||||
|
message: "role is protected and cannot be used for password-based authentication"
|
||||||
|
.to_owned()
|
||||||
|
.into_boxed_str(),
|
||||||
|
details: Details {
|
||||||
|
error_info: Some(ErrorInfo {
|
||||||
|
reason: Reason::RoleProtected,
|
||||||
|
}),
|
||||||
|
retry_info: None,
|
||||||
|
user_facing_message: None,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let generic_msg = Box::new(ControlPlaneErrorMessage {
|
||||||
|
error: "oh noes".to_owned().into_boxed_str(),
|
||||||
|
http_status_code: http::StatusCode::NOT_FOUND,
|
||||||
|
status: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let get_role_secret = |endpoint_id, role_name| {
|
||||||
|
cache
|
||||||
|
.get_role_secret_with_ttl(endpoint_id, role_name)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
};
|
||||||
|
let get_endpoint_access =
|
||||||
|
|endpoint_id| cache.get_endpoint_access_with_ttl(endpoint_id).unwrap().0;
|
||||||
|
|
||||||
|
// stores role-specific errors only for get_role_secret
|
||||||
|
cache.insert_endpoint_access_err(
|
||||||
|
(&endpoint_id).into(),
|
||||||
|
(&user1).into(),
|
||||||
|
role_msg.clone(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_role_secret(&endpoint_id, &user1).unwrap_err().error,
|
||||||
|
role_msg.error
|
||||||
|
);
|
||||||
|
assert!(cache.get_endpoint_access_with_ttl(&endpoint_id).is_none());
|
||||||
|
|
||||||
|
// stores non-role specific errors for both get_role_secret and get_endpoint_access
|
||||||
|
cache.insert_endpoint_access_err(
|
||||||
|
(&endpoint_id).into(),
|
||||||
|
(&user1).into(),
|
||||||
|
generic_msg.clone(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_role_secret(&endpoint_id, &user1).unwrap_err().error,
|
||||||
|
generic_msg.error
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_endpoint_access(&endpoint_id).unwrap_err().error,
|
||||||
|
generic_msg.error
|
||||||
|
);
|
||||||
|
|
||||||
|
// error isn't returned for other roles in the same endpoint
|
||||||
|
assert!(
|
||||||
|
cache
|
||||||
|
.get_role_secret_with_ttl(&endpoint_id, &user2)
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
|
// success for a role does not overwrite errors for other roles
|
||||||
|
cache.insert_endpoint_access(
|
||||||
|
account_id,
|
||||||
|
project_id,
|
||||||
|
(&endpoint_id).into(),
|
||||||
|
(&user2).into(),
|
||||||
|
EndpointAccessControl {
|
||||||
|
allowed_ips: Arc::new(vec![]),
|
||||||
|
allowed_vpce: Arc::new(vec![]),
|
||||||
|
flags: AccessBlockerFlags::default(),
|
||||||
|
rate_limits: EndpointRateLimitConfig::default(),
|
||||||
|
},
|
||||||
|
RoleAccessControl {
|
||||||
|
secret: secret.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(get_role_secret(&endpoint_id, &user1).is_err());
|
||||||
|
assert!(get_role_secret(&endpoint_id, &user2).is_ok());
|
||||||
|
// ...but does clear the access control error
|
||||||
|
assert!(get_endpoint_access(&endpoint_id).is_ok());
|
||||||
|
|
||||||
|
// storing an error does not overwrite successful access control response
|
||||||
|
cache.insert_endpoint_access_err(
|
||||||
|
(&endpoint_id).into(),
|
||||||
|
(&user2).into(),
|
||||||
|
generic_msg.clone(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert!(get_role_secret(&endpoint_id, &user2).is_err());
|
||||||
|
assert!(get_endpoint_access(&endpoint_id).is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,66 @@ impl NeonControlPlaneClient {
|
|||||||
self.endpoint.url().as_str()
|
self.endpoint.url().as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_and_cache_auth_info<T>(
|
||||||
|
&self,
|
||||||
|
ctx: &RequestContext,
|
||||||
|
endpoint: &EndpointId,
|
||||||
|
role: &RoleName,
|
||||||
|
cache_key: &EndpointId,
|
||||||
|
extract: impl FnOnce(&EndpointAccessControl, &RoleAccessControl) -> T,
|
||||||
|
) -> Result<T, GetAuthInfoError> {
|
||||||
|
match self.do_get_auth_req(ctx, endpoint, role).await {
|
||||||
|
Ok(auth_info) => {
|
||||||
|
let control = EndpointAccessControl {
|
||||||
|
allowed_ips: Arc::new(auth_info.allowed_ips),
|
||||||
|
allowed_vpce: Arc::new(auth_info.allowed_vpc_endpoint_ids),
|
||||||
|
flags: auth_info.access_blocker_flags,
|
||||||
|
rate_limits: auth_info.rate_limits,
|
||||||
|
};
|
||||||
|
let role_control = RoleAccessControl {
|
||||||
|
secret: auth_info.secret,
|
||||||
|
};
|
||||||
|
let res = extract(&control, &role_control);
|
||||||
|
|
||||||
|
self.caches.project_info.insert_endpoint_access(
|
||||||
|
auth_info.account_id,
|
||||||
|
auth_info.project_id,
|
||||||
|
cache_key.into(),
|
||||||
|
role.into(),
|
||||||
|
control,
|
||||||
|
role_control,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(project_id) = auth_info.project_id {
|
||||||
|
ctx.set_project_id(project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
Err(err) => match err {
|
||||||
|
GetAuthInfoError::ApiError(ControlPlaneError::Message(ref msg)) => {
|
||||||
|
let retry_info = msg.status.as_ref().and_then(|s| s.details.retry_info);
|
||||||
|
|
||||||
|
// If we can retry this error, do not cache it,
|
||||||
|
// unless we were given a retry delay.
|
||||||
|
if msg.could_retry() && retry_info.is_none() {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.caches.project_info.insert_endpoint_access_err(
|
||||||
|
cache_key.into(),
|
||||||
|
role.into(),
|
||||||
|
msg.clone(),
|
||||||
|
retry_info.map(|r| Duration::from_millis(r.retry_delay_ms)),
|
||||||
|
);
|
||||||
|
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
err => Err(err),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn do_get_auth_req(
|
async fn do_get_auth_req(
|
||||||
&self,
|
&self,
|
||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
@@ -284,43 +344,34 @@ impl super::ControlPlaneApi for NeonControlPlaneClient {
|
|||||||
ctx: &RequestContext,
|
ctx: &RequestContext,
|
||||||
endpoint: &EndpointId,
|
endpoint: &EndpointId,
|
||||||
role: &RoleName,
|
role: &RoleName,
|
||||||
) -> Result<RoleAccessControl, crate::control_plane::errors::GetAuthInfoError> {
|
) -> Result<RoleAccessControl, GetAuthInfoError> {
|
||||||
let normalized_ep = &endpoint.normalize();
|
let key = endpoint.normalize();
|
||||||
if let Some(secret) = self
|
|
||||||
|
if let Some((role_control, ttl)) = self
|
||||||
.caches
|
.caches
|
||||||
.project_info
|
.project_info
|
||||||
.get_role_secret(normalized_ep, role)
|
.get_role_secret_with_ttl(&key, role)
|
||||||
{
|
{
|
||||||
return Ok(secret);
|
return match role_control {
|
||||||
|
Err(mut msg) => {
|
||||||
|
info!(key = &*key, "found cached get_role_access_control error");
|
||||||
|
|
||||||
|
// if retry_delay_ms is set change it to the remaining TTL
|
||||||
|
replace_retry_delay_ms(&mut msg, |_| ttl.as_millis() as u64);
|
||||||
|
|
||||||
|
Err(GetAuthInfoError::ApiError(ControlPlaneError::Message(msg)))
|
||||||
|
}
|
||||||
|
Ok(role_control) => {
|
||||||
|
debug!(key = &*key, "found cached role access control");
|
||||||
|
Ok(role_control)
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth_info = self.do_get_auth_req(ctx, endpoint, role).await?;
|
self.get_and_cache_auth_info(ctx, endpoint, role, &key, |_, role_control| {
|
||||||
|
role_control.clone()
|
||||||
let control = EndpointAccessControl {
|
})
|
||||||
allowed_ips: Arc::new(auth_info.allowed_ips),
|
.await
|
||||||
allowed_vpce: Arc::new(auth_info.allowed_vpc_endpoint_ids),
|
|
||||||
flags: auth_info.access_blocker_flags,
|
|
||||||
rate_limits: auth_info.rate_limits,
|
|
||||||
};
|
|
||||||
let role_control = RoleAccessControl {
|
|
||||||
secret: auth_info.secret,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(project_id) = auth_info.project_id {
|
|
||||||
let normalized_ep_int = normalized_ep.into();
|
|
||||||
|
|
||||||
self.caches.project_info.insert_endpoint_access(
|
|
||||||
auth_info.account_id,
|
|
||||||
project_id,
|
|
||||||
normalized_ep_int,
|
|
||||||
role.into(),
|
|
||||||
control,
|
|
||||||
role_control.clone(),
|
|
||||||
);
|
|
||||||
ctx.set_project_id(project_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(role_control)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
@@ -330,38 +381,30 @@ impl super::ControlPlaneApi for NeonControlPlaneClient {
|
|||||||
endpoint: &EndpointId,
|
endpoint: &EndpointId,
|
||||||
role: &RoleName,
|
role: &RoleName,
|
||||||
) -> Result<EndpointAccessControl, GetAuthInfoError> {
|
) -> Result<EndpointAccessControl, GetAuthInfoError> {
|
||||||
let normalized_ep = &endpoint.normalize();
|
let key = endpoint.normalize();
|
||||||
if let Some(control) = self.caches.project_info.get_endpoint_access(normalized_ep) {
|
|
||||||
return Ok(control);
|
if let Some((control, ttl)) = self.caches.project_info.get_endpoint_access_with_ttl(&key) {
|
||||||
|
return match control {
|
||||||
|
Err(mut msg) => {
|
||||||
|
info!(
|
||||||
|
key = &*key,
|
||||||
|
"found cached get_endpoint_access_control error"
|
||||||
|
);
|
||||||
|
|
||||||
|
// if retry_delay_ms is set change it to the remaining TTL
|
||||||
|
replace_retry_delay_ms(&mut msg, |_| ttl.as_millis() as u64);
|
||||||
|
|
||||||
|
Err(GetAuthInfoError::ApiError(ControlPlaneError::Message(msg)))
|
||||||
|
}
|
||||||
|
Ok(control) => {
|
||||||
|
debug!(key = &*key, "found cached endpoint access control");
|
||||||
|
Ok(control)
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth_info = self.do_get_auth_req(ctx, endpoint, role).await?;
|
self.get_and_cache_auth_info(ctx, endpoint, role, &key, |control, _| control.clone())
|
||||||
|
.await
|
||||||
let control = EndpointAccessControl {
|
|
||||||
allowed_ips: Arc::new(auth_info.allowed_ips),
|
|
||||||
allowed_vpce: Arc::new(auth_info.allowed_vpc_endpoint_ids),
|
|
||||||
flags: auth_info.access_blocker_flags,
|
|
||||||
rate_limits: auth_info.rate_limits,
|
|
||||||
};
|
|
||||||
let role_control = RoleAccessControl {
|
|
||||||
secret: auth_info.secret,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(project_id) = auth_info.project_id {
|
|
||||||
let normalized_ep_int = normalized_ep.into();
|
|
||||||
|
|
||||||
self.caches.project_info.insert_endpoint_access(
|
|
||||||
auth_info.account_id,
|
|
||||||
project_id,
|
|
||||||
normalized_ep_int,
|
|
||||||
role.into(),
|
|
||||||
control.clone(),
|
|
||||||
role_control,
|
|
||||||
);
|
|
||||||
ctx.set_project_id(project_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(control)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
@@ -390,13 +433,9 @@ impl super::ControlPlaneApi for NeonControlPlaneClient {
|
|||||||
info!(key = &*key, "found cached wake_compute error");
|
info!(key = &*key, "found cached wake_compute error");
|
||||||
|
|
||||||
// if retry_delay_ms is set, reduce it by the amount of time it spent in cache
|
// if retry_delay_ms is set, reduce it by the amount of time it spent in cache
|
||||||
if let Some(status) = &mut msg.status {
|
replace_retry_delay_ms(&mut msg, |delay| {
|
||||||
if let Some(retry_info) = &mut status.details.retry_info {
|
delay.saturating_sub(created_at.elapsed().as_millis() as u64)
|
||||||
retry_info.retry_delay_ms = retry_info
|
});
|
||||||
.retry_delay_ms
|
|
||||||
.saturating_sub(created_at.elapsed().as_millis() as u64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(WakeComputeError::ControlPlane(ControlPlaneError::Message(
|
Err(WakeComputeError::ControlPlane(ControlPlaneError::Message(
|
||||||
msg,
|
msg,
|
||||||
@@ -478,6 +517,14 @@ impl super::ControlPlaneApi for NeonControlPlaneClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replace_retry_delay_ms(msg: &mut ControlPlaneErrorMessage, f: impl FnOnce(u64) -> u64) {
|
||||||
|
if let Some(status) = &mut msg.status
|
||||||
|
&& let Some(retry_info) = &mut status.details.retry_info
|
||||||
|
{
|
||||||
|
retry_info.retry_delay_ms = f(retry_info.retry_delay_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse http response body, taking status code into account.
|
/// Parse http response body, taking status code into account.
|
||||||
fn parse_body<T: for<'a> serde::Deserialize<'a>>(
|
fn parse_body<T: for<'a> serde::Deserialize<'a>>(
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ impl ReportableError for ControlPlaneError {
|
|||||||
| Reason::EndpointNotFound
|
| Reason::EndpointNotFound
|
||||||
| Reason::EndpointDisabled
|
| Reason::EndpointDisabled
|
||||||
| Reason::BranchNotFound
|
| Reason::BranchNotFound
|
||||||
| Reason::InvalidEphemeralEndpointOptions => ErrorKind::User,
|
| Reason::WrongLsnOrTimestamp => ErrorKind::User,
|
||||||
|
|
||||||
Reason::RateLimitExceeded => ErrorKind::ServiceRateLimit,
|
Reason::RateLimitExceeded => ErrorKind::ServiceRateLimit,
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ pub(crate) struct ErrorInfo {
|
|||||||
// Schema could also have `metadata` field, but it's not structured. Skip it for now.
|
// Schema could also have `metadata` field, but it's not structured. Skip it for now.
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Default)]
|
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub(crate) enum Reason {
|
pub(crate) enum Reason {
|
||||||
/// RoleProtected indicates that the role is protected and the attempted operation is not permitted on protected roles.
|
/// RoleProtected indicates that the role is protected and the attempted operation is not permitted on protected roles.
|
||||||
#[serde(rename = "ROLE_PROTECTED")]
|
#[serde(rename = "ROLE_PROTECTED")]
|
||||||
@@ -133,9 +133,9 @@ pub(crate) enum Reason {
|
|||||||
/// or that the subject doesn't have enough permissions to access the requested branch.
|
/// or that the subject doesn't have enough permissions to access the requested branch.
|
||||||
#[serde(rename = "BRANCH_NOT_FOUND")]
|
#[serde(rename = "BRANCH_NOT_FOUND")]
|
||||||
BranchNotFound,
|
BranchNotFound,
|
||||||
/// InvalidEphemeralEndpointOptions indicates that the specified LSN or timestamp are wrong.
|
/// WrongLsnOrTimestamp indicates that the specified LSN or timestamp are wrong.
|
||||||
#[serde(rename = "INVALID_EPHEMERAL_OPTIONS")]
|
#[serde(rename = "WRONG_LSN_OR_TIMESTAMP")]
|
||||||
InvalidEphemeralEndpointOptions,
|
WrongLsnOrTimestamp,
|
||||||
/// RateLimitExceeded indicates that the rate limit for the operation has been exceeded.
|
/// RateLimitExceeded indicates that the rate limit for the operation has been exceeded.
|
||||||
#[serde(rename = "RATE_LIMIT_EXCEEDED")]
|
#[serde(rename = "RATE_LIMIT_EXCEEDED")]
|
||||||
RateLimitExceeded,
|
RateLimitExceeded,
|
||||||
@@ -205,7 +205,7 @@ impl Reason {
|
|||||||
| Reason::EndpointNotFound
|
| Reason::EndpointNotFound
|
||||||
| Reason::EndpointDisabled
|
| Reason::EndpointDisabled
|
||||||
| Reason::BranchNotFound
|
| Reason::BranchNotFound
|
||||||
| Reason::InvalidEphemeralEndpointOptions => false,
|
| Reason::WrongLsnOrTimestamp => false,
|
||||||
// we were asked to go away
|
// we were asked to go away
|
||||||
Reason::RateLimitExceeded
|
Reason::RateLimitExceeded
|
||||||
| Reason::NonDefaultBranchComputeTimeExceeded
|
| Reason::NonDefaultBranchComputeTimeExceeded
|
||||||
@@ -257,19 +257,19 @@ pub(crate) struct GetEndpointAccessControl {
|
|||||||
pub(crate) rate_limits: EndpointRateLimitConfig,
|
pub(crate) rate_limits: EndpointRateLimitConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Deserialize, Default)]
|
#[derive(Copy, Clone, Deserialize, Default, Debug)]
|
||||||
pub struct EndpointRateLimitConfig {
|
pub struct EndpointRateLimitConfig {
|
||||||
pub connection_attempts: ConnectionAttemptsLimit,
|
pub connection_attempts: ConnectionAttemptsLimit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Deserialize, Default)]
|
#[derive(Copy, Clone, Deserialize, Default, Debug)]
|
||||||
pub struct ConnectionAttemptsLimit {
|
pub struct ConnectionAttemptsLimit {
|
||||||
pub tcp: Option<LeakyBucketSetting>,
|
pub tcp: Option<LeakyBucketSetting>,
|
||||||
pub ws: Option<LeakyBucketSetting>,
|
pub ws: Option<LeakyBucketSetting>,
|
||||||
pub http: Option<LeakyBucketSetting>,
|
pub http: Option<LeakyBucketSetting>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Deserialize)]
|
#[derive(Copy, Clone, Deserialize, Debug)]
|
||||||
pub struct LeakyBucketSetting {
|
pub struct LeakyBucketSetting {
|
||||||
pub rps: f64,
|
pub rps: f64,
|
||||||
pub burst: f64,
|
pub burst: f64,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ impl NodeInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
#[derive(Copy, Clone, Default, Debug)]
|
||||||
pub(crate) struct AccessBlockerFlags {
|
pub(crate) struct AccessBlockerFlags {
|
||||||
pub public_access_blocked: bool,
|
pub public_access_blocked: bool,
|
||||||
pub vpc_access_blocked: bool,
|
pub vpc_access_blocked: bool,
|
||||||
@@ -92,12 +92,12 @@ pub(crate) type NodeInfoCache =
|
|||||||
TimedLru<EndpointCacheKey, Result<NodeInfo, Box<ControlPlaneErrorMessage>>>;
|
TimedLru<EndpointCacheKey, Result<NodeInfo, Box<ControlPlaneErrorMessage>>>;
|
||||||
pub(crate) type CachedNodeInfo = Cached<&'static NodeInfoCache, NodeInfo>;
|
pub(crate) type CachedNodeInfo = Cached<&'static NodeInfoCache, NodeInfo>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RoleAccessControl {
|
pub struct RoleAccessControl {
|
||||||
pub secret: Option<AuthSecret>,
|
pub secret: Option<AuthSecret>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct EndpointAccessControl {
|
pub struct EndpointAccessControl {
|
||||||
pub allowed_ips: Arc<Vec<IpPattern>>,
|
pub allowed_ips: Arc<Vec<IpPattern>>,
|
||||||
pub allowed_vpce: Arc<Vec<String>>,
|
pub allowed_vpce: Arc<Vec<String>>,
|
||||||
|
|||||||
Reference in New Issue
Block a user