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
4 changes: 4 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions apps/server/src/correction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ async fn remote(state: &AppState, user_uuid: &str, project: &Project) -> Option<
if token.trim().is_empty() {
token = state.github.instance_token().unwrap_or_default();
}
let token = crate::github_auth::project_token(state, &project.uuid, token).await;
Some(draft::token_url(repo, Some(&token)))
}

Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/dev_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,8 @@ pub(crate) async fn run_dev_deploy(
.flatten()
.map(|(t,)| crate::secret_box::open_compat(&t))
.filter(|t| !t.trim().is_empty());
// GitHub App : token d'installation restreint au dépôt si le projet est lié.
let deploy_tok = crate::github_auth::deploy_token(state, &project.uuid, token).await;

// Mêmes variables que la prod (jamais loguées).
let env_file = crate::routes::load_env_file_content(&state.pool, &project.uuid).await;
Expand Down Expand Up @@ -585,7 +587,8 @@ pub(crate) async fn run_dev_deploy(
docker_build_context: project.docker_build_context.clone(),
publish_directory: project.publish_directory.clone(),
is_static: project.is_static != 0,
github_token: token,
github_token: deploy_tok.token,
github_token_ephemeral: deploy_tok.ephemeral,
env_file,
proxy_labels: Some(labels),
gpu_nvidia: project.gpu_nvidia != 0,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/draft_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ async fn remote_url(state: &AppState, c: &Ctx) -> Option<String> {
if token.trim().is_empty() {
token = state.github.instance_token().unwrap_or_default();
}
// GitHub App : token d'installation si le projet est lié (URL jamais persistée).
let token = crate::github_auth::project_token(state, &c.project.uuid, token).await;
Some(draft::token_url(repo, Some(&token)))
}

Expand Down
43 changes: 15 additions & 28 deletions apps/server/src/github_app_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use axum::{
use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
use base64::Engine;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use serde_json::{json, Value};
use sqlx::PgPool;

Expand Down Expand Up @@ -405,40 +405,27 @@ async fn manifest_callback(

// ───────────────────── Installation ─────────────────────

#[derive(Serialize)]
struct JwtClaims {
iat: i64,
exp: i64,
iss: String,
}

/// JWT d'app RS256 (iat = now−60, exp = now+540). Sera déplacé dans `crates/github` en P2.
pub fn app_jwt(app_id: i64, pem: &[u8]) -> Result<Redacted<String>, String> {
let key = jsonwebtoken::EncodingKey::from_rsa_pem(pem).map_err(|_| "clé privée invalide")?;
let now = chrono::Utc::now().timestamp();
let claims = JwtClaims {
iat: now - 60,
exp: now + 540,
iss: app_id.to_string(),
};
jsonwebtoken::encode(
&jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256),
&claims,
&key,
)
.map(Redacted)
.map_err(|_| "signature JWT impossible".into())
}

async fn load_app_jwt(pool: &PgPool) -> Result<Redacted<String>, String> {
/// Identifiants de l'app (clé privée descellée, jamais journalisée).
pub async fn load_credentials(
pool: &PgPool,
) -> Result<devforge_github::app_auth::AppCredentials, String> {
let row: Option<(i64, String)> =
sqlx::query_as("SELECT app_id, private_key_enc FROM github_app WHERE id = 1")
.fetch_optional(pool)
.await
.map_err(|e| e.to_string())?;
let (app_id, enc) = row.ok_or("aucune app GitHub configurée")?;
let pem = crate::secret_box::open(&enc).map_err(|e| e.to_string())?;
app_jwt(app_id, &pem)
let pem = String::from_utf8(pem.to_vec()).map_err(|_| "clé privée illisible")?;
Ok(devforge_github::app_auth::AppCredentials::new(app_id, pem))
}

async fn load_app_jwt(pool: &PgPool) -> Result<Redacted<String>, String> {
let creds = load_credentials(pool).await?;
creds
.app_jwt()
.map(|j| Redacted(j.expose().to_string()))
.map_err(|e| e.to_string())
}

#[derive(Deserialize)]
Expand Down
43 changes: 0 additions & 43 deletions apps/server/src/github_app_routes_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,46 +299,3 @@ async fn installation_is_verified_then_stored_with_repos() {
assert_eq!(i["missing_permissions"].as_array().unwrap().len(), 4);
assert!(!st.to_string().contains("ghs_inst"));
}

#[test]
fn app_jwt_rs256_claims() {
let dir = std::env::temp_dir().join(format!("df-jwt-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
let key = dir.join("k.pem");
let pubk = dir.join("k.pub");
let ok = std::process::Command::new("openssl")
.args(["genrsa", "-traditional", "-out"])
.arg(&key)
.arg("2048")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
&& std::process::Command::new("openssl")
.args(["rsa", "-pubout", "-in"])
.arg(&key)
.arg("-out")
.arg(&pubk)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !ok {
eprintln!("openssl indisponible — test JWT ignoré");
return;
}
let pem = std::fs::read(&key).unwrap();
let jwt = app_jwt(4242, &pem).unwrap();
assert_eq!(format!("{jwt:?}"), "***");
let mut v = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
v.set_issuer(&["4242"]);
v.set_required_spec_claims(&["exp", "iat", "iss"]);
let data = jsonwebtoken::decode::<Value>(
jwt.expose(),
&jsonwebtoken::DecodingKey::from_rsa_pem(&std::fs::read(&pubk).unwrap()).unwrap(),
&v,
)
.unwrap();
let c = data.claims;
assert!(c["exp"].as_i64().unwrap() - c["iat"].as_i64().unwrap() <= 600);
let _ = std::fs::remove_dir_all(&dir);
assert!(app_jwt(1, b"not a key").is_err());
}
Loading
Loading