Skip to content

Commit 9130d5b

Browse files
authored
feat(gateway): compare permission checks to permit (#1706)
* feat: permit logic in project list * feat: update scopeduser * fix: rebase * fix: allow deployer bypass * fix: project list name lookup * fix: check the project id * test: set high pdp timeout * refactor: compare permission checks & log diffs * feat: sort project list desc, remove pagination * fix: adjust pdp timeout * nits
1 parent 7c48569 commit 9130d5b

File tree

7 files changed

+151
-81
lines changed

7 files changed

+151
-81
lines changed

backends/tests/integration/permit_tests.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ mod needs_docker {
9090
let client = Client::new(
9191
api_url.to_owned(),
9292
PDP.get().unwrap().uri.clone(),
93-
// "http://localhost:19716".to_owned(),
9493
"default".to_owned(),
9594
std::env::var("PERMIT_ENV").unwrap_or_else(|_| "testing".to_owned()),
9695
api_key,
@@ -100,8 +99,6 @@ mod needs_docker {
10099

101100
Wrap(client)
102101
}
103-
104-
async fn teardown(self) {}
105102
}
106103

107104
#[test_context(Wrap)]

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ services:
283283
environment:
284284
- PDP_CONTROL_PLANE=https://api.eu-central-1.permit.io
285285
- PDP_API_KEY=${PERMIT_API_KEY}
286+
# Querying users with lots of resource instances takes more than the default 1s
287+
- PDP_OPA_CLIENT_QUERY_TIMEOUT=10
286288
ports:
287289
- 7000:7000
288290
networks:

gateway/src/api/latest.rs

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ async fn get_project(
106106
State(RouterState { service, .. }): State<RouterState>,
107107
ScopedUser { scope, .. }: ScopedUser,
108108
) -> Result<AxumJson<project::Response>, Error> {
109-
let project = service.find_project(&scope).await?;
109+
let project = service.find_project_by_name(&scope).await?;
110110
let idle_minutes = project.state.idle_minutes();
111111

112112
let response = project::Response {
113-
id: project.project_id.to_uppercase(),
113+
id: project.id.to_uppercase(),
114114
name: scope.to_string(),
115115
state: project.state.into(),
116116
idle_minutes,
@@ -129,25 +129,31 @@ async fn check_project_name(
129129
.await
130130
.map(AxumJson)
131131
}
132-
133132
async fn get_projects_list(
134133
State(RouterState { service, .. }): State<RouterState>,
135-
User { id: name, .. }: User,
136-
Query(PaginationDetails { page, limit }): Query<PaginationDetails>,
134+
User { id, .. }: User,
137135
) -> Result<AxumJson<Vec<project::Response>>, Error> {
138-
let limit = limit.unwrap_or(u32::MAX);
139-
let page = page.unwrap_or(0);
140-
let projects = service
141-
// The `offset` is page size * amount of pages
142-
.iter_user_projects_detailed(&name, limit * page, limit)
143-
.await?
144-
.map(|project| project::Response {
145-
id: project.0.to_uppercase(),
146-
name: project.1.to_string(),
147-
idle_minutes: project.2.idle_minutes(),
148-
state: project.2.into(),
149-
})
150-
.collect();
136+
let mut projects = vec![];
137+
for p in service
138+
.permit_client
139+
.get_user_projects(&id)
140+
.await
141+
.map_err(|_| Error::from(ErrorKind::Internal))?
142+
{
143+
let proj_id = p.resource.expect("project resource").key;
144+
let project = service.find_project_by_id(&proj_id).await?;
145+
let idle_minutes = project.state.idle_minutes();
146+
147+
let response = project::Response {
148+
id: project.id,
149+
name: project.name,
150+
state: project.state.into(),
151+
idle_minutes,
152+
};
153+
projects.push(response);
154+
}
155+
// sort by descending id
156+
projects.sort_by(|p1, p2| p2.id.cmp(&p1.id));
151157

152158
Ok(AxumJson(projects))
153159
}
@@ -199,7 +205,7 @@ async fn create_project(
199205
.await?;
200206

201207
let response = project::Response {
202-
id: project.project_id.to_string().to_uppercase(),
208+
id: project.id.to_string().to_uppercase(),
203209
name: project_name.to_string(),
204210
state: project.state.into(),
205211
idle_minutes,
@@ -218,11 +224,11 @@ async fn destroy_project(
218224
..
219225
}: ScopedUser,
220226
) -> Result<AxumJson<project::Response>, Error> {
221-
let project = service.find_project(&project_name).await?;
227+
let project = service.find_project_by_name(&project_name).await?;
222228
let idle_minutes = project.state.idle_minutes();
223229

224230
let mut response = project::Response {
225-
id: project.project_id.to_uppercase(),
231+
id: project.id.to_uppercase(),
226232
name: project_name.to_string(),
227233
state: project.state.into(),
228234
idle_minutes,
@@ -266,10 +272,9 @@ async fn delete_project(
266272
}
267273

268274
let project_name = scoped_user.scope.clone();
269-
let project = state.service.find_project(&project_name).await?;
275+
let project = state.service.find_project_by_name(&project_name).await?;
270276

271-
let project_id =
272-
Ulid::from_string(&project.project_id).expect("stored project id to be a valid ULID");
277+
let project_id = Ulid::from_string(&project.id).expect("stored project id to be a valid ULID");
273278

274279
// Try to startup destroyed, errored or outdated projects
275280
let project_deletable = project.state.is_ready() || project.state.is_stopped();
@@ -309,7 +314,7 @@ async fn delete_project(
309314
// Wait for the project to be ready
310315
handle.await;
311316

312-
let new_state = state.service.find_project(&project_name).await?;
317+
let new_state = state.service.find_project_by_name(&project_name).await?;
313318

314319
if !new_state.state.is_ready() {
315320
return Err(Error::from_kind(ErrorKind::ProjectCorrupted));
@@ -393,7 +398,7 @@ async fn override_create_service(
393398
scoped_user: ScopedUser,
394399
req: Request<Body>,
395400
) -> Result<Response<Body>, Error> {
396-
let user_id = scoped_user.user.claim.sub.clone();
401+
let user_id = scoped_user.user.id.clone();
397402
let posthog_client = state.posthog_client.clone();
398403
tokio::spawn(async move {
399404
let event = async_posthog::Event::new("shuttle_api_start_deployment", &user_id);

gateway/src/auth.rs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ use axum::extract::{FromRef, FromRequestParts, Path};
44
use axum::http::request::Parts;
55
use serde::{Deserialize, Serialize};
66
use shuttle_backends::project_name::ProjectName;
7-
use shuttle_common::claims::{Claim, Scope};
7+
use shuttle_backends::ClaimExt;
8+
use shuttle_common::claims::Claim;
89
use shuttle_common::models::error::InvalidProjectName;
910
use shuttle_common::models::user::UserId;
10-
use tracing::{trace, Span};
11+
use tracing::{error, trace, Span};
1112

1213
use crate::api::latest::RouterState;
1314
use crate::{Error, ErrorKind};
@@ -19,7 +20,6 @@ use crate::{Error, ErrorKind};
1920
/// is valid against the user's owned resources.
2021
#[derive(Clone, Deserialize, PartialEq, Eq, Serialize, Debug)]
2122
pub struct User {
22-
pub projects: Vec<ProjectName>,
2323
pub claim: Claim,
2424
pub id: UserId,
2525
}
@@ -32,18 +32,15 @@ where
3232
{
3333
type Rejection = Error;
3434

35-
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
35+
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
3636
let claim = parts.extensions.get::<Claim>().ok_or(ErrorKind::Internal)?;
3737
let user_id = claim.sub.clone();
3838

3939
// Record current account name for tracing purposes
4040
Span::current().record("account.user_id", &user_id);
4141

42-
let RouterState { service, .. } = RouterState::from_ref(state);
43-
4442
let user = User {
4543
claim: claim.clone(),
46-
projects: service.iter_user_projects(&user_id).await?.collect(),
4744
id: user_id,
4845
};
4946

@@ -83,7 +80,43 @@ where
8380
.map_err(|_| Error::from(ErrorKind::InvalidProjectName(InvalidProjectName)))?,
8481
};
8582

86-
if user.projects.contains(&scope) || user.claim.scopes.contains(&Scope::Admin) {
83+
let RouterState { service, .. } = RouterState::from_ref(state);
84+
85+
let has_bypass = user.claim.is_admin() || user.claim.is_deployer();
86+
87+
let allowed = has_bypass
88+
|| {
89+
let projects: Vec<_> = service.iter_user_projects(&user.id).await?.collect();
90+
let internal_allowed = projects.contains(&scope);
91+
92+
let permit_allowed = service
93+
.permit_client
94+
.allowed(
95+
&user.id,
96+
&service.find_project_by_name(&scope).await?.id,
97+
"develop", // TODO?: make this configurable per endpoint?
98+
)
99+
.await
100+
.map_err(|_| {
101+
error!("failed to check Permit permission");
102+
// Error::from_kind(ErrorKind::Internal)
103+
})
104+
.unwrap_or_default();
105+
106+
if internal_allowed != permit_allowed {
107+
error!(
108+
"PERMIT: Permissions for user {} project {} did not match internal permissions. Internal: {}, Permit: {}",
109+
user.id,
110+
scope,
111+
internal_allowed,
112+
permit_allowed
113+
);
114+
}
115+
116+
internal_allowed
117+
};
118+
119+
if allowed {
87120
Ok(Self { user, scope })
88121
} else {
89122
Err(Error::from(ErrorKind::ProjectNotFound(scope.to_string())))

gateway/src/project.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,7 +1845,12 @@ pub mod exec {
18451845
.await
18461846
.expect("could not list projects")
18471847
{
1848-
match gateway.find_project(&project_name).await.unwrap().state {
1848+
match gateway
1849+
.find_project_by_name(&project_name)
1850+
.await
1851+
.unwrap()
1852+
.state
1853+
{
18491854
Project::Errored(ProjectError { ctx: Some(ctx), .. }) => {
18501855
if let Some(container) = ctx.container() {
18511856
if let Ok(container) = gateway
@@ -1938,8 +1943,11 @@ pub mod exec {
19381943
.await
19391944
.expect("could not list cch projects")
19401945
{
1941-
if let Project::Ready(ProjectReady { container, .. }) =
1942-
gateway.find_project(&project_name).await.unwrap().state
1946+
if let Project::Ready(ProjectReady { container, .. }) = gateway
1947+
.find_project_by_name(&project_name)
1948+
.await
1949+
.unwrap()
1950+
.state
19431951
{
19441952
if let Ok(container) = gateway
19451953
.context()

0 commit comments

Comments
 (0)