Skip to content
Clone

refactor(gitauth): refresh crate across auth, permission, resolve, service

zhenyi committed
parent 92c3b35 commit 90b42d8 Browse files

8 files changed

+117 −63

File tree 8
Modified lib/gitauth/basic.rs View lib/gitauth/basic.rs
@@ -9,7 +9,6 @@
99 pub password: String,
1010}
1111
12/// Parse an `Authorization: Basic <base64>` header value.
1312pub fn parse_authorization(header: &str) -> GitAuthResult<BasicCredentials> {
1413 let encoded = header.strip_prefix("Basic ").ok_or_else(|| {
1514 GitAuthError::BadRequest("expected 'Basic <credentials>' authorization header".into())
@@ -28,7 +27,6 @@
2827 })
2928}
3029
31/// Build a `Basic <base64>` header value (used by clients and tests).
3230pub fn encode_credentials(username: &str, password: &str) -> String {
3331 format!(
3432 "Basic {}",
Modified lib/gitauth/config.rs View lib/gitauth/config.rs
@@ -1,8 +1,6 @@
11#[derive(Debug, Clone)]
22pub struct GitAuthConfig {
3 /// Whether anonymous clients may read public repositories.
4 /// When false, every git smart HTTP request requires authentication.
5 pub allow_anonymous_read: bool,
3 pub allow_anonymous_read: bool,
64}
75
86impl Default for GitAuthConfig {
Modified lib/gitauth/lib.rs View lib/gitauth/lib.rs
@@ -1,6 +1,7 @@
11pub mod basic;
22pub mod config;
33pub mod error;
4pub mod metrics;
45pub mod permission;
56pub mod resolve;
67pub mod service;
Added lib/gitauth/metrics.rs View lib/gitauth/metrics.rs
@@ -0,0 +1,62 @@
1use std::sync::Once;
2
3use metrics::{counter, describe_counter};
4
5use crate::error::{GitAuthError, GitAuthResult};
6
7pub const OUTCOME_SUCCESS: &str = "success";
8
9static DESCRIBE_ONCE: Once = Once::new();
10
11fn describe_all() {
12 describe_counter!(
13 "gitauth_resolve_total",
14 "Repository resolution attempts partitioned by outcome (success, error)."
15 );
16 describe_counter!(
17 "gitauth_authenticate_total",
18 "Git authentication attempts partitioned by outcome (success, unauthorized, error)."
19 );
20 describe_counter!(
21 "gitauth_authorize_total",
22 "Git authorization checks partitioned by outcome (success, unauthorized, forbidden, not_found, error)."
23 );
24}
25
26pub fn ensure_described() {
27 DESCRIBE_ONCE.call_once(describe_all);
28}
29
30pub(crate) fn record_resolve<T>(result: &GitAuthResult<T>) {
31 ensure_described();
32 counter!("gitauth_resolve_total", "outcome" => outcome_for(result)).increment(1);
33}
34
35pub(crate) fn record_authenticate<T>(result: &GitAuthResult<T>) {
36 ensure_described();
37 let outcome = match result {
38 Ok(_) => OUTCOME_SUCCESS,
39 Err(GitAuthError::Unauthorized(_)) => "unauthorized",
40 Err(_) => "error",
41 };
42 counter!("gitauth_authenticate_total", "outcome" => outcome).increment(1);
43}
44
45pub(crate) fn record_authorize<T>(result: &GitAuthResult<T>) {
46 ensure_described();
47 let outcome = match result {
48 Ok(_) => OUTCOME_SUCCESS,
49 Err(GitAuthError::Unauthorized(_)) => "unauthorized",
50 Err(GitAuthError::Forbidden(_)) => "forbidden",
51 Err(GitAuthError::NotFound(_)) => "not_found",
52 Err(_) => "error",
53 };
54 counter!("gitauth_authorize_total", "outcome" => outcome).increment(1);
55}
56
57fn outcome_for<T>(result: &GitAuthResult<T>) -> &'static str {
58 match result {
59 Ok(_) => OUTCOME_SUCCESS,
60 Err(_) => "error",
61 }
62}
No newline at end of file
Modified lib/gitauth/permission.rs View lib/gitauth/permission.rs
@@ -8,8 +8,6 @@
88use crate::error::{GitAuthError, GitAuthResult};
99use crate::resolve::ResolvedRepo;
1010
11/// Space-level permission that marks a member as an administrator of the
12/// space (mirrors `space::features::authz::MANAGE_MEMBERS`).
1311pub const SPACE_ADMIN_PERMISSION: &str = "manage_members";
1412
1513pub fn channel_role_for(member: Option<&channel_members::Model>) -> Option<ChannelRole> {
@@ -81,8 +79,6 @@
8179 }))
8280}
8381
84/// Enforce read/write access for `actor` against a resolved repository,
85/// delegating role grading to `repo::permission::effective_access`.
8682pub async fn check_access(
8783 db: &impl ConnectionTrait,
8884 actor: &ActorContext,
Modified lib/gitauth/resolve.rs View lib/gitauth/resolve.rs
@@ -13,12 +13,6 @@
1313 pub space: spaces::Model,
1414}
1515
16/// Resolve `/{space_name}/{repo_name}` to a concrete repository.
17///
18/// The space segment matches `spaces.lower_name`; the repository must live in
19/// one of that space's channels and match `repos.lower_name`. A name that maps
20/// to more than one repository (cross-channel collision) is rejected as
21/// ambiguous.
2216pub async fn resolve_repo(
2317 db: &impl ConnectionTrait,
2418 space_name: &str,
Modified lib/gitauth/service.rs View lib/gitauth/service.rs
@@ -10,15 +10,14 @@
1010
1111use crate::config::GitAuthConfig;
1212use crate::error::{GitAuthError, GitAuthResult};
13use crate::metrics;
1314use crate::permission;
1415use crate::resolve::{self, ResolvedRepo};
1516
1617#[derive(Clone)]
1718pub struct GitAuthService {
1819 pub db: Db,
19 /// Optional Redis-backed cache, reserved for access-token blacklist /
20 /// rate-limit integration. `None` disables the cache (unit tests).
21 pub kv: Option<Arc<RedisCache>>,
20 pub kv: Option<Arc<RedisCache>>,
2221 pub jwt: JwtState,
2322 pub config: GitAuthConfig,
2423}
@@ -38,69 +37,76 @@
3837 self
3938 }
4039
41 /// Resolve `/{space_name}/{repo_name}` to a repository without any
42 /// authentication check.
43 pub async fn resolve_repo(
40 pub async fn resolve_repo(
4441 &self,
4542 space_name: &str,
4643 repo_name: &str,
4744 ) -> GitAuthResult<ResolvedRepo> {
48 let db = self.db.reader();
49 resolve::resolve_repo(&db, space_name, repo_name).await
45 let result: GitAuthResult<ResolvedRepo> = async {
46 let db = self.db.reader();
47 resolve::resolve_repo(&db, space_name, repo_name).await
48 }
49 .await;
50 metrics::record_resolve(&result);
51 result
5052 }
5153
52 /// Authenticate a git client presented as HTTP Basic credentials.
53 ///
54 /// The password is accepted either as a JWT access token (validated
55 /// against the access-token signing keys) or as the user's login password.
56 pub async fn authenticate(
54 pub async fn authenticate(
5755 &self,
5856 username: &str,
5957 password: &str,
6058 ) -> GitAuthResult<AuthenticatedUser> {
61 let db = self.db.reader();
62 let Some(user) = users::Entity::find()
63 .filter(users::Column::LowerName.eq(username.to_lowercase()))
64 .one(&db)
65 .await?
66 else {
67 return Err(GitAuthError::Unauthorized("invalid credentials".into()));
68 };
69 if !user.is_active || user.prohibit_login {
70 return Err(GitAuthError::Unauthorized("account is not active".into()));
59 let result: GitAuthResult<AuthenticatedUser> = async {
60 let db = self.db.reader();
61 let Some(user) = users::Entity::find()
62 .filter(users::Column::LowerName.eq(username.to_lowercase()))
63 .one(&db)
64 .await?
65 else {
66 return Err(GitAuthError::Unauthorized("invalid credentials".into()));
67 };
68 if !user.is_active || user.prohibit_login {
69 return Err(GitAuthError::Unauthorized("account is not active".into()));
70 }
71
72 let user_id = self.verify_credentials(&user, password).await?;
73 let actor = ActorContext {
74 user_id: Some(user_id),
75 scopes: Default::default(),
76 is_site_admin: user.is_admin,
77 channel_role: None,
78 space_role: None,
79 };
80 Ok(AuthenticatedUser { user_id, actor })
7181 }
72
73 let user_id = self.verify_credentials(&user, password).await?;
74 let actor = ActorContext {
75 user_id: Some(user_id),
76 scopes: Default::default(),
77 is_site_admin: user.is_admin,
78 channel_role: None,
79 space_role: None,
80 };
81 Ok(AuthenticatedUser { user_id, actor })
82 .await;
83 metrics::record_authenticate(&result);
84 result
8285 }
8386
84 /// Resolve the repository and enforce `required` access on behalf of
85 /// `actor`. Convenience wrapper used by protocol handlers.
86 pub async fn authorize(
87 pub async fn authorize(
8788 &self,
8889 space_name: &str,
8990 repo_name: &str,
9091 actor: &ActorContext,
9192 required: RepoAccess,
9293 ) -> GitAuthResult<ResolvedRepo> {
93 let db = self.db.reader();
94 let resolved = resolve::resolve_repo(&db, space_name, repo_name).await?;
95 permission::check_access(
96 &db,
97 actor,
98 &resolved,
99 required,
100 self.config.allow_anonymous_read,
101 )
102 .await?;
103 Ok(resolved)
94 let result: GitAuthResult<ResolvedRepo> = async {
95 let db = self.db.reader();
96 let resolved = resolve::resolve_repo(&db, space_name, repo_name).await?;
97 permission::check_access(
98 &db,
99 actor,
100 &resolved,
101 required,
102 self.config.allow_anonymous_read,
103 )
104 .await?;
105 Ok(resolved)
106 }
107 .await;
108 metrics::record_authorize(&result);
109 result
104110 }
105111
106112 async fn verify_credentials(&self, user: &users::Model, password: &str) -> GitAuthResult<Uuid> {
Modified lib/gitauth/testing.rs View lib/gitauth/testing.rs
@@ -7,7 +7,6 @@
77 db.execute(&stmt).await.expect("create table");
88}
99
10/// Build an in-memory sqlite database with the tables needed by gitauth.
1110pub async fn setup_db() -> DatabaseConnection {
1211 let db = Database::connect("sqlite::memory:")
1312 .await