Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(db, webserver): Implement GitHubRepositoryProvider connect flow #1749

Merged
merged 18 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
1 change: 1 addition & 0 deletions ee/tabby-db/migrations/0022_github-provider.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE github_repository_provider;
8 changes: 8 additions & 0 deletions ee/tabby-db/migrations/0022_github-provider.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE github_repository_provider(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL,
application_id TEXT NOT NULL,
secret TEXT NOT NULL,
access_token TEXT,
CONSTRAINT `idx_application_id` UNIQUE (`application_id`)
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
);
Binary file modified ee/tabby-db/schema.sqlite
Binary file not shown.
73 changes: 73 additions & 0 deletions ee/tabby-db/src/github_repository_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use anyhow::{anyhow, Result};
use sqlx::{prelude::FromRow, query, query_as};

use crate::{DbConn, SQLXResultExt};

pub struct GithubRepositoryProviderDAO {
pub display_name: String,
pub application_id: String,
pub secret: String,
}

#[derive(FromRow)]
pub struct GithubProvidedRepositoryDAO {
pub github_repository_provider_id: i64,
pub vendor_id: String,
pub name: String,
pub git_url: String,
}

impl DbConn {
pub async fn create_github_provider(
&self,
name: String,
application_id: String,
secret: String,
) -> Result<i64> {
let res = query!("INSERT INTO github_repository_provider (display_name, application_id, secret) VALUES ($1, $2, $3);",
name,
application_id,
secret
).execute(&self.pool).await.unique_error("GitHub Application ID already exists")?;
Ok(res.last_insert_rowid())
}

Check warning on line 33 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L21-L33

Added lines #L21 - L33 were not covered by tests

pub async fn get_github_provider(&self, id: i64) -> Result<GithubRepositoryProviderDAO> {
let provider = query_as!(
GithubRepositoryProviderDAO,
"SELECT display_name, application_id, secret FROM github_repository_provider WHERE id = ?;",
id
)
.fetch_one(&self.pool)
.await?;
Ok(provider)
}

Check warning on line 44 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L35-L44

Added lines #L35 - L44 were not covered by tests

pub async fn delete_github_provider(&self, id: i64) -> Result<()> {
let res = query!("DELETE FROM github_repository_provider WHERE id = ?;", id)
.execute(&self.pool)
.await?;
if res.rows_affected() != 1 {
return Err(anyhow!("No github provider details to delete"));
}
Ok(())
}

Check warning on line 54 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L46-L54

Added lines #L46 - L54 were not covered by tests

pub async fn update_github_provider_token(&self, id: i64, access_token: String) -> Result<()> {
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
let res = query!(
"UPDATE github_repository_provider SET access_token = ? WHERE id = ?",
access_token,
id
)
.execute(&self.pool)
.await?;

Check warning on line 63 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L56-L63

Added lines #L56 - L63 were not covered by tests

if res.rows_affected() != 1 {
return Err(anyhow!(
"The specified Github repository provider does not exist"
));
}

Ok(())
}

Check warning on line 72 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L65-L72

Added lines #L65 - L72 were not covered by tests
}
1 change: 1 addition & 0 deletions ee/tabby-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub use users::UserDAO;

pub mod cache;
mod email_setting;
mod github_repository_provider;
mod invitations;
mod job_runs;
mod oauth_credential;
Expand Down
7 changes: 6 additions & 1 deletion ee/tabby-webserver/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@
)
.nest(
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
"/repositories",
repositories::routes(rs.clone(), ctx.auth()),
repositories::routes(
rs.clone(),
ctx.auth(),
ctx.setting(),
ctx.github_repository_provider(),
),

Check warning on line 90 in ee/tabby-webserver/src/handler.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/handler.rs#L85-L90

Added lines #L85 - L90 were not covered by tests
)
.route(
"/avatar/:id",
Expand Down
14 changes: 7 additions & 7 deletions ee/tabby-webserver/src/oauth/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ use crate::schema::auth::{AuthenticationService, OAuthCredential, OAuthProvider}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct GithubOAuthResponse {
pub struct GithubOAuthResponse {
#[serde(default)]
access_token: String,
pub access_token: String,
#[serde(default)]
scope: String,
pub scope: String,
#[serde(default)]
token_type: String,
pub token_type: String,

#[serde(default)]
error: String,
pub error: String,
#[serde(default)]
error_description: String,
pub error_description: String,
#[serde(default)]
error_uri: String,
pub error_uri: String,
}

#[derive(Debug, Deserialize)]
Expand Down
20 changes: 18 additions & 2 deletions ee/tabby-webserver/src/repositories/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod oauth;
mod resolve;

use std::sync::Arc;
Expand All @@ -16,12 +17,26 @@
use crate::{
handler::require_login_middleware,
repositories::resolve::{RepositoryMeta, ResolveParams},
schema::auth::AuthenticationService,
schema::{
auth::AuthenticationService, github_repository_provider::GithubRepositoryProviderService,
setting::SettingService,
},
};

use self::oauth::OAuthState;

pub type ResolveState = Arc<RepositoryCache>;

pub fn routes(rs: Arc<ResolveState>, auth: Arc<dyn AuthenticationService>) -> Router {
pub fn routes(
rs: Arc<ResolveState>,
auth: Arc<dyn AuthenticationService>,
settings: Arc<dyn SettingService>,
github_repository_provider: Arc<dyn GithubRepositoryProviderService>,
) -> Router {
let oauth_state = OAuthState {
settings,
github_repository_provider,
};

Check warning on line 39 in ee/tabby-webserver/src/repositories/mod.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/mod.rs#L30-L39

Added lines #L30 - L39 were not covered by tests
Router::new()
.route("/resolve", routing::get(resolve))
.route("/resolve/", routing::get(resolve))
Expand All @@ -32,6 +47,7 @@
.route("/:name/meta/", routing::get(meta))
.route("/:name/meta/*path", routing::get(meta))
.with_state(rs.clone())
.nest("/oauth", oauth::routes(oauth_state))

Check warning on line 50 in ee/tabby-webserver/src/repositories/mod.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/mod.rs#L50

Added line #L50 was not covered by tests
.fallback(not_found)
.layer(from_fn_with_state(auth, require_login_middleware))
}
Expand Down
120 changes: 120 additions & 0 deletions ee/tabby-webserver/src/repositories/oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use anyhow::Result;
use std::sync::Arc;

use axum::{
extract::{Path, Query, State},
response::Redirect,
routing, Router,
};
use hyper::StatusCode;
use juniper::ID;
use serde::Deserialize;

use crate::{
oauth::github::GithubOAuthResponse,
schema::{
github_repository_provider::GithubRepositoryProviderService, setting::SettingService,
},
};

#[derive(Clone)]
pub struct OAuthState {
pub settings: Arc<dyn SettingService>,
pub github_repository_provider: Arc<dyn GithubRepositoryProviderService>,
}

pub fn routes(state: OAuthState) -> Router {
Router::new()
.route("/github/login/:id", routing::get(login))
.route("/github/callback", routing::get(callback))
.with_state(state)
}

Check warning on line 31 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L26-L31

Added lines #L26 - L31 were not covered by tests

fn github_redirect_url(client_id: &str, redirect_uri: &str, id: &ID) -> String {
format!("https://github.com/login/oauth/authorize?client_id={client_id}&response_type=code&scope=repo&redirect_uri={redirect_uri}/repositories/oauth/github/callback&state={id}")
}

Check warning on line 35 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L33-L35

Added lines #L33 - L35 were not covered by tests

#[derive(Deserialize)]

Check warning on line 37 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L37

Added line #L37 was not covered by tests
struct CallbackParams {
state: ID,
code: String,
}

macro_rules! log_error {
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
($val:expr) => {
$val.map_err(|e| {
tracing::error!("{e}");
StatusCode::INTERNAL_SERVER_ERROR
})
};
}

async fn exchange_access_token(
state: &OAuthState,
params: &CallbackParams,
) -> Result<GithubOAuthResponse> {
let client = reqwest::Client::new();

Check warning on line 56 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L52-L56

Added lines #L52 - L56 were not covered by tests

let client_id = state
.github_repository_provider
.get_github_repository_provider(params.state.clone())
.await?

Check warning on line 61 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L58-L61

Added lines #L58 - L61 were not covered by tests
.application_id;

let secret = state
.github_repository_provider
.read_github_repository_provider_secret(params.state.clone())
.await?;

Check warning on line 67 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L64-L67

Added lines #L64 - L67 were not covered by tests

Ok(client
.post("https://github.com/login/oauth/access_token")
.header(reqwest::header::ACCEPT, "application/json")
.form(&[
("client_id", &client_id),
("client_secret", &secret),
("code", &params.code),
])
.send()
.await?
.json()
.await?)
}

Check warning on line 81 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L69-L81

Added lines #L69 - L81 were not covered by tests

async fn callback(
State(state): State<OAuthState>,
Query(params): Query<CallbackParams>,
) -> Result<Redirect, StatusCode> {
let network_setting = log_error!(state.settings.read_network_setting().await)?;
let external_url = network_setting.external_url;

Check warning on line 88 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L83-L88

Added lines #L83 - L88 were not covered by tests

let response = log_error!(exchange_access_token(&state, &params).await)?;
dbg!(&response);
log_error!(
state
.github_repository_provider
.set_github_repository_provider_token(params.state, response.access_token)
.await
)?;

Check warning on line 97 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L90-L97

Added lines #L90 - L97 were not covered by tests

Ok(Redirect::permanent(&external_url))
}

Check warning on line 100 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L99-L100

Added lines #L99 - L100 were not covered by tests

async fn login(
State(state): State<OAuthState>,
Path(id): Path<ID>,
) -> Result<Redirect, StatusCode> {
let network_setting = log_error!(state.settings.read_network_setting().await)?;
let external_url = network_setting.external_url;
let client_id = log_error!(
state
.github_repository_provider
.get_github_repository_provider(id.clone())
.await
)?

Check warning on line 113 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L102-L113

Added lines #L102 - L113 were not covered by tests
.application_id;
Ok(Redirect::temporary(&github_redirect_url(
&client_id,
&external_url,
&id,
)))
}

Check warning on line 120 in ee/tabby-webserver/src/repositories/oauth.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/repositories/oauth.rs#L115-L120

Added lines #L115 - L120 were not covered by tests
20 changes: 20 additions & 0 deletions ee/tabby-webserver/src/schema/github_repository_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::schema::Result;
use async_trait::async_trait;
use juniper::{GraphQLObject, ID};

#[derive(GraphQLObject)]

Check warning on line 5 in ee/tabby-webserver/src/schema/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/schema/github_repository_provider.rs#L5

Added line #L5 was not covered by tests
pub struct GithubRepositoryProvider {
pub display_name: String,
pub application_id: String,
}

#[async_trait]
pub trait GithubRepositoryProviderService: Send + Sync {
async fn get_github_repository_provider(&self, id: ID) -> Result<GithubRepositoryProvider>;
async fn read_github_repository_provider_secret(&self, id: ID) -> Result<String>;
async fn set_github_repository_provider_token(
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
&self,
id: ID,
access_token: String,
) -> Result<()>;
}
7 changes: 6 additions & 1 deletion ee/tabby-webserver/src/schema/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod analytic;
pub mod auth;
pub mod email;
pub mod github_repository_provider;
pub mod job;
pub mod license;
pub mod repository;
Expand Down Expand Up @@ -29,16 +30,19 @@ use tracing::error;
use validator::{Validate, ValidationErrors};
use worker::{Worker, WorkerService};

use crate::schema::repository::FileEntrySearchResult;

use self::{
analytic::{AnalyticService, CompletionStats},
auth::{
JWTPayload, OAuthCredential, OAuthProvider, PasswordChangeInput, PasswordResetInput,
RequestInvitationInput, RequestPasswordResetEmailInput, UpdateOAuthCredentialInput,
},
email::{EmailService, EmailSetting, EmailSettingInput},
github_repository_provider::GithubRepositoryProviderService,
job::JobStats,
license::{IsLicenseValid, LicenseInfo, LicenseService, LicenseType},
repository::{FileEntrySearchResult, Repository, RepositoryService},
repository::{Repository, RepositoryService},
setting::{
NetworkSetting, NetworkSettingInput, SecuritySetting, SecuritySettingInput, SettingService,
},
Expand All @@ -55,6 +59,7 @@ pub trait ServiceLocator: Send + Sync {
fn setting(&self) -> Arc<dyn SettingService>;
fn license(&self) -> Arc<dyn LicenseService>;
fn analytic(&self) -> Arc<dyn AnalyticService>;
fn github_repository_provider(&self) -> Arc<dyn GithubRepositoryProviderService>;
}

pub struct Context {
Expand Down
Loading
Loading