1use axum::Json;
2use axum::extract::{Path, State};
3use kellnr_common::original_name::OriginalName;
4use kellnr_http::authz;
5use kellnr_http::error::ApiError;
6use kellnr_http::identity::{AdminUser, AnyUser};
7use kellnr_http::state::DbState;
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10
11#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
12pub struct AccessData {
13 pub download_restricted: bool,
14}
15
16#[utoipa::path(
18 get,
19 path = "/{crate_name}",
20 tag = "acl",
21 params(
22 ("crate_name" = OriginalName, Path, description = "Crate name")
23 ),
24 responses(
25 (status = 200, description = "Access settings", body = AccessData),
26 (status = 403, description = "Admin or crate owner access required")
27 ),
28 security(("session_cookie" = []))
29)]
30pub async fn get_access_data(
31 user: AnyUser,
32 Path(crate_name): Path<OriginalName>,
33 State(db): DbState,
34) -> Result<Json<AccessData>, ApiError> {
35 let crate_name = crate_name.to_canonical();
36 authz::require_owner(&crate_name, &user, &db).await?;
39 Ok(Json(AccessData {
40 download_restricted: db.is_download_restricted(&crate_name).await?,
41 }))
42}
43
44#[utoipa::path(
46 put,
47 path = "/{crate_name}",
48 tag = "acl",
49 params(
50 ("crate_name" = OriginalName, Path, description = "Crate name")
51 ),
52 request_body = AccessData,
53 responses(
54 (status = 200, description = "Access settings updated", body = AccessData),
55 (status = 403, description = "Admin access required")
56 ),
57 security(("session_cookie" = []))
58)]
59pub async fn set_access_data(
60 _user: AdminUser,
61 State(db): DbState,
62 Path(crate_name): Path<OriginalName>,
63 Json(input): Json<AccessData>,
64) -> Result<Json<AccessData>, ApiError> {
65 let crate_name = crate_name.to_canonical();
66 db.change_download_restricted(&crate_name, input.download_restricted)
67 .await?;
68
69 Ok(Json(AccessData {
70 download_restricted: db.is_download_restricted(&crate_name).await?,
71 }))
72}
73
74#[cfg(test)]
75mod tests {
76 use axum::Router;
77 use axum::body::Body;
78 use axum::http::{Request, StatusCode, header};
79 use axum::routing::get;
80 use kellnr_common::account_name::UserName;
81 use kellnr_common::canonical_name::CanonicalName;
82 use kellnr_db::test_utils::TestCrateBuilder;
83 use kellnr_db::{DbProvider, UserFlags};
84 use kellnr_http::state::AppStateData;
85 use kellnr_http::test_support::{session_cookie, sqlite_test_state};
86 use kellnr_test_db::TestDb;
87 use tower::ServiceExt;
88
89 use super::*;
90
91 fn crate_name() -> CanonicalName {
92 CanonicalName::try_from("mycrate").unwrap()
93 }
94
95 async fn state_with_session(name: &str, flags: UserFlags) -> (AppStateData, TestDb, String) {
98 let (state, db) = sqlite_test_state().await;
99 let cookie = session_cookie(&db, name, flags).await;
100 if name != "owner" {
101 db.add_user(&UserName::new("owner").unwrap(), "pwd", UserFlags::normal())
102 .await
103 .unwrap();
104 }
105 TestCrateBuilder::new(&db)
106 .name("mycrate")
107 .owner("owner")
108 .build()
109 .await
110 .unwrap();
111 (state, db, cookie)
112 }
113
114 async fn send(
115 state: AppStateData,
116 cookie: &str,
117 request: axum::http::request::Builder,
118 body: Body,
119 ) -> StatusCode {
120 let app = Router::new()
121 .route("/{crate_name}", get(get_access_data).put(set_access_data))
122 .with_state(state);
123 let request = request
124 .header(header::COOKIE, cookie)
125 .header(header::CONTENT_TYPE, "application/json")
126 .body(body)
127 .unwrap();
128 app.oneshot(request).await.unwrap().status()
129 }
130
131 async fn restrict(state: AppStateData, cookie: &str) -> StatusCode {
132 let body = Body::from(r#"{"download_restricted":true}"#);
133 send(state, cookie, Request::put("/mycrate"), body).await
134 }
135
136 #[tokio::test]
137 async fn owner_and_admin_can_read_access_settings() {
138 for (name, flags) in [
139 ("owner", UserFlags::normal()),
140 ("owner", UserFlags::read_only()),
141 ("admin", UserFlags::admin()),
142 ] {
143 let (state, _db, cookie) = state_with_session(name, flags).await;
144 assert_eq!(
145 StatusCode::OK,
146 send(state, &cookie, Request::get("/mycrate"), Body::empty()).await,
147 "{name} {flags:?}"
148 );
149 }
150 }
151
152 #[tokio::test]
153 async fn non_owner_cannot_read_access_settings() {
154 let (state, _db, cookie) = state_with_session("user", UserFlags::normal()).await;
155 assert_eq!(
156 StatusCode::FORBIDDEN,
157 send(state, &cookie, Request::get("/mycrate"), Body::empty()).await
158 );
159 }
160
161 #[tokio::test]
162 async fn owner_cannot_change_download_restriction() {
163 let (state, db, cookie) = state_with_session("owner", UserFlags::normal()).await;
164
165 assert_eq!(StatusCode::FORBIDDEN, restrict(state, &cookie).await);
166 assert!(!db.is_download_restricted(&crate_name()).await.unwrap());
167 }
168
169 #[tokio::test]
170 async fn admin_can_change_download_restriction() {
171 let (state, db, cookie) = state_with_session("admin", UserFlags::admin()).await;
172
173 assert_eq!(StatusCode::OK, restrict(state, &cookie).await);
174 assert!(db.is_download_restricted(&crate_name()).await.unwrap());
175 }
176}