Skip to main content

kellnr_web_ui/
crate_access.rs

1use axum::Json;
2use axum::extract::{Path, State};
3use kellnr_appstate::DbState;
4use kellnr_common::original_name::OriginalName;
5use kellnr_registry::crate_group::{CrateGroup, CrateGroupList};
6use kellnr_registry::crate_user::{CrateUser, CrateUserList};
7use serde::{Deserialize, Serialize};
8use utoipa::ToSchema;
9
10use crate::error::RouteError;
11use crate::session::AdminUser;
12
13#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
14pub struct AccessData {
15    pub download_restricted: bool,
16}
17
18/// List users with access to a crate (admin only)
19#[utoipa::path(
20    get,
21    path = "/{crate_name}/users",
22    tag = "acl",
23    params(
24        ("crate_name" = OriginalName, Path, description = "Crate name")
25    ),
26    responses(
27        (status = 200, description = "List of users with access", body = CrateUserList),
28        (status = 403, description = "Admin access required")
29    ),
30    security(("session_cookie" = []))
31)]
32pub async fn list_users(
33    _user: AdminUser,
34    Path(crate_name): Path<OriginalName>,
35    State(db): DbState,
36) -> Result<Json<CrateUserList>, RouteError> {
37    let crate_name = crate_name.to_normalized();
38    let users: Vec<CrateUser> = db
39        .get_crate_users(&crate_name)
40        .await?
41        .iter()
42        .map(|u| CrateUser {
43            id: u.id,
44            login: u.name.clone(),
45            name: None,
46        })
47        .collect();
48
49    Ok(Json(CrateUserList::from(users)))
50}
51
52/// Add a user to a crate's access list (admin only)
53#[utoipa::path(
54    put,
55    path = "/{crate_name}/users/{name}",
56    tag = "acl",
57    params(
58        ("crate_name" = OriginalName, Path, description = "Crate name"),
59        ("name" = String, Path, description = "Username to add")
60    ),
61    responses(
62        (status = 200, description = "User added successfully"),
63        (status = 404, description = "User not found"),
64        (status = 403, description = "Admin access required")
65    ),
66    security(("session_cookie" = []))
67)]
68pub async fn add_user(
69    _user: AdminUser,
70    Path((crate_name, name)): Path<(OriginalName, String)>,
71    State(db): DbState,
72) -> Result<(), RouteError> {
73    let crate_name = crate_name.to_normalized();
74
75    if db.get_user(&name).await.is_err() {
76        return Err(RouteError::UserNotFound(name));
77    }
78
79    if !db.is_crate_user(&crate_name, &name).await? {
80        db.add_crate_user(&crate_name, &name).await?;
81    }
82
83    Ok(())
84}
85
86/// Remove a user from a crate's access list (admin only)
87#[utoipa::path(
88    delete,
89    path = "/{crate_name}/users/{name}",
90    tag = "acl",
91    params(
92        ("crate_name" = OriginalName, Path, description = "Crate name"),
93        ("name" = String, Path, description = "Username to remove")
94    ),
95    responses(
96        (status = 200, description = "User removed successfully"),
97        (status = 403, description = "Admin access required")
98    ),
99    security(("session_cookie" = []))
100)]
101pub async fn delete_user(
102    _user: AdminUser,
103    Path((crate_name, name)): Path<(OriginalName, String)>,
104    State(db): DbState,
105) -> Result<(), RouteError> {
106    let crate_name = crate_name.to_normalized();
107    Ok(db.delete_crate_user(&crate_name, &name).await?)
108}
109
110/// List groups with access to a crate (admin only)
111#[utoipa::path(
112    get,
113    path = "/{crate_name}/groups",
114    tag = "acl",
115    params(
116        ("crate_name" = OriginalName, Path, description = "Crate name")
117    ),
118    responses(
119        (status = 200, description = "List of groups with access", body = CrateGroupList),
120        (status = 403, description = "Admin access required")
121    ),
122    security(("session_cookie" = []))
123)]
124pub async fn list_groups(
125    _user: AdminUser,
126    Path(crate_name): Path<OriginalName>,
127    State(db): DbState,
128) -> Result<Json<CrateGroupList>, RouteError> {
129    let crate_name = crate_name.to_normalized();
130    let groups: Vec<CrateGroup> = db
131        .get_crate_groups(&crate_name)
132        .await?
133        .iter()
134        .map(|u| CrateGroup {
135            id: u.id,
136            name: u.name.clone(),
137        })
138        .collect();
139
140    Ok(Json(CrateGroupList::from(groups)))
141}
142
143/// Add a group to a crate's access list (admin only)
144#[utoipa::path(
145    put,
146    path = "/{crate_name}/groups/{name}",
147    tag = "acl",
148    params(
149        ("crate_name" = OriginalName, Path, description = "Crate name"),
150        ("name" = String, Path, description = "Group name to add")
151    ),
152    responses(
153        (status = 200, description = "Group added successfully"),
154        (status = 403, description = "Admin access required")
155    ),
156    security(("session_cookie" = []))
157)]
158pub async fn add_group(
159    _user: AdminUser,
160    Path((crate_name, name)): Path<(OriginalName, String)>,
161    State(db): DbState,
162) -> Result<(), RouteError> {
163    let crate_name = crate_name.to_normalized();
164    if !db.is_crate_group(&crate_name, &name).await? {
165        db.add_crate_group(&crate_name, &name).await?;
166    }
167
168    Ok(())
169}
170
171/// Remove a group from a crate's access list (admin only)
172#[utoipa::path(
173    delete,
174    path = "/{crate_name}/groups/{name}",
175    tag = "acl",
176    params(
177        ("crate_name" = OriginalName, Path, description = "Crate name"),
178        ("name" = String, Path, description = "Group name to remove")
179    ),
180    responses(
181        (status = 200, description = "Group removed successfully"),
182        (status = 403, description = "Admin access required")
183    ),
184    security(("session_cookie" = []))
185)]
186pub async fn delete_group(
187    _user: AdminUser,
188    Path((crate_name, name)): Path<(OriginalName, String)>,
189    State(db): DbState,
190) -> Result<(), RouteError> {
191    let crate_name = crate_name.to_normalized();
192    Ok(db.delete_crate_group(&crate_name, &name).await?)
193}
194
195/// Get a crate's access settings (admin only)
196#[utoipa::path(
197    get,
198    path = "/{crate_name}",
199    tag = "acl",
200    params(
201        ("crate_name" = OriginalName, Path, description = "Crate name")
202    ),
203    responses(
204        (status = 200, description = "Access settings", body = AccessData),
205        (status = 403, description = "Admin access required")
206    ),
207    security(("session_cookie" = []))
208)]
209pub async fn get_access_data(
210    _user: AdminUser,
211    Path(crate_name): Path<OriginalName>,
212    State(db): DbState,
213) -> Result<Json<AccessData>, RouteError> {
214    let crate_name = crate_name.to_normalized();
215    Ok(Json(AccessData {
216        download_restricted: db.is_download_restricted(&crate_name).await?,
217    }))
218}
219
220/// Update a crate's access settings (admin only)
221#[utoipa::path(
222    put,
223    path = "/{crate_name}",
224    tag = "acl",
225    params(
226        ("crate_name" = OriginalName, Path, description = "Crate name")
227    ),
228    request_body = AccessData,
229    responses(
230        (status = 200, description = "Access settings updated", body = AccessData),
231        (status = 403, description = "Admin access required")
232    ),
233    security(("session_cookie" = []))
234)]
235pub async fn set_access_data(
236    _user: AdminUser,
237    State(db): DbState,
238    Path(crate_name): Path<OriginalName>,
239    Json(input): Json<AccessData>,
240) -> Result<Json<AccessData>, RouteError> {
241    let crate_name = crate_name.to_normalized();
242    db.change_download_restricted(&crate_name, input.download_restricted)
243        .await?;
244
245    Ok(Json(AccessData {
246        download_restricted: db.is_download_restricted(&crate_name).await?,
247    }))
248}