Skip to main content

kellnr_web_ui/
crate_access.rs

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/// Get a crate's access settings (admin or crate owner)
17#[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    // Admins and crate owners, also read-only ones, may read the access
37    // settings of a crate.
38    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/// Update a crate's access settings (admin only)
45#[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    /// A database with "mycrate" owned by "owner", and a session cookie for
96    /// `name` with `flags`.
97    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}