Skip to content

Commit 408db78

Browse files
fix(moon): clear ESLint errors blocking test-web-ui
Satisfy padding-line-between-statements in MergedItem and drop unused catch bindings flagged on PR CI. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7c5aa8a commit 408db78

15 files changed

Lines changed: 313 additions & 57 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ceres/src/application/api_service/mono/admin/bot.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl AdminApplicationService {
105105
})
106106
}
107107

108-
/// Ensure mega-init bot + fresh push token (for unauthenticated bootstrap-init).
108+
/// Ensure mega-init bot + fresh push token (for secret-gated bootstrap-init).
109109
pub async fn ensure_init_bot_token(
110110
&self,
111111
) -> Result<crate::model::bots::BootstrapInitBotResponse, MegaError> {
@@ -156,12 +156,16 @@ impl AdminApplicationService {
156156
.await
157157
}
158158

159-
/// Check whether a bot has sufficient permission on a given resource.
159+
/// Check whether a bot has sufficient permission on a given resource path.
160+
///
161+
/// `resource_id` is treated as a repository path (same as receive-pack).
162+
/// Requires an Enabled installation covering that path and a permission
163+
/// scope that satisfies `required_permission`.
160164
pub async fn check_bot_permission(
161165
&self,
162166
bot_id: i64,
163167
_resource_type: callisto::sea_orm_active_enums::ResourceTypeEnum,
164-
_resource_id: &str,
168+
resource_id: &str,
165169
required_permission: PermissionEnum,
166170
) -> Result<bool, MegaError> {
167171
let bots_storage = self.ctx.storage().bots_storage();
@@ -175,19 +179,23 @@ impl AdminApplicationService {
175179
return Ok(false);
176180
}
177181

178-
let installations = bots_storage.get_installed_bot_by_id(bot_id).await?;
179-
let has_enabled_installation = installations
180-
.iter()
181-
.any(|inst| inst.status == InstallationBotStatusEnum::Enabled);
182-
183-
if !has_enabled_installation {
182+
if !scope_satisfies_permission(&bot.permission_scope, &required_permission) {
184183
return Ok(false);
185184
}
186185

187-
Ok(scope_satisfies_permission(
188-
&bot.permission_scope,
189-
&required_permission,
190-
))
186+
// Push-level checks also enforce installation coverage on the path.
187+
if matches!(
188+
required_permission,
189+
PermissionEnum::Write | PermissionEnum::Admin
190+
) {
191+
return bots_storage.bot_may_push_to_path(&bot, resource_id).await;
192+
}
193+
194+
// Read: any Enabled installation is enough (scope already checked).
195+
let installations = bots_storage.get_installed_bot_by_id(bot_id).await?;
196+
Ok(installations
197+
.iter()
198+
.any(|inst| inst.status == InstallationBotStatusEnum::Enabled))
191199
}
192200
}
193201

ceres/src/model/bots.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ pub struct CreateBotTokenResponse {
126126
pub token_plain: String,
127127
}
128128

129-
/// Response for unauthenticated mega-init bot bootstrap.
129+
/// Response for mega-init bot bootstrap (`POST /bots/bootstrap-init`).
130130
///
131+
/// Requires header `X-Mega-Init-Secret` matching `MEGA_INIT_BOOTSTRAP_SECRET`.
131132
/// `token` is a `bot_` push token returned once; use as Bearer (or Basic password).
132133
#[derive(Serialize, ToSchema)]
133134
pub struct BootstrapInitBotResponse {

jupiter/src/storage/bots_storage.rs

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{collections::HashSet, ops::Deref};
22

33
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
44
use callisto::{
5-
bot_installations, bot_keys, bot_tokens, bots,
5+
bot_installations, bot_keys, bot_tokens, bots, git_repo,
66
sea_orm_active_enums::{
77
BotStatusEnum, InstallationBotStatusEnum, InstallationTargetTypeEnum, PermissionScopeEnum,
88
},
@@ -32,6 +32,8 @@ use crate::{
3232
const BOT_TOKEN_PREFIX: &str = "bot_";
3333
const BOT_TOKEN_RANDOM_LEN: usize = 32;
3434
const BOT_TOKEN_HMAC_KEY_ENV: &str = "MEGA_BOT_TOKEN_HMAC_SECRET";
35+
/// Organization installation target_id for system bots (`organization_id` is null).
36+
const SYSTEM_ORG_INSTALL_TARGET_ID: i64 = 0;
3537

3638
#[derive(Clone)]
3739
pub struct BotsStorage {
@@ -281,8 +283,10 @@ impl BotsStorage {
281283

282284
/// Ensure the fixed mega-init bot exists and return a fresh push token.
283285
///
284-
/// Creates bot `mega-init` if missing, revokes any existing `mega-init-push`
285-
/// tokens, then issues a new token (plaintext returned once).
286+
/// Creates bot `mega-init` if missing, ensures a system Organization
287+
/// installation (so receive-pack accepts its Write token), revokes any
288+
/// existing `mega-init-push` tokens, then issues a new token (plaintext
289+
/// returned once).
286290
pub async fn ensure_init_bot_token(&self) -> Result<(bots::Model, String), MegaError> {
287291
const INIT_BOT_NAME: &str = "mega-init";
288292
const INIT_TOKEN_NAME: &str = "mega-init-push";
@@ -303,6 +307,8 @@ impl BotsStorage {
303307
)));
304308
}
305309

310+
self.ensure_system_org_installation(bot.id).await?;
311+
306312
self.revoke_bot_tokens_by_name(bot.id, INIT_TOKEN_NAME)
307313
.await?;
308314
let (_model, token_plain) = self
@@ -311,6 +317,110 @@ impl BotsStorage {
311317
Ok((bot, token_plain))
312318
}
313319

320+
/// Ensure an Enabled Organization installation for a system bot (target_id 0).
321+
pub async fn ensure_system_org_installation(&self, bot_id: i64) -> Result<(), MegaError> {
322+
let existing = bot_installations::Entity::find()
323+
.filter(bot_installations::Column::BotId.eq(bot_id))
324+
.filter(
325+
bot_installations::Column::TargetType.eq(InstallationTargetTypeEnum::Organization),
326+
)
327+
.filter(bot_installations::Column::TargetId.eq(SYSTEM_ORG_INSTALL_TARGET_ID))
328+
.one(self.get_connection())
329+
.await?;
330+
331+
if let Some(inst) = existing {
332+
if inst.status != InstallationBotStatusEnum::Enabled {
333+
let mut active: bot_installations::ActiveModel = inst.into_active_model();
334+
active.status = Set(InstallationBotStatusEnum::Enabled);
335+
active.update(self.get_connection()).await?;
336+
}
337+
return Ok(());
338+
}
339+
340+
self.install_bot(
341+
bot_id,
342+
InstallationTargetTypeEnum::Organization,
343+
SYSTEM_ORG_INSTALL_TARGET_ID,
344+
0,
345+
)
346+
.await?;
347+
Ok(())
348+
}
349+
350+
/// Whether `permission_scope` is sufficient for git receive-pack (push).
351+
pub fn scope_allows_push(scope: &PermissionScopeEnum) -> bool {
352+
matches!(
353+
scope,
354+
PermissionScopeEnum::Write | PermissionScopeEnum::Admin
355+
)
356+
}
357+
358+
/// Resolve an import `git_repo.id` for `repo_path`, if registered.
359+
pub async fn find_repo_id_by_path(&self, repo_path: &str) -> Result<Option<i64>, MegaError> {
360+
let path = repo_path.trim_end_matches('/');
361+
let path = if path.is_empty() { "/" } else { path };
362+
363+
if let Some(repo) = git_repo::Entity::find()
364+
.filter(git_repo::Column::RepoPath.eq(path))
365+
.one(self.get_connection())
366+
.await?
367+
{
368+
return Ok(Some(repo.id));
369+
}
370+
371+
// Longest registered prefix (same idea as find_git_repo_like_path).
372+
let like = git_repo::Entity::find()
373+
.filter(Expr::cust(format!("'{path}' LIKE repo_path || '%'")))
374+
.order_by_desc(Expr::cust("LENGTH(repo_path)"))
375+
.one(self.get_connection())
376+
.await?;
377+
Ok(like.map(|r| r.id))
378+
}
379+
380+
/// True if the bot may authenticate a receive-pack push to `repo_path`.
381+
///
382+
/// Requires Write/Admin scope and an Enabled installation covering the
383+
/// target: Repository install for the resolved git_repo, and/or an
384+
/// Organization install for the bot's org (or system target_id 0).
385+
pub async fn bot_may_push_to_path(
386+
&self,
387+
bot: &bots::Model,
388+
repo_path: &str,
389+
) -> Result<bool, MegaError> {
390+
if bot.status != BotStatusEnum::Enabled {
391+
return Ok(false);
392+
}
393+
if !Self::scope_allows_push(&bot.permission_scope) {
394+
return Ok(false);
395+
}
396+
397+
let installations = self.get_installed_bot_by_id(bot.id).await?;
398+
let enabled: Vec<_> = installations
399+
.into_iter()
400+
.filter(|i| i.status == InstallationBotStatusEnum::Enabled)
401+
.collect();
402+
if enabled.is_empty() {
403+
return Ok(false);
404+
}
405+
406+
let org_target_id = bot.organization_id.unwrap_or(SYSTEM_ORG_INSTALL_TARGET_ID);
407+
let has_org_install = enabled.iter().any(|i| {
408+
i.target_type == InstallationTargetTypeEnum::Organization
409+
&& i.target_id == org_target_id
410+
});
411+
412+
let repo_id = self.find_repo_id_by_path(repo_path).await?;
413+
if let Some(repo_id) = repo_id {
414+
let has_repo_install = enabled.iter().any(|i| {
415+
i.target_type == InstallationTargetTypeEnum::Repository && i.target_id == repo_id
416+
});
417+
return Ok(has_repo_install || has_org_install);
418+
}
419+
420+
// Monorepo / unregistered path: Organization install only.
421+
Ok(has_org_install)
422+
}
423+
314424
/// Generate a new bot token, persist its HMAC-SHA256 hash and return the model with plaintext.
315425
pub async fn generate_bot_token(
316426
&self,

mono/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,5 @@ mimalloc = { workspace = true }
8585
tempfile = { workspace = true }
8686
jupiter-migrate = { workspace = true }
8787

88-
# [target.'cfg(target_os = "linux")'.dev-dependencies]
89-
# qlean = "0.3.0"
88+
[target.'cfg(target_os = "linux")'.dev-dependencies]
89+
qlean = { git = "https://github.com/benjamin-747/qlean.git", rev = "c3bf91e0f4020b580fd5bfe5d3d7d22bc3754651" }

mono/src/api/router/bot_router.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use api_model::common::CommonResult;
33
use axum::{
44
Json,
55
extract::{Path, State},
6+
http::{HeaderMap, StatusCode},
67
};
78
use ceres::model::bots::{
89
BootstrapInitBotResponse, BotRes, ChangeInstallationStatus, CreateBotTokenRequest,
@@ -22,6 +23,68 @@ const MAX_EXPIRES_IN_SECS: i64 = 365 * 24 * 3600 * 10;
2223
/// Minimum allowed expires_in in seconds.
2324
const MIN_EXPIRES_IN_SECS: i64 = 1;
2425

26+
/// Env var holding the shared secret that gates `POST /bots/bootstrap-init`.
27+
const INIT_BOOTSTRAP_SECRET_ENV: &str = "MEGA_INIT_BOOTSTRAP_SECRET";
28+
/// Header the mega-init Job / script must send.
29+
const INIT_BOOTSTRAP_SECRET_HEADER: &str = "x-mega-init-secret";
30+
const INIT_BOOTSTRAP_SECRET_MIN_LEN: usize = 32;
31+
32+
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
33+
if a.len() != b.len() {
34+
return false;
35+
}
36+
let mut diff = 0u8;
37+
for (x, y) in a.iter().zip(b.iter()) {
38+
diff |= x ^ y;
39+
}
40+
diff == 0
41+
}
42+
43+
/// Fail closed unless `MEGA_INIT_BOOTSTRAP_SECRET` is set and matches the request header.
44+
fn ensure_init_bootstrap_secret(headers: &HeaderMap) -> Result<(), ApiError> {
45+
let expected = match std::env::var(INIT_BOOTSTRAP_SECRET_ENV) {
46+
Ok(v) => {
47+
let trimmed = v.trim().to_owned();
48+
if trimmed.len() < INIT_BOOTSTRAP_SECRET_MIN_LEN {
49+
tracing::error!(
50+
env = INIT_BOOTSTRAP_SECRET_ENV,
51+
min_len = INIT_BOOTSTRAP_SECRET_MIN_LEN,
52+
"init bootstrap secret is too short; refusing bootstrap-init"
53+
);
54+
return Err(ApiError::with_status(
55+
StatusCode::UNAUTHORIZED,
56+
anyhow!("bootstrap-init is not configured"),
57+
));
58+
}
59+
trimmed
60+
}
61+
Err(_) => {
62+
tracing::error!(
63+
env = INIT_BOOTSTRAP_SECRET_ENV,
64+
"init bootstrap secret is unset; refusing bootstrap-init"
65+
);
66+
return Err(ApiError::with_status(
67+
StatusCode::UNAUTHORIZED,
68+
anyhow!("bootstrap-init is not configured"),
69+
));
70+
}
71+
};
72+
73+
let provided = headers
74+
.get(INIT_BOOTSTRAP_SECRET_HEADER)
75+
.and_then(|v| v.to_str().ok())
76+
.unwrap_or("");
77+
78+
if !constant_time_eq(expected.as_bytes(), provided.as_bytes()) {
79+
tracing::warn!("bootstrap-init rejected: missing or invalid X-Mega-Init-Secret");
80+
return Err(ApiError::with_status(
81+
StatusCode::UNAUTHORIZED,
82+
anyhow!("Unauthorized"),
83+
));
84+
}
85+
Ok(())
86+
}
87+
2588
async fn ensure_bot_exists(state: &MonoApiServiceState, bot_id: i64) -> Result<(), ApiError> {
2689
let bot = state.services().admin().get_bot_by_id(bot_id).await?;
2790
if bot.is_none() {
@@ -46,21 +109,29 @@ pub fn routers() -> OpenApiRouter<MonoApiServiceState> {
46109
)
47110
}
48111

49-
/// Bootstrap mega-init bot + push token (no auth; same class as merge-no-auth).
112+
/// Bootstrap mega-init bot + push token.
50113
///
114+
/// Gated by shared secret `MEGA_INIT_BOOTSTRAP_SECRET` via header
115+
/// `X-Mega-Init-Secret` (not LoginUser — for the onprem mega-init Job / CronJob).
51116
/// Creates the `mega-init` bot if needed and returns a fresh `bot_` token for
52-
/// git HTTP push. Intended for onprem mega-init Job / CronJob.
117+
/// git HTTP push.
53118
#[utoipa::path(
54119
post,
55120
path = "/bootstrap-init",
121+
params(
122+
("X-Mega-Init-Secret" = String, Header, description = "Must match MEGA_INIT_BOOTSTRAP_SECRET on mono-engine")
123+
),
56124
responses(
57-
(status = 200, body = CommonResult<BootstrapInitBotResponse>, content_type = "application/json")
125+
(status = 200, body = CommonResult<BootstrapInitBotResponse>, content_type = "application/json"),
126+
(status = 401, description = "Missing/invalid X-Mega-Init-Secret or secret not configured"),
58127
),
59128
tag = BOT_TAG
60129
)]
61130
async fn bootstrap_init_bot(
62131
State(state): State<MonoApiServiceState>,
132+
headers: HeaderMap,
63133
) -> Result<Json<CommonResult<BootstrapInitBotResponse>>, ApiError> {
134+
ensure_init_bootstrap_secret(&headers)?;
64135
let resp = state.services().admin().ensure_init_bot_token().await?;
65136
Ok(Json(CommonResult::success(Some(resp))))
66137
}

mono/src/git_protocol/http.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ fn basic_auth_password_from_authorization_value(value: &str) -> Option<String> {
7777

7878
/// Uses [`crate::api::oauth::login_user_from_mono_access_token`] for user access tokens,
7979
/// or bot tokens (`bot_` prefix) via [`jupiter::storage::bots_storage::BotsStorage::find_bot_by_token`].
80+
/// Bot tokens additionally require Write/Admin scope and an Enabled installation
81+
/// covering the receive-pack target path.
8082
/// Supports both Bearer tokens and Basic Auth (with token as password).
8183
async fn git_receive_pack_auth(
8284
state: &TransportRuntime,
@@ -99,15 +101,31 @@ async fn git_receive_pack_auth(
99101

100102
// Bot tokens are prefixed with `bot_`.
101103
if token.starts_with("bot_") {
102-
let found = state
103-
.storage
104-
.bots_storage()
104+
let bots = state.storage.bots_storage();
105+
let found = bots
105106
.find_bot_by_token(&token)
106107
.await
107108
.map_err(mega_to_protocol_error)?;
108109
let Some((bot, _)) = found else {
109110
return Ok(false);
110111
};
112+
113+
let repo_path = pack_protocol.repo_path.to_string_lossy();
114+
let may_push = bots
115+
.bot_may_push_to_path(&bot, repo_path.as_ref())
116+
.await
117+
.map_err(mega_to_protocol_error)?;
118+
if !may_push {
119+
tracing::warn!(
120+
bot_id = bot.id,
121+
bot_name = %bot.name,
122+
repo_path = %repo_path,
123+
permission_scope = ?bot.permission_scope,
124+
"bot token rejected for receive-pack: insufficient scope or no enabled installation"
125+
);
126+
return Ok(false);
127+
}
128+
111129
let username = bot.name;
112130
pack_protocol.auth.username = Some(username.clone());
113131
pack_protocol.auth.authenticated_user = Some(PushUserInfo { username });

0 commit comments

Comments
 (0)