Skip to main content

kellnr_http/
authz.rs

1use std::sync::Arc;
2
3use axum::http::StatusCode;
4use kellnr_common::account_name::UserName;
5use kellnr_common::canonical_name::CanonicalName;
6use kellnr_db::provider::DbResult;
7use kellnr_db::{CrateVisibility, DbProvider};
8
9use crate::error::ApiError;
10use crate::identity::Identity;
11
12/// Fail unless `user` is an admin.
13pub fn require_admin(user: &Identity) -> Result<(), ApiError> {
14    if user.is_admin {
15        Ok(())
16    } else {
17        Err(ApiError::new(
18            "Admin access required",
19            StatusCode::FORBIDDEN,
20        ))
21    }
22}
23
24/// Fail unless `user` owns the crate or is an admin.
25pub async fn require_owner(
26    crate_name: &CanonicalName,
27    user: &Identity,
28    db: &Arc<dyn DbProvider>,
29) -> Result<(), ApiError> {
30    if user.is_admin || db.is_owner(crate_name, &user.name).await? {
31        Ok(())
32    } else {
33        Err(ApiError::new(
34            "Not the owner of the crate",
35            StatusCode::FORBIDDEN,
36        ))
37    }
38}
39
40/// Fail if `user` is read-only. Admins can always modify, as they can change
41/// their read-only state anyway.
42pub fn require_writable(user: &Identity) -> Result<(), ApiError> {
43    if !user.is_admin && user.is_read_only {
44        Err(ApiError::new(
45            "Read-only users cannot modify the registry",
46            StatusCode::FORBIDDEN,
47        ))
48    } else {
49        Ok(())
50    }
51}
52
53/// Whether `user` may read the content of a download-restricted crate,
54/// i.e. the crate archive and its hosted rustdoc.
55pub async fn has_restricted_crate_access(
56    crate_name: &CanonicalName,
57    user: &UserName,
58    is_admin: bool,
59    db: &Arc<dyn DbProvider>,
60) -> DbResult<bool> {
61    Ok(is_admin
62        || db.is_crate_user(crate_name, user).await?
63        || db.is_crate_group_user(crate_name, user).await?
64        || db.is_owner(crate_name, user).await?)
65}
66
67/// Whether `user` may see a crate and its metadata, honoring its download
68/// restriction. `None` is an anonymous caller, who only sees unrestricted crates.
69pub async fn can_read_crate(
70    crate_name: &CanonicalName,
71    user: Option<&Identity>,
72    db: &Arc<dyn DbProvider>,
73) -> DbResult<bool> {
74    if !db.is_download_restricted(crate_name).await? {
75        return Ok(true);
76    }
77    match user {
78        Some(user) => has_restricted_crate_access(crate_name, &user.name, user.is_admin, db).await,
79        None => Ok(false),
80    }
81}
82
83/// Which download-restricted crates `user` may see in crate listings.
84pub fn crate_visibility(user: Option<&Identity>) -> CrateVisibility {
85    match user {
86        None => CrateVisibility::Unrestricted,
87        Some(user) if user.is_admin => CrateVisibility::All,
88        Some(user) => CrateVisibility::User(user.name.clone()),
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use kellnr_common::account_name::GroupName;
95    use kellnr_db::UserFlags;
96    use kellnr_db::test_utils::TestCrateBuilder;
97    use kellnr_test_db::TestDb;
98
99    use super::*;
100    use crate::identity::Source;
101
102    fn name() -> CanonicalName {
103        CanonicalName::try_from("restricted").unwrap()
104    }
105
106    fn user_name() -> UserName {
107        UserName::new("user").unwrap()
108    }
109
110    fn user(is_admin: bool) -> Identity {
111        Identity {
112            name: user_name(),
113            is_admin,
114            is_read_only: false,
115            source: Source::Session,
116        }
117    }
118
119    /// A database with the crate `restricted`, owned by `admin`, and the
120    /// user `user` without any access to it.
121    async fn crate_db(restricted: bool) -> TestDb {
122        let db = TestDb::sqlite().await;
123        TestCrateBuilder::new(&db)
124            .name("restricted")
125            .build()
126            .await
127            .unwrap();
128        db.change_download_restricted(&name(), restricted)
129            .await
130            .unwrap();
131        db.add_user(&user_name(), "pwd", UserFlags::default())
132            .await
133            .unwrap();
134        db
135    }
136
137    async fn can_read(db: &TestDb, user: Option<&Identity>) -> bool {
138        can_read_crate(&name(), user, &db.provider()).await.unwrap()
139    }
140
141    #[test]
142    fn crate_visibility_depends_on_caller() {
143        assert_eq!(CrateVisibility::Unrestricted, crate_visibility(None));
144        assert_eq!(CrateVisibility::All, crate_visibility(Some(&user(true))));
145        assert_eq!(
146            CrateVisibility::User(UserName::new("user").unwrap()),
147            crate_visibility(Some(&user(false)))
148        );
149    }
150
151    #[tokio::test]
152    async fn unrestricted_crate_is_readable_by_anyone() {
153        let db = crate_db(false).await;
154        assert!(can_read(&db, None).await);
155    }
156
157    #[tokio::test]
158    async fn restricted_crate_is_hidden_from_anonymous() {
159        let db = crate_db(true).await;
160        assert!(!can_read(&db, None).await);
161    }
162
163    #[tokio::test]
164    async fn restricted_crate_is_readable_by_admin() {
165        let db = crate_db(true).await;
166        assert!(can_read(&db, Some(&user(true))).await);
167    }
168
169    #[tokio::test]
170    async fn restricted_crate_is_hidden_from_user_without_access() {
171        let db = crate_db(true).await;
172        assert!(!can_read(&db, Some(&user(false))).await);
173    }
174
175    #[tokio::test]
176    async fn restricted_crate_is_readable_by_crate_user() {
177        let db = crate_db(true).await;
178        db.add_crate_user(&name(), &user_name()).await.unwrap();
179        assert!(can_read(&db, Some(&user(false))).await);
180    }
181
182    #[tokio::test]
183    async fn restricted_crate_is_readable_by_crate_group_member() {
184        let db = crate_db(true).await;
185        let group = GroupName::new("group").unwrap();
186        db.add_group(&group).await.unwrap();
187        db.add_group_user(&group, &user_name()).await.unwrap();
188        db.add_crate_group(&name(), &group).await.unwrap();
189        assert!(can_read(&db, Some(&user(false))).await);
190    }
191
192    #[tokio::test]
193    async fn restricted_crate_is_readable_by_owner() {
194        let db = crate_db(true).await;
195        db.add_owner(&name(), &user_name()).await.unwrap();
196        assert!(can_read(&db, Some(&user(false))).await);
197    }
198}
199
200#[cfg(test)]
201mod require_tests {
202    use super::*;
203    use crate::identity::Source;
204
205    fn user(is_admin: bool, is_read_only: bool) -> Identity {
206        Identity {
207            name: UserName::new("user").unwrap(),
208            is_admin,
209            is_read_only,
210            source: Source::Token(None),
211        }
212    }
213
214    fn status(result: Result<(), ApiError>) -> Option<StatusCode> {
215        result.err().map(|e| e.status())
216    }
217
218    #[test]
219    fn only_admins_pass_require_admin() {
220        assert_eq!(status(require_admin(&user(true, false))), None);
221        assert_eq!(
222            status(require_admin(&user(false, false))),
223            Some(StatusCode::FORBIDDEN)
224        );
225    }
226
227    #[test]
228    fn read_only_users_are_forbidden_to_write() {
229        assert_eq!(
230            status(require_writable(&user(false, true))),
231            Some(StatusCode::FORBIDDEN)
232        );
233        assert_eq!(status(require_writable(&user(false, false))), None);
234        assert_eq!(status(require_writable(&user(true, true))), None);
235    }
236}