Skip to content
Clone

fix(git): authorize html fragments by session instead of basic auth

ZhenYi committed
parent 23daab3 commit cb06060 Browse files

29 files changed

+78 −212

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 29
Modified lib/auth/src/openapi.rs View lib/auth/src/openapi.rs
@@ -98,7 +98,6 @@
9898 handlers::two_factor::TotpCodeCmd,
9999 handlers::two_factor::RegenerateRecoveryCodesCmd,
100100 handlers::two_factor::RecoveryCodesResp,
101 models::users::access_scope::AccessKeyScope,
102101 handlers::sessions::SessionDto,
103102 handlers::sessions::SessionKind,
104103 handlers::sessions::CountResp,
Modified lib/git/src/handler/html/badge.rs View lib/git/src/handler/html/badge.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, archived_class, format_size, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[utoipa::path(
99 get,
@@ -20,9 +20,9 @@
2020pub async fn render_badge(
2121 State(service): State<GitService>,
2222 Path(path): Path<HtmlRepoPath>,
23 auth: BasicAuth,
23 session: Session,
2424) -> Response {
25 let ctx = match resolve(service, path, auth).await {
25 let ctx = match resolve(service, path, session).await {
2626 Ok(c) => c,
2727 Err(resp) => return resp,
2828 };
Modified lib/git/src/handler/html/blame.rs View lib/git/src/handler/html/blame.rs
@@ -8,7 +8,7 @@
88 HtmlRepoPath, archived_class, error, resolve_view, response_html, short_sha,
99};
1010use crate::service::GitService;
11use crate::share::access::BasicAuth;
11use ::session::Session;
1212
1313#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
1414pub struct BlameQuery {
@@ -34,9 +34,9 @@
3434 State(service): State<GitService>,
3535 Path(path): Path<HtmlRepoPath>,
3636 Query(q): Query<BlameQuery>,
37 auth: BasicAuth,
37 session: Session,
3838) -> Response {
39 let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, true).await {
39 let view = match resolve_view(service.clone(), path, session, q.revision, q.path, true).await {
4040 Ok(v) => v,
4141 Err(resp) => return resp,
4242 };
Modified lib/git/src/handler/html/blob_content.rs View lib/git/src/handler/html/blob_content.rs
@@ -6,7 +6,7 @@
66 HtmlRepoPath, archived_class, error, format_size, resolve_view, response_html,
77};
88use crate::service::GitService;
9use crate::share::access::BasicAuth;
9use ::session::Session;
1010
1111#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
1212pub struct BlobContentQuery {
@@ -32,9 +32,9 @@
3232 State(service): State<GitService>,
3333 Path(path): Path<HtmlRepoPath>,
3434 Query(q): Query<BlobContentQuery>,
35 auth: BasicAuth,
35 session: Session,
3636) -> Response {
37 let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, true).await {
37 let view = match resolve_view(service.clone(), path, session, q.revision, q.path, true).await {
3838 Ok(v) => v,
3939 Err(resp) => return resp,
4040 };
Modified lib/git/src/handler/html/blob_header.rs View lib/git/src/handler/html/blob_header.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, archived_class, resolve_view, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
99pub struct BlobHeaderQuery {
@@ -29,9 +29,9 @@
2929 State(service): State<GitService>,
3030 Path(path): Path<HtmlRepoPath>,
3131 Query(q): Query<BlobHeaderQuery>,
32 auth: BasicAuth,
32 session: Session,
3333) -> Response {
34 let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, true).await {
34 let view = match resolve_view(service.clone(), path, session, q.revision, q.path, true).await {
3535 Ok(v) => v,
3636 Err(resp) => return resp,
3737 };
Modified lib/git/src/handler/html/blob_pages.rs View lib/git/src/handler/html/blob_pages.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, error, resolve_view};
55use crate::service::{GitService, pdf};
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
99pub struct BlobPagesQuery {
@@ -41,9 +41,9 @@
4141 State(service): State<GitService>,
4242 Path(path): Path<HtmlRepoPath>,
4343 Query(q): Query<BlobPagesQuery>,
44 auth: BasicAuth,
44 session: Session,
4545) -> Response {
46 let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, true).await {
46 let view = match resolve_view(service.clone(), path, session, q.revision, q.path, true).await {
4747 Ok(v) => v,
4848 Err(resp) => return resp,
4949 };
Modified lib/git/src/handler/html/breadcrumbs.rs View lib/git/src/handler/html/breadcrumbs.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, archived_class, error, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77use crate::share::validate;
88
99#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
@@ -30,9 +30,9 @@
3030 State(service): State<GitService>,
3131 Path(path): Path<HtmlRepoPath>,
3232 Query(q): Query<BreadcrumbsQuery>,
33 auth: BasicAuth,
33 session: Session,
3434) -> Response {
35 let ctx = match resolve(service, path, auth).await {
35 let ctx = match resolve(service, path, session).await {
3636 Ok(c) => c,
3737 Err(resp) => return resp,
3838 };
Modified lib/git/src/handler/html/clone_buttons.rs View lib/git/src/handler/html/clone_buttons.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, clone_https_url, clone_ssh_url, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[utoipa::path(
99 get,
@@ -20,9 +20,9 @@
2020pub async fn render_clone_buttons(
2121 State(service): State<GitService>,
2222 Path(path): Path<HtmlRepoPath>,
23 auth: BasicAuth,
23 session: Session,
2424) -> Response {
25 let ctx = match resolve(service.clone(), path.clone(), auth).await {
25 let ctx = match resolve(service.clone(), path.clone(), session).await {
2626 Ok(c) => c,
2727 Err(resp) => return resp,
2828 };
Modified lib/git/src/handler/html/commit_diff.rs View lib/git/src/handler/html/commit_diff.rs
@@ -6,7 +6,7 @@
66};
77use crate::service::GitService;
88use crate::service::diffs::ChangedPathDiffDto;
9use crate::share::access::BasicAuth;
9use ::session::Session;
1010
1111const MAX_FILES: usize = 50;
1212const MAX_LINES_PER_FILE: usize = 2000;
@@ -35,9 +35,9 @@
3535 State(service): State<GitService>,
3636 Path(path): Path<HtmlRepoPath>,
3737 Query(q): Query<CommitDiffQuery>,
38 auth: BasicAuth,
38 session: Session,
3939) -> Response {
40 let ctx = match resolve(service.clone(), path, auth).await {
40 let ctx = match resolve(service.clone(), path, session).await {
4141 Ok(c) => c,
4242 Err(resp) => return resp,
4343 };
Modified lib/git/src/handler/html/commits.rs View lib/git/src/handler/html/commits.rs
@@ -9,7 +9,7 @@
99};
1010use crate::service::GitService;
1111use crate::service::commits::ListCommitsCmd;
12use crate::share::access::BasicAuth;
12use ::session::Session;
1313
1414#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
1515pub struct CommitsQuery {
@@ -39,9 +39,9 @@
3939 State(service): State<GitService>,
4040 Path(path): Path<HtmlRepoPath>,
4141 Query(q): Query<CommitsQuery>,
42 auth: BasicAuth,
42 session: Session,
4343) -> Response {
44 let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, false).await {
44 let view = match resolve_view(service.clone(), path, session, q.revision, q.path, false).await {
4545 Ok(v) => v,
4646 Err(resp) => return resp,
4747 };
Modified lib/git/src/handler/html/error.rs View lib/git/src/handler/html/error.rs
@@ -30,11 +30,5 @@
3030 );
3131 resp.headers_mut()
3232 .insert(actix_web::http::header::VARY, "Cookie".parse().unwrap());
33 if status == StatusCode::UNAUTHORIZED {
34 resp.headers_mut().insert(
35 actix_web::http::header::WWW_AUTHENTICATE,
36 "Basic realm=\"gitdata\"".parse().unwrap(),
37 );
38 }
3933 resp
4034}
Modified lib/git/src/handler/html/header.rs View lib/git/src/handler/html/header.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, archived_class, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[utoipa::path(
99 get,
@@ -20,9 +20,9 @@
2020pub async fn render_header(
2121 State(service): State<GitService>,
2222 Path(path): Path<HtmlRepoPath>,
23 auth: BasicAuth,
23 session: Session,
2424) -> Response {
25 let ctx = match resolve(service, path, auth).await {
25 let ctx = match resolve(service, path, session).await {
2626 Ok(c) => c,
2727 Err(resp) => return resp,
2828 };
Modified lib/git/src/handler/html/languages.rs View lib/git/src/handler/html/languages.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, archived_class, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[utoipa::path(
99 get,
@@ -20,9 +20,9 @@
2020pub async fn render_languages(
2121 State(service): State<GitService>,
2222 Path(path): Path<HtmlRepoPath>,
23 auth: BasicAuth,
23 session: Session,
2424) -> Response {
25 let ctx = match resolve(service.clone(), path, auth).await {
25 let ctx = match resolve(service.clone(), path, session).await {
2626 Ok(c) => c,
2727 Err(resp) => return resp,
2828 };
Modified lib/git/src/handler/html/last_commit.rs View lib/git/src/handler/html/last_commit.rs
@@ -6,7 +6,7 @@
66 HtmlRepoPath, archived_class, error, first_line, resolve, response_html, short_sha,
77};
88use crate::service::GitService;
9use crate::share::access::BasicAuth;
9use ::session::Session;
1010use crate::share::validate;
1111
1212#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
@@ -33,9 +33,9 @@
3333 State(service): State<GitService>,
3434 Path(path): Path<HtmlRepoPath>,
3535 Query(q): Query<LastCommitQuery>,
36 auth: BasicAuth,
36 session: Session,
3737) -> Response {
38 let ctx = match resolve(service.clone(), path.clone(), auth).await {
38 let ctx = match resolve(service.clone(), path.clone(), session).await {
3939 Ok(c) => c,
4040 Err(resp) => return resp,
4141 };
Modified lib/git/src/handler/html/mod.rs View lib/git/src/handler/html/mod.rs
@@ -7,9 +7,10 @@
77use tracing::warn;
88
99use crate::context::facade::GitContext;
10use crate::errors::ApiError;
11use crate::handler::ensure_read_access;
1012use crate::service::GitService;
1113use crate::service::repos::RepoDto;
12use crate::share::access::{BasicAuth, allow_repo_read};
1314use crate::share::errors::GitError;
1415
1516pub mod badge;
@@ -43,6 +44,17 @@
4344const MAX_SUBJECT_LEN: usize = 200;
4445const MAX_REVISION_LEN: usize = 256;
4546
47fn error_status(error: &ApiError) -> u16 {
48 match error {
49 ApiError::BadRequest(_) => 400,
50 ApiError::Unauthorized(_) => 401,
51 ApiError::Forbidden(_) => 403,
52 ApiError::NotFound(_) => 404,
53 ApiError::Conflict(_) => 409,
54 ApiError::Internal(_) => 500,
55 }
56}
57
4658#[derive(Clone)]
4759pub struct HtmlCtx {
4860 pub service: GitService,
@@ -135,7 +147,7 @@
135147pub async fn resolve(
136148 service: GitService,
137149 path: HtmlRepoPath,
138 auth: BasicAuth,
150 session: ::session::Session,
139151) -> Result<HtmlCtx, Response> {
140152 if path.space_name.chars().count() > MAX_NAME_LEN
141153 || path.repo_name.chars().count() > MAX_NAME_LEN
@@ -153,14 +165,9 @@
153165 return Err(html_err(500, "internal"));
154166 }
155167 };
156 let uid = match allow_repo_read(&service, &auth, &ctx).await {
168 let uid = match ensure_read_access(&service, &session, &ctx).await {
157169 Ok(uid) => uid,
158 Err(GitError::Unauthorized) => return Err(html_err(401, "auth required")),
159 Err(GitError::Forbidden) => return Err(html_err(403, "forbidden")),
160 Err(e) => {
161 warn!(error = %e, "html auth internal");
162 return Err(html_err(500, "internal"));
163 }
170 Err(error) => return Err(html_err(error_status(&error), "")),
164171 };
165172 let dto = match service.get_repo_for_html(ctx.clone()).await {
166173 Ok(d) => d,
@@ -191,12 +198,12 @@
191198pub async fn resolve_view(
192199 service: GitService,
193200 path: HtmlRepoPath,
194 auth: BasicAuth,
201 session: ::session::Session,
195202 revision: Option<String>,
196203 raw_path: Option<String>,
197204 require_path: bool,
198205) -> Result<HtmlView, Response> {
199 let ctx = resolve(service, path, auth).await?;
206 let ctx = resolve(service, path, session).await?;
200207 let revision = revision
201208 .as_deref()
202209 .unwrap_or(ctx.dto.default_branch.as_str())
Modified lib/git/src/handler/html/quickstart.rs View lib/git/src/handler/html/quickstart.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, clone_https_url, clone_ssh_url, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[utoipa::path(
99 get,
@@ -20,9 +20,9 @@
2020pub async fn render_quickstart(
2121 State(service): State<GitService>,
2222 Path(path): Path<HtmlRepoPath>,
23 auth: BasicAuth,
23 session: Session,
2424) -> Response {
25 let ctx = match resolve(service.clone(), path.clone(), auth).await {
25 let ctx = match resolve(service.clone(), path.clone(), session).await {
2626 Ok(c) => c,
2727 Err(resp) => return resp,
2828 };
Modified lib/git/src/handler/html/readme.rs View lib/git/src/handler/html/readme.rs
@@ -6,7 +6,7 @@
66 HtmlRepoPath, archived_class, error, format_size, resolve_view, response_html,
77};
88use crate::service::GitService;
9use crate::share::access::BasicAuth;
9use ::session::Session;
1010
1111const README_CANDIDATES: &[&str] = &[
1212 "README.md",
@@ -45,9 +45,9 @@
4545 State(service): State<GitService>,
4646 Path(path): Path<HtmlRepoPath>,
4747 Query(q): Query<ReadmeQuery>,
48 auth: BasicAuth,
48 session: Session,
4949) -> Response {
50 let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, false).await {
50 let view = match resolve_view(service.clone(), path, session, q.revision, q.path, false).await {
5151 Ok(v) => v,
5252 Err(resp) => return resp,
5353 };
Modified lib/git/src/handler/html/refs_dropdown.rs View lib/git/src/handler/html/refs_dropdown.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, archived_class, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77use crate::share::validate;
88
99#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
@@ -28,9 +28,9 @@
2828 State(service): State<GitService>,
2929 Path(path): Path<HtmlRepoPath>,
3030 Query(q): Query<RefsDropdownQuery>,
31 auth: BasicAuth,
31 session: Session,
3232) -> Response {
33 let ctx = match resolve(service.clone(), path, auth).await {
33 let ctx = match resolve(service.clone(), path, session).await {
3434 Ok(c) => c,
3535 Err(resp) => return resp,
3636 };
Modified lib/git/src/handler/html/refs_summary.rs View lib/git/src/handler/html/refs_summary.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, archived_class, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[utoipa::path(
99 get,
@@ -20,9 +20,9 @@
2020pub async fn render_refs_summary(
2121 State(service): State<GitService>,
2222 Path(path): Path<HtmlRepoPath>,
23 auth: BasicAuth,
23 session: Session,
2424) -> Response {
25 let ctx = match resolve(service, path, auth).await {
25 let ctx = match resolve(service, path, session).await {
2626 Ok(c) => c,
2727 Err(resp) => return resp,
2828 };
Modified lib/git/src/handler/html/star_button.rs View lib/git/src/handler/html/star_button.rs
@@ -3,7 +3,7 @@
33
44use crate::handler::html::{HtmlRepoPath, archived_class, resolve, response_html};
55use crate::service::GitService;
6use crate::share::access::BasicAuth;
6use ::session::Session;
77
88#[utoipa::path(
99 get,
@@ -20,9 +20,9 @@
2020pub async fn render_star_button(
2121 State(service): State<GitService>,
2222 Path(path): Path<HtmlRepoPath>,
23 auth: BasicAuth,
23 session: Session,
2424) -> Response {
25 let ctx = match resolve(service.clone(), path, auth).await {
25 let ctx = match resolve(service.clone(), path, session).await {
2626 Ok(c) => c,
2727 Err(resp) => return resp,
2828 };
Modified lib/git/src/handler/html/tree.rs View lib/git/src/handler/html/tree.rs
@@ -8,7 +8,7 @@
88 resolve_view, response_html, short_sha,
99};
1010use crate::service::GitService;
11use crate::share::access::BasicAuth;
11use ::session::Session;
1212
1313#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
1414pub struct TreeQuery {
@@ -36,9 +36,9 @@
3636 State(service): State<GitService>,
3737 Path(path): Path<HtmlRepoPath>,
3838 Query(q): Query<TreeQuery>,
39 auth: BasicAuth,
39 session: Session,
4040) -> Response {
41 let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, false).await {
41 let view = match resolve_view(service.clone(), path, session, q.revision, q.path, false).await {
4242 Ok(v) => v,
4343 Err(resp) => return resp,
4444 };
Modified lib/git/src/handler/html/tree_row.rs View lib/git/src/handler/html/tree_row.rs
@@ -7,7 +7,7 @@
77 short_sha,
88};
99use crate::service::GitService;
10use crate::share::access::BasicAuth;
10use ::session::Session;
1111
1212#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
1313pub struct TreeRowQuery {
@@ -35,9 +35,9 @@
3535 State(service): State<GitService>,
3636 Path(path): Path<HtmlRepoPath>,
3737 Query(q): Query<TreeRowQuery>,
38 auth: BasicAuth,
38 session: Session,
3939) -> Response {
40 let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, false).await {
40 let view = match resolve_view(service.clone(), path, session, q.revision, q.path, false).await {
4141 Ok(v) => v,
4242 Err(resp) => return resp,
4343 };
Modified lib/git/src/handler/html/watch_button.rs View lib/git/src/handler/html/watch_button.rs
@@ -4,7 +4,7 @@
44
55use crate::handler::html::{HtmlRepoPath, archived_class, resolve, response_html};
66use crate::service::GitService;
7use crate::share::access::BasicAuth;
7use ::session::Session;
88
99#[utoipa::path(
1010 get,
@@ -21,9 +21,9 @@
2121pub async fn render_watch_button(
2222 State(service): State<GitService>,
2323 Path(path): Path<HtmlRepoPath>,
24 auth: BasicAuth,
24 session: Session,
2525) -> Response {
26 let ctx = match resolve(service.clone(), path, auth).await {
26 let ctx = match resolve(service.clone(), path, session).await {
2727 Ok(c) => c,
2828 Err(resp) => return resp,
2929 };
Modified lib/git/src/share/access.rs View lib/git/src/share/access.rs
@@ -1,121 +1,8 @@
1use std::future::Ready;
2
3use actix_web::dev::Payload;
4use actix_web::http::header;
5use actix_web::{FromRequest, HttpRequest};
6use base64::Engine as _;
7use models::users::access_scope::AccessKeyScope;
81use uuid::Uuid;
92
103use crate::context::facade::GitContext;
114use crate::service::GitService;
125use crate::share::errors::GitError;
13
14#[derive(Debug, Clone)]
15pub struct BasicAuth {
16 pub token: Option<String>,
17}
18
19impl FromRequest for BasicAuth {
20 type Error = GitError;
21 type Future = Ready<Result<Self, Self::Error>>;
22
23 fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future {
24 std::future::ready(parse_basic_auth(request))
25 }
26}
27
28fn parse_basic_auth(request: &HttpRequest) -> Result<BasicAuth, GitError> {
29 let Some(value) = request.headers().get(header::AUTHORIZATION) else {
30 return Ok(BasicAuth { token: None });
31 };
32 let raw = value
33 .to_str()
34 .map_err(|_| GitError::InvalidArgument("malformed authorization header".into()))?;
35 let Some(b64) = raw.strip_prefix("Basic ") else {
36 return Ok(BasicAuth { token: None });
37 };
38 let decoded = base64::engine::general_purpose::STANDARD
39 .decode(b64)
40 .map_err(|_| GitError::InvalidArgument("malformed basic credentials".into()))?;
41 let creds = String::from_utf8(decoded)
42 .map_err(|_| GitError::InvalidArgument("malformed basic credentials".into()))?;
43 let token = creds.split_once(':').map(|(_, pass)| pass.to_string());
44 Ok(BasicAuth { token })
45}
46
47pub async fn verify_access_key(
48 _svc: &GitService,
49 _token: &str,
50) -> Result<Option<(i64, AccessKeyScope)>, GitError> {
51 Ok(None)
52}
53
54/// HTTP write-path auth: token must be valid AND the resolved `uid` must
55/// have write membership on the resolved `ctx` repo. Use everywhere the
56/// HTTP path accepts mutating requests.
57pub async fn require_writer(
58 svc: &GitService,
59 auth: &BasicAuth,
60 ctx: &GitContext,
61) -> Result<i64, GitError> {
62 let uid = resolve_caller_for_write(svc, auth).await?;
63 authorize_repo_access(svc, uid, ctx, true, false).await?;
64 Ok(uid)
65}
66
67/// HTTP read-path auth: `Ok(None)` only for anonymous reads on public
68/// repos; otherwise returns `Unauthorized` / `Forbidden` / `Some(uid)`.
69pub async fn allow_repo_read(
70 svc: &GitService,
71 auth: &BasicAuth,
72 ctx: &GitContext,
73) -> Result<Option<i64>, GitError> {
74 let Some(token) = &auth.token else {
75 if ctx.model.is_private {
76 return Err(GitError::Unauthorized);
77 }
78 return Ok(None);
79 };
80 let (uid, _) = verify_access_key(svc, token)
81 .await?
82 .ok_or(GitError::Unauthorized)?;
83 authorize_repo_access(svc, uid, ctx, false, false).await?;
84 Ok(Some(uid))
85}
86
87/// Admin-only HTTP auth: caller must hold a valid write-scope token AND
88/// be the space owner or an Admin collaborator. Use for LFS force-unlock
89/// and any action spec'd as admin-only.
90pub async fn require_repo_admin(
91 svc: &GitService,
92 auth: &BasicAuth,
93 ctx: &GitContext,
94) -> Result<i64, GitError> {
95 if auth
96 .token
97 .as_deref()
98 .is_some_and(|token| token.starts_with("ci_"))
99 {
100 return Err(GitError::Forbidden);
101 }
102 let uid = resolve_caller_for_write(svc, auth).await?;
103 authorize_repo_access(svc, uid, ctx, true, true).await?;
104 Ok(uid)
105}
106
107async fn resolve_caller_for_write(svc: &GitService, auth: &BasicAuth) -> Result<i64, GitError> {
108 let Some(token) = &auth.token else {
109 return Err(GitError::Unauthorized);
110 };
111 let (uid, scope) = verify_access_key(svc, token)
112 .await?
113 .ok_or(GitError::Unauthorized)?;
114 if !scope.allows(true) {
115 return Err(GitError::Forbidden);
116 }
117 Ok(uid)
118}
1196
1207/// Per-repo authorization — the central ACL that was previously only on
1218/// the SSH path (`ssh/handler/command.rs::authorize_repo`) and missing
Modified lib/git/Cargo.toml View lib/git/Cargo.toml
@@ -22,7 +22,6 @@
2222uuid = { workspace = true }
2323time = { workspace = true }
2424globset = { workspace = true }
25base64 = { workspace = true }
2625sha2 = { workspace = true }
2726actix-web = { workspace = true }
2827futures = { workspace = true }
Deleted lib/models/src/users/access_scope.rs
@@ -1,18 +0,0 @@
1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, utoipa::ToSchema)]
4#[serde(rename_all = "snake_case")]
5pub enum AccessKeyScope {
6 #[default]
7 Read,
8 Write,
9}
10
11impl AccessKeyScope {
12 pub const fn allows(self, want_write: bool) -> bool {
13 match self {
14 Self::Write => true,
15 Self::Read => !want_write,
16 }
17 }
18}
Modified lib/models/src/users/mod.rs View lib/models/src/users/mod.rs
@@ -1,6 +1,5 @@
11pub const DATABASE: &str = "user";
22
3pub mod access_scope;
43pub mod user;
54pub mod user_agreement_acceptances;
65pub mod user_blocks;
Modified Cargo.lock View Cargo.lock
@@ -2726,7 +2726,6 @@
27262726 "actix-web",
27272727 "async-trait",
27282728 "auth",
2729 "base64 0.23.1",
27302729 "bytes",
27312730 "cache",
27322731 "config 1.0.0",
Modified openapi.json View openapi.json

Diff too large to display.