Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Srynetix committed Apr 15, 2024
1 parent 720f8a1 commit 6cf1301
Show file tree
Hide file tree
Showing 76 changed files with 1,631 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ BOT_SERVER_BIND_PORT=8008
BOT_SERVER_DISABLE_WEBHOOK_SIGNATURE=
# Enable welcome comments
BOT_SERVER_ENABLE_WELCOME_COMMENTS=
# Admin private key
BOT_SERVER_ADMIN_PRIVATE_KEY=
# Tenor API key
BOT_TENOR_API_KEY=
# Debug mode
Expand Down
5 changes: 5 additions & 0 deletions Cargo.lock

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

5 changes: 4 additions & 1 deletion crates/prbot-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ pub struct ServerConfig {
pub webhook_secret: String,
/// Disable webhook signature verification.
pub disable_webhook_signature: bool,
/// Enable welcome coments.
/// Enable welcome comments.
pub enable_welcome_comments: bool,
/// Admin private key.
pub admin_private_key: String,
}

/// Bot configuration.
Expand Down Expand Up @@ -182,6 +184,7 @@ impl Config {
false,
),
enable_welcome_comments: env_to_bool("BOT_SERVER_ENABLE_WELCOME_COMMENTS", false),
admin_private_key: env_to_str("BOT_SERVER_ADMIN_PRIVATE_KEY", ""),
},
tenor_api_key: env_to_str("BOT_TENOR_API_KEY", ""),
test_debug_mode: env_to_bool("BOT_TEST_DEBUG_MODE", false),
Expand Down
2 changes: 1 addition & 1 deletion crates/prbot-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub use rand;

pub use self::{
errors::{CryptoError, Result},
rsa::RsaUtils,
rsa::{PrivateRsaKey, PublicRsaKey, RsaUtils},
sig::Signature,
};

Expand Down
28 changes: 26 additions & 2 deletions crates/prbot-crypto/src/rsa.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::fmt::Display;

use jsonwebtoken::{DecodingKey, EncodingKey};
use rsa::pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey};
use rsa::{
pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, EncodeRsaPublicKey},
RsaPrivateKey,
};

use super::{CryptoError, Result};

Expand All @@ -15,17 +18,38 @@ pub struct PublicRsaKey(String);
pub struct PrivateRsaKey(String);

impl PublicRsaKey {
pub fn new(value: String) -> Self {
Self(value)
}

/// Get key as string.
pub fn as_str(&self) -> &str {
&self.0
}
}

impl PrivateRsaKey {
pub fn new(value: String) -> Self {
Self(value)
}

/// Get key as string.
pub fn as_str(&self) -> &str {
&self.0
}

/// Extract public key.
pub fn extract_public_key(&self) -> PublicRsaKey {
let priv_key: RsaPrivateKey = RsaPrivateKey::from_pkcs1_pem(&self.0).unwrap();
let value = priv_key.to_public_key();

PublicRsaKey(
value
.to_pkcs1_pem(rsa::pkcs8::LineEnding::LF)
.unwrap()
.to_string(),
)
}
}

impl Display for PublicRsaKey {
Expand All @@ -43,7 +67,7 @@ impl Display for PrivateRsaKey {
impl RsaUtils {
/// Generate a RSA key-pair.
pub fn generate_rsa_keys() -> (PrivateRsaKey, PublicRsaKey) {
use ::rsa::{RsaPrivateKey, RsaPublicKey};
use ::rsa::RsaPublicKey;
use rand::rngs::OsRng;

let mut rng = OsRng;
Expand Down
4 changes: 3 additions & 1 deletion crates/prbot-database-pg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ where
A: Acquire<'a>,
<A::Connection as Deref>::Target: Migrate,
{
info!("Running database migrations...");

sqlx::migrate!("./migrations")
.run(migrator)
.await
Expand All @@ -30,7 +32,7 @@ where
}

pub async fn establish_pool_connection(config: &Config) -> Result<DbPool> {
info!("Trying to establish connection to database pool");
info!("Establishing connection to database pool...");

PgPoolOptions::new()
.acquire_timeout(Duration::from_secs(
Expand Down
7 changes: 7 additions & 0 deletions crates/prbot-frontend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "prbot-frontend"
version = "0.0.0"
authors = ["Denis BOURGE <Srynetix@users.noreply.github.com>"]
edition = "2021"

[dependencies]
1 change: 1 addition & 0 deletions crates/prbot-frontend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions crates/prbot-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ thiserror = { workspace = true }
time = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
percent-encoding = "2.3.1"

[dev-dependencies]
prbot-database-memory = { path = "../prbot-database-memory" }
Expand Down
103 changes: 103 additions & 0 deletions crates/prbot-server/src/admin/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! Admin module.
use actix_web::{web, HttpResponse, Result};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use percent_encoding::{percent_decode, percent_decode_str};
use prbot_models::{ExternalAccount, ExternalAccountRight, MergeRule, PullRequest, PullRequestRule, Repository};
use serde::Serialize;

use crate::server::AppContext;

pub mod validator;

#[derive(Serialize)]
struct ExtendedRepository {
repository: Repository,
pull_requests: Vec<PullRequest>,
merge_rules: Vec<MergeRule>,
pull_request_rules: Vec<PullRequestRule>
}

#[derive(Serialize)]
struct ExtendedExternalAccount {
external_account: ExternalAccount,
rights: Vec<ExternalAccountRight>
}


#[tracing::instrument(skip_all)]
pub(crate) async fn repositories_list(
ctx: web::Data<AppContext>,
_auth: BearerAuth,
) -> Result<HttpResponse> {
let mut output = vec![];
let repositories = ctx.db_service.repositories_all().await.unwrap();

for repository in repositories.into_iter() {
let pull_requests = ctx.db_service.pull_requests_list(&repository.owner, &repository.name).await.unwrap();
let merge_rules = ctx.db_service.merge_rules_list(&repository.owner, &repository.name).await.unwrap();
let pull_request_rules = ctx.db_service.pull_request_rules_list(&repository.owner, &repository.name).await.unwrap();

output.push(ExtendedRepository {
repository,
pull_requests,
merge_rules,
pull_request_rules
})
}

Ok(HttpResponse::Ok().json(&output))
}

#[tracing::instrument(skip_all)]
pub(crate) async fn accounts_list(
ctx: web::Data<AppContext>,
_auth: BearerAuth,
) -> Result<HttpResponse> {
let accounts = ctx.db_service.accounts_all().await.unwrap();
Ok(HttpResponse::Ok().json(&accounts))
}

#[tracing::instrument(skip_all)]
pub(crate) async fn external_accounts_list(
ctx: web::Data<AppContext>,
_auth: BearerAuth,
) -> Result<HttpResponse> {
let mut output = vec![];
let external_accounts = ctx.db_service.external_accounts_all().await.unwrap();
for external_account in external_accounts.into_iter() {
let rights = ctx.db_service.external_account_rights_list(&external_account.username).await.unwrap();
output.push(ExtendedExternalAccount {
external_account,
rights
});
}

Ok(HttpResponse::Ok().json(&output))
}

#[tracing::instrument(skip_all)]
pub(crate) async fn pull_request_rules_create(
ctx: web::Data<AppContext>,
rule: web::Json<PullRequestRule>,
_auth: BearerAuth,
) -> Result<HttpResponse> {
let output = ctx.db_service.pull_request_rules_create(rule.0).await.unwrap();
Ok(HttpResponse::Ok().json(&output))
}

#[tracing::instrument(skip_all)]
pub(crate) async fn pull_request_rules_delete(
ctx: web::Data<AppContext>,
path: web::Path<(u64, String)>,
_auth: BearerAuth,
) -> Result<HttpResponse> {
let repository_id = &path.0;
let repository = ctx.db_service.repositories_get_from_id_expect(*repository_id).await.unwrap();

let rule_name = &path.1;
let rule_name = percent_decode_str(rule_name).decode_utf8_lossy().to_string();
ctx.db_service.pull_request_rules_delete(&repository.owner, &repository.name, &rule_name).await.unwrap();

Ok(HttpResponse::NoContent().finish())
}
104 changes: 104 additions & 0 deletions crates/prbot-server/src/admin/validator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! External API validator.
use std::time::{SystemTime, UNIX_EPOCH};

use actix_web::{dev::ServiceRequest, http::StatusCode, web, Error, ResponseError};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use prbot_config::Config;
use prbot_crypto::{CryptoError, JwtUtils, PrivateRsaKey};
use prbot_sentry::sentry;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::server::AppContext;

/// External Jwt claims.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminJwtClaims {
/// Issued at time
pub iat: u64,
/// Expiration
pub exp: u64,
}

/// Validation error.
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("Admin disabled")]
AdminDisabled,
#[error("Token error,\n caused by: {}", source)]
TokenError { source: CryptoError },
#[error("Token expired")]
TokenExpired
}

impl ValidationError {
pub fn token_error(token: &str, source: CryptoError) -> Self {
sentry::configure_scope(|scope| {
scope.set_extra("Token", token.into());
});

Self::TokenError { source }
}
}

impl ResponseError for ValidationError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}

/// Admin jwt authentication validator.
pub async fn admin_jwt_auth_validator(
req: ServiceRequest,
credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
admin_jwt_auth_validator_inner(req, credentials)
.await
.map_err(|(err, req)| (err.into(), req))
}

async fn admin_jwt_auth_validator_inner(
req: ServiceRequest,
credentials: BearerAuth,
) -> Result<ServiceRequest, (ValidationError, ServiceRequest)> {
let ctx = req.app_data::<web::Data<AppContext>>().unwrap();
if ctx.config.server.admin_private_key.is_empty() {
return Err((ValidationError::AdminDisabled, req));
}

// Validate token
let pubkey =
PrivateRsaKey::new(ctx.config.server.admin_private_key.clone()).extract_public_key();
let tok = credentials.token();
let claims: AdminJwtClaims = match JwtUtils::verify_jwt(tok, pubkey.as_str()) {
Ok(claims) => claims,
Err(e) => return Err((ValidationError::token_error(tok, e), req)),
};

if now_timestamp() >= claims.exp {
return Err((ValidationError::TokenExpired, req));
}

Ok(req)
}

fn now_timestamp() -> u64 {
let start = SystemTime::now();
let duration = start.duration_since(UNIX_EPOCH).expect("time collapsed");

duration.as_secs()
}

/// Generate admin token.
pub fn generate_admin_token(config: &Config) -> Result<String, CryptoError> {
let now_ts = now_timestamp();
let claims = AdminJwtClaims {
// Issued at time
iat: now_ts,
// Expiration in 24h
exp: now_ts + (60 * 60 * 24),
};

JwtUtils::create_jwt(&config.server.admin_private_key, &claims)
}
1 change: 1 addition & 0 deletions crates/prbot-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#![warn(missing_docs)]
#![warn(clippy::all)]

pub mod admin;
pub mod constants;
mod debug;
pub mod errors;
Expand Down
11 changes: 11 additions & 0 deletions crates/prbot-server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use sentry_actix::Sentry;
use tracing::info;

use crate::{
admin::{accounts_list, external_accounts_list, pull_request_rules_create, pull_request_rules_delete, repositories_list, validator::admin_jwt_auth_validator},
debug::configure_debug_handlers,
external::{status::set_qa_status, validator::jwt_auth_validator},
ghapi::MetricsApiService,
Expand Down Expand Up @@ -111,6 +112,16 @@ pub fn build_actix_app(
.wrap(Cors::permissive())
.route("/set-qa-status", web::post().to(set_qa_status)),
)
.service(
web::scope("/admin")
.wrap(HttpAuthentication::bearer(admin_jwt_auth_validator))
.wrap(Cors::permissive())
.route("/accounts/", web::get().to(accounts_list))
.route("/repositories/", web::get().to(repositories_list))
.route("/repositories/{repository_id}/pull-request-rules/", web::post().to(pull_request_rules_create))
.route("/repositories/{repository_id}/pull-request-rules/{rule_name}/", web::delete().to(pull_request_rules_delete))
.route("/external-accounts/", web::get().to(external_accounts_list))
)
.service(
web::scope("/webhook")
.wrap(VerifySignature::new(&context.config))
Expand Down
Loading

0 comments on commit 6cf1301

Please sign in to comment.