Separate permission checks for Pageserver and Safekeeper

There will be different scopes for those two, so authorization code should be different.

The `check_permission` function is now not in the shared library. Its implementation
is very similar to the one which will be added for Safekeeper. In fact, we may reuse
the same existing root-like 'PageServerApi' scope, but I would prefer to have separate
root-like scopes for services.

Also, generate_management_token in tests is generate_pageserver_token now.
This commit is contained in:
Egor Suvorov
2022-08-11 03:44:44 +03:00
committed by Egor Suvorov
parent a406783098
commit 2ce5d8137d
12 changed files with 73 additions and 34 deletions

View File

@@ -7,7 +7,7 @@ use serde;
use std::fs;
use std::path::Path;
use anyhow::{bail, Result};
use anyhow::Result;
use jsonwebtoken::{
decode, encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation,
};
@@ -40,22 +40,6 @@ impl Claims {
}
}
pub fn check_permission(claims: &Claims, tenant_id: Option<TenantId>) -> Result<()> {
match (&claims.scope, tenant_id) {
(Scope::Tenant, None) => {
bail!("Attempt to access management api with tenant scope. Permission denied")
}
(Scope::Tenant, Some(tenant_id)) => {
if claims.tenant_id.unwrap() != tenant_id {
bail!("Tenant id mismatch. Permission denied")
}
Ok(())
}
(Scope::PageServerApi, None) => Ok(()), // access to management api for PageServerApi scope
(Scope::PageServerApi, Some(_)) => Ok(()), // access to tenant api using PageServerApi scope
}
}
pub struct JwtAuth {
decoding_key: DecodingKey,
validation: Validation,

View File

@@ -1,6 +1,5 @@
use crate::auth::{self, Claims, JwtAuth};
use crate::auth::{Claims, JwtAuth};
use crate::http::error;
use crate::id::TenantId;
use anyhow::anyhow;
use hyper::header::AUTHORIZATION;
use hyper::{header::CONTENT_TYPE, Body, Request, Response, Server};
@@ -144,10 +143,14 @@ pub fn auth_middleware<B: hyper::body::HttpBody + Send + Sync + 'static>(
})
}
pub fn check_permission(req: &Request<Body>, tenant_id: Option<TenantId>) -> Result<(), ApiError> {
pub fn check_permission_with(
req: &Request<Body>,
check_permission: impl Fn(&Claims) -> Result<(), anyhow::Error>,
) -> Result<(), ApiError> {
match req.context::<Claims>() {
Some(claims) => Ok(auth::check_permission(&claims, tenant_id)
.map_err(|err| ApiError::Forbidden(err.to_string()))?),
Some(claims) => {
Ok(check_permission(&claims).map_err(|err| ApiError::Forbidden(err.to_string()))?)
}
None => Ok(()), // claims is None because auth is disabled
}
}

19
pageserver/src/auth.rs Normal file
View File

@@ -0,0 +1,19 @@
use anyhow::{bail, Result};
use utils::auth::{Claims, Scope};
use utils::id::TenantId;
pub fn check_permission(claims: &Claims, tenant_id: Option<TenantId>) -> Result<()> {
match (&claims.scope, tenant_id) {
(Scope::Tenant, None) => {
bail!("Attempt to access management api with tenant scope. Permission denied")
}
(Scope::Tenant, Some(tenant_id)) => {
if claims.tenant_id.unwrap() != tenant_id {
bail!("Tenant id mismatch. Permission denied")
}
Ok(())
}
(Scope::PageServerApi, None) => Ok(()), // access to management api for PageServerApi scope
(Scope::PageServerApi, Some(_)) => Ok(()), // access to tenant api using PageServerApi scope
}
}

View File

@@ -21,7 +21,7 @@ use crate::{config::PageServerConf, tenant_mgr};
use utils::{
auth::JwtAuth,
http::{
endpoint::{self, attach_openapi_ui, auth_middleware, check_permission},
endpoint::{self, attach_openapi_ui, auth_middleware, check_permission_with},
error::{ApiError, HttpErrorBody},
json::{json_request, json_response},
request::parse_request_param,
@@ -79,6 +79,12 @@ fn get_config(request: &Request<Body>) -> &'static PageServerConf {
get_state(request).conf
}
fn check_permission(request: &Request<Body>, tenant_id: Option<TenantId>) -> Result<(), ApiError> {
check_permission_with(request, |claims| {
crate::auth::check_permission(claims, tenant_id)
})
}
// Helper function to construct a TimelineInfo struct for a timeline
async fn build_timeline_info(
state: &State,

View File

@@ -1,3 +1,4 @@
mod auth;
pub mod basebackup;
pub mod config;
pub mod http;

View File

@@ -31,7 +31,7 @@ use tokio_util::io::SyncIoBridge;
use tracing::*;
use utils::id::ConnectionId;
use utils::{
auth::{self, Claims, JwtAuth, Scope},
auth::{Claims, JwtAuth, Scope},
id::{TenantId, TimelineId},
lsn::Lsn,
postgres_backend::AuthType,
@@ -39,6 +39,7 @@ use utils::{
simple_rcu::RcuReadGuard,
};
use crate::auth::check_permission;
use crate::basebackup;
use crate::config::{PageServerConf, ProfilingConfig};
use crate::import_datadir::import_wal_from_tar;
@@ -670,7 +671,7 @@ impl PageServerHandler {
.claims
.as_ref()
.expect("claims presence already checked");
auth::check_permission(claims, tenant_id)
check_permission(claims, tenant_id)
}
}

18
safekeeper/src/auth.rs Normal file
View File

@@ -0,0 +1,18 @@
use anyhow::{bail, Result};
use utils::auth::{Claims, Scope};
use utils::id::TenantId;
pub fn check_permission(claims: &Claims, tenant_id: Option<TenantId>) -> Result<()> {
match (&claims.scope, tenant_id) {
(Scope::Tenant, None) => {
bail!("Attempt to access management api with tenant scope. Permission denied")
}
(Scope::Tenant, Some(tenant_id)) => {
if claims.tenant_id.unwrap() != tenant_id {
bail!("Tenant id mismatch. Permission denied")
}
Ok(())
}
(Scope::PageServerApi, _) => bail!("PageServerApi scope makes no sense for Safekeeper"),
}
}

View File

@@ -20,7 +20,7 @@ use etcd_broker::subscription_value::SkTimelineInfo;
use utils::{
auth::JwtAuth,
http::{
endpoint::{self, auth_middleware, check_permission},
endpoint::{self, auth_middleware, check_permission_with},
error::ApiError,
json::{json_request, json_response},
request::{ensure_no_body, parse_request_param},
@@ -103,6 +103,12 @@ struct TimelineStatus {
remote_consistent_lsn: Lsn,
}
fn check_permission(request: &Request<Body>, tenant_id: Option<TenantId>) -> Result<(), ApiError> {
check_permission_with(request, |claims| {
crate::auth::check_permission(claims, tenant_id)
})
}
/// Report info about timeline.
async fn timeline_status_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
let ttid = TenantTimelineId::new(

View File

@@ -12,6 +12,7 @@ use utils::{
logging::LogFormat,
};
mod auth;
pub mod broker;
pub mod control_file;
pub mod control_file_upgrade;

View File

@@ -432,7 +432,7 @@ class AuthKeys:
return token
def generate_management_token(self) -> str:
def generate_pageserver_token(self) -> str:
return self.generate_token(scope="pageserverapi")
def generate_tenant_token(self, tenant_id: TenantId) -> str:

View File

@@ -16,13 +16,13 @@ def test_pageserver_auth(neon_env_builder: NeonEnvBuilder):
invalid_tenant_token = env.auth_keys.generate_tenant_token(TenantId.generate())
invalid_tenant_http_client = env.pageserver.http_client(invalid_tenant_token)
management_token = env.auth_keys.generate_management_token()
management_http_client = env.pageserver.http_client(management_token)
pageserver_token = env.auth_keys.generate_pageserver_token()
pageserver_http_client = env.pageserver.http_client(pageserver_token)
# this does not invoke auth check and only decodes jwt and checks it for validity
# check both tokens
ps.safe_psql("set FOO", password=tenant_token)
ps.safe_psql("set FOO", password=management_token)
ps.safe_psql("set FOO", password=pageserver_token)
new_timeline_id = env.neon_cli.create_branch(
"test_pageserver_auth", tenant_id=env.initial_tenant
@@ -33,7 +33,7 @@ def test_pageserver_auth(neon_env_builder: NeonEnvBuilder):
tenant_id=env.initial_tenant, ancestor_timeline_id=new_timeline_id
)
# console can create branches for tenant
management_http_client.timeline_create(
pageserver_http_client.timeline_create(
tenant_id=env.initial_tenant, ancestor_timeline_id=new_timeline_id
)
@@ -46,7 +46,7 @@ def test_pageserver_auth(neon_env_builder: NeonEnvBuilder):
)
# create tenant using management token
management_http_client.tenant_create()
pageserver_http_client.tenant_create()
# fail to create tenant using tenant token
with pytest.raises(

View File

@@ -181,7 +181,7 @@ def test_pageserver_http_api_client_auth_enabled(neon_env_builder: NeonEnvBuilde
neon_env_builder.auth_enabled = True
env = neon_env_builder.init_start()
management_token = env.auth_keys.generate_management_token()
pageserver_token = env.auth_keys.generate_pageserver_token()
with env.pageserver.http_client(auth_token=management_token) as client:
with env.pageserver.http_client(auth_token=pageserver_token) as client:
check_client(client, env.initial_tenant)