Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/mono-engine-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
deploy-aws:
needs: manifest
if: ${{ github.repository == 'gitmono-dev/mega' }}
if: false # disabled
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/orion-server-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
deploy-aws:
needs: manifest
if: ${{ github.repository == 'gitmono-dev/mega' }}
if: false # disabled
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/web-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
deploy-aws:
needs: manifest
if: ${{ github.repository == 'gitmono-dev/mega' }}
if: false # disabled
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/web-sync-server-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
deploy-aws:
needs: build-and-push
if: ${{ github.repository == 'gitmono-dev/mega' }}
if: false # disabled
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ clap = "4.6.4"

#====
tokio = "1.53.1"
tokio-stream = "0.1.18"
tokio-stream = "0.1.19"
tokio-util = "0.7.19"
async-trait = "0.1.89"
async-stream = "0.3.6"
Expand Down
3 changes: 2 additions & 1 deletion ceres/src/model/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ pub struct AdminListResponse {
pub admins: Vec<String>,
}

/// Request body for generating `.mega_cedar.json` content from admin usernames.
/// Request body for generating `.mega_cedar.json` content from admin GitHub logins.
#[derive(Debug, Deserialize, ToSchema)]
pub struct GenerateCedarRequest {
/// GitHub login names used as Cedar `User` euids (e.g. `octocat`).
pub admins: Vec<String>,
}

Expand Down
2 changes: 1 addition & 1 deletion clients/orion-scheduler-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ path = "src/lib.rs"

[dependencies]
common = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
reqwest = { workspace = true, features = ["json", "stream"] }
anyhow = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
Expand Down
60 changes: 60 additions & 0 deletions clients/orion-scheduler-client/src/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,60 @@ impl OrionSchedulerHttpClient {
))
}
}

/// Open an SSE stream of live Orion runner logs (`GET /logs/orion/stream`).
///
/// Prefer `vm_id`; when absent, `domain` is used. At least one must be set.
/// The returned response body is a long-lived `text/event-stream` — do not
/// apply a short request timeout.
pub async fn stream_orion_logs(
&self,
vm_id: Option<&str>,
domain: Option<&str>,
) -> anyhow::Result<reqwest::Response> {
if vm_id.is_none() && domain.is_none() {
return Err(anyhow::anyhow!(
"stream_orion_logs requires vm_id or domain"
));
}

let mut url = format!("{}/logs/orion/stream?", self.base_url);
let mut params: Vec<String> = Vec::new();
if let Some(id) = vm_id {
params.push(format!("vm_id={}", urlencoding_query(id)));
}
if let Some(d) = domain {
params.push(format!("domain={}", urlencoding_query(d)));
}
url.push_str(&params.join("&"));

let req = self.client.get(&url);
let res = self.auth_headers(req).send().await?;
let status = res.status();
if status.is_success() {
Ok(res)
} else {
let body = res.text().await.unwrap_or_default();
Err(anyhow::anyhow!(
"Scheduler stream_orion_logs failed ({}): {}",
status,
body
))
}
}
}

fn urlencoding_query(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for b in value.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}

#[cfg(test)]
Expand All @@ -139,4 +193,10 @@ mod tests {
let client = OrionSchedulerHttpClient::new("http://127.0.0.1:8080/", "");
assert_eq!(client.base_url, "http://127.0.0.1:8080");
}

#[test]
fn urlencoding_query_encodes_reserved_bytes() {
assert_eq!(urlencoding_query("a b"), "a%20b");
assert_eq!(urlencoding_query("orion.gitmega.com"), "orion.gitmega.com");
}
}
11 changes: 10 additions & 1 deletion clients/orion-scheduler-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! HTTP client for orion-scheduler VM provisioning (`/webhook`, `/status`, `/vms/{id}`).
//! HTTP client for orion-scheduler VM provisioning (`/webhook`, `/status`, `/vms/{id}`, logs SSE).

mod http_client;

Expand Down Expand Up @@ -95,4 +95,13 @@ impl OrionSchedulerClient {
pub async fn get_status(&self) -> anyhow::Result<SchedulerStatusResponse> {
self.http.get_status().await
}

/// Proxy-friendly SSE stream of runner / orion-client startup logs.
pub async fn stream_orion_logs(
&self,
vm_id: Option<&str>,
domain: Option<&str>,
) -> anyhow::Result<reqwest::Response> {
self.http.stream_orion_logs(vm_id, domain).await
}
}
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ When the `mono` HTTP server is running (default port `8000`):
| Swagger UI | `http://localhost:8000/swagger-ui` |
| OpenAPI JSON | `http://localhost:8000/api/openapi.json` |

REST handlers are defined in `mono/src/api/` with `utoipa` annotations. Do not maintain a separate hand-written API catalog.
REST handlers are defined in `mono/src/api/` with `utoipa` annotations. Do not maintain a separate hand-written API catalog. Frontend typed clients are generated from those specs — do not hand-edit `moon/api/gen/gitmono.json` or `moon/packages/types/generated.ts`; see [moon/api/README.md](../moon/api/README.md).

### Git LFS

Expand Down
4 changes: 4 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Database migrations apply automatically on first `Storage::new` when the `migrat

Swagger UI: `http://localhost:8000/swagger-ui` (default HTTP port).

## Frontend OpenAPI client types

Do **not** hand-edit `moon/api/gen/gitmono.json` or `moon/packages/types/generated.ts`. Export OpenAPI from the running services and regenerate with `moon/script/gen-client`. See [moon/api/README.md](../moon/api/README.md).

## Post-start initialization (optional)

To seed Buckal bundles and third-party imports via API:
Expand Down
14 changes: 9 additions & 5 deletions mono/src/api/api_common/group_permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ use http::StatusCode;
use crate::api::{MonoApiServiceState, error::ApiError, oauth::model::LoginUser};

pub async fn ensure_admin(state: &MonoApiServiceState, user: &LoginUser) -> Result<(), ApiError> {
if state
.services()
.admin()
.check_is_admin(&user.username)
.await?
let admin = state.services().admin();
let cedar_id = user.cedar_user_id();

// Prefer GitHub login (cedar_user_id). Also accept Campsite username while
// `.mega_cedar.json` is still migrating from username → github_login.
if admin.check_is_admin(cedar_id).await?
|| (cedar_id != user.username.as_str() && admin.check_is_admin(&user.username).await?)
{
return Ok(());
}

tracing::warn!(
actor = %user.username,
cedar_user_id = %cedar_id,
github_login = ?user.github_login,
"admin check failed: access forbidden"
);

Expand Down
14 changes: 7 additions & 7 deletions mono/src/api/guard/cedar_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ pub async fn cedar_guard(
if let Ok(bot) = BotAuth::from_request_parts(&mut parts, &state).await {
("Bot".to_string(), bot.bot_id.to_string())
} else if let Ok(user) = LoginUser::from_request_parts(&mut parts, &state).await {
("User".to_string(), user.username.clone())
("User".to_string(), user.cedar_user_id().to_string())
} else {
("User".to_string(), "reader".to_string())
};
Expand Down Expand Up @@ -219,27 +219,27 @@ mod tests {
let patterns: HashMap<String, String> = HashMap::from([
(
"/{link}/approve".to_string(),
"approveMergeRequest".to_string(),
"approveChangeList".to_string(),
),
("/{link}/close".to_string(), "editMergeRequest".to_string()),
("/{link}/close".to_string(), "editChangeList".to_string()),
(
"/{link}/review/delete".to_string(),
"editMergeRequest".to_string(),
"editChangeList".to_string(),
),
]);

let suffix = "/my-cl-link/approve";
let result = match_operation(suffix, &patterns);
assert_eq!(
result,
Some((ActionEnum::ApproveMergeRequest, "my-cl-link".to_string()))
Some((ActionEnum::ApproveChangeList, "my-cl-link".to_string()))
);

let suffix = "/another-cl-link/close";
let result = match_operation(suffix, &patterns);
assert_eq!(
result,
Some((ActionEnum::EditMergeRequest, "another-cl-link".to_string()))
Some((ActionEnum::EditChangeList, "another-cl-link".to_string()))
);

let suffix = "/no-match-link/delete";
Expand All @@ -250,7 +250,7 @@ mod tests {
let result = match_operation(suffix, &patterns);
assert_eq!(
result,
Some((ActionEnum::EditMergeRequest, "path/subpath".to_string()))
Some((ActionEnum::EditChangeList, "path/subpath".to_string()))
);
}
}
28 changes: 14 additions & 14 deletions mono/src/api/guard/guarded_endpoints.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
{
"/cl": {
"/list": "unprotectedRequest",
"/{link}/reopen": "editMergeRequest",
"/{link}/close": "editMergeRequest",
"/{link}/merge": "approveMergeRequest",
"/{link}/comment": "editMergeRequest",
"/{link}/title": "editMergeRequest",
"/{link}/labels": "editMergeRequest",
"/{link}/assignees": "editMergeRequest",
"/{link}/reviewers": "editMergeRequest",
"/{link}/approve": "approveMergeRequest",
"/{link}/resolve": "editMergeRequest",
"/{link}/reopen": "editChangeList",
"/{link}/close": "editChangeList",
"/{link}/merge": "approveChangeList",
"/{link}/comment": "editChangeList",
"/{link}/title": "editChangeList",
"/{link}/labels": "editChangeList",
"/{link}/assignees": "editChangeList",
"/{link}/reviewers": "editChangeList",
"/{link}/approve": "approveChangeList",
"/{link}/resolve": "editChangeList",
"/{link}/detail": "viewRepo",
"/{link}/mui-tree": "viewRepo",
"/{link}/files-changed": "viewRepo",
"/{link}/files-list": "viewRepo",
"/{link}/merge-box": "viewRepo",
"/{link}/status": "editMergeRequest",
"/{link}/status": "editChangeList",
"/{link}/update-status": "viewRepo",
"/{link}/update-branch": "editMergeRequest",
"/{link}/update-branch": "editChangeList",
"/reviewer/{link}": "viewRepo",
"/reviewer": "editMergeRequest"
"/reviewer": "editChangeList"
}
}
}
18 changes: 18 additions & 0 deletions mono/src/api/oauth/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub struct CampsiteUserJson {
pub id: String,
pub avatar_url: String,
pub email: Option<String>,
#[serde(default)]
pub github_login: Option<String>,
}

impl From<CampsiteUserJson> for LoginUser {
Expand All @@ -15,6 +17,7 @@ impl From<CampsiteUserJson> for LoginUser {
email: value.email.unwrap_or_default(),
avatar_url: value.avatar_url,
campsite_user_id: value.id,
github_login: value.github_login.filter(|s| !s.trim().is_empty()),
}
}
}
Expand Down Expand Up @@ -49,6 +52,7 @@ impl From<TinyshipAuthUserJson> for LoginUser {
username: value.name,
email: value.email.unwrap_or_default(),
avatar_url: value.image.unwrap_or_default(),
github_login: None,
}
}
}
Expand All @@ -57,6 +61,20 @@ impl From<TinyshipAuthUserJson> for LoginUser {
pub struct LoginUser {
pub campsite_user_id: String,
pub username: String,
/// GitHub login when Campsite authenticated via GitHub; used as Cedar User euid.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub github_login: Option<String>,
pub avatar_url: String,
pub email: String,
}

impl LoginUser {
/// Identity for Cedar / `.mega_cedar.json` admin checks: GitHub login when present.
pub fn cedar_user_id(&self) -> &str {
self.github_login
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or(self.username.as_str())
}
}
11 changes: 5 additions & 6 deletions mono/src/api/router/admin_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,10 @@ async fn is_admin_me(
user: LoginUser,
State(state): State<MonoApiServiceState>,
) -> Result<Json<CommonResult<IsAdminResponse>>, ApiError> {
let is_admin = state
.services()
.admin()
.check_is_admin(&user.username)
.await?;
let admin = state.services().admin();
let cedar_id = user.cedar_user_id();
let is_admin = admin.check_is_admin(cedar_id).await?
|| (cedar_id != user.username.as_str() && admin.check_is_admin(&user.username).await?);

Ok(Json(CommonResult::success(Some(IsAdminResponse {
is_admin,
Expand Down Expand Up @@ -100,7 +99,7 @@ async fn admin_list(

/// POST /api/v1/admin/cedar/generate
///
/// Generate `.mega_cedar.json` content from a list of admin usernames.
/// Generate `.mega_cedar.json` content from a list of admin GitHub logins.
/// Only admins can access this endpoint. Does not write to the repository.
#[utoipa::path(
post,
Expand Down
Loading
Loading