Skip to content

Commit

Permalink
WIP github provider OAuth flow
Browse files Browse the repository at this point in the history
  • Loading branch information
boxbeam committed Apr 9, 2024
1 parent 468db09 commit 9dbe8a3
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 12 deletions.
2 changes: 2 additions & 0 deletions ee/tabby-db/migrations/0022_github-provider.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CREATE TABLE github_repository_provider(
display_name TEXT NOT NULL,
application_id TEXT NOT NULL,
secret TEXT NOT NULL,
access_token TEXT,
CONSTRAINT `idx_application_id` UNIQUE (`application_id`)
);

Expand All @@ -13,6 +14,7 @@ CREATE TABLE github_provided_repositories(
vendor_id TEXT NOT NULL,
name TEXT NOT NULL,
git_url TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT `idx_vendor_id` UNIQUE (`vendor_id`),
FOREIGN KEY (github_repository_provider_id) REFERENCES github_repository_provider(id) ON DELETE CASCADE
);
Binary file modified ee/tabby-db/schema.sqlite
Binary file not shown.
18 changes: 18 additions & 0 deletions ee/tabby-db/src/github_repository_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ impl DbConn {
Ok(())
}

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

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

Ok(())
}

pub async fn create_github_provided_repository(
&self,
provider_id: i64,
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 @@ -77,7 +77,12 @@ impl WebserverHandle {
)
.nest(
"/repositories",
repositories::routes(rs.clone(), ctx.auth()),
repositories::routes(
rs.clone(),
ctx.auth(),
ctx.setting(),
ctx.github_repository_provider(),
),
)
.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
21 changes: 19 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 @@ -11,17 +12,32 @@ use axum::{
routing, Json, Router,
};
pub use resolve::RepositoryCache;
use tower_http::trace::TraceLayer;
use tracing::{instrument, warn};

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,
};
Router::new()
.route("/resolve", routing::get(resolve))
.route("/resolve/", routing::get(resolve))
Expand All @@ -32,6 +48,7 @@ pub fn routes(rs: Arc<ResolveState>, auth: Arc<dyn AuthenticationService>) -> Ro
.route("/:name/meta/", routing::get(meta))
.route("/:name/meta/*path", routing::get(meta))
.with_state(rs.clone())
.nest("/oauth", oauth::routes(oauth_state))
.fallback(not_found)
.layer(from_fn_with_state(auth, require_login_middleware))
}
Expand Down
121 changes: 121 additions & 0 deletions ee/tabby-webserver/src/repositories/oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use anyhow::Result;
use std::sync::Arc;
use tower_http::trace::TraceLayer;

use axum::{
extract::{Path, Query, State},
response::{Redirect, Response},
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)
}

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/callback&state={id}")
}

#[derive(Deserialize)]
struct CallbackParams {
state: ID,
code: String,
}

macro_rules! log_error {
($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();

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

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

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?)
}

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;

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
)?;

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

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
)?
.application_id;
Ok(Redirect::temporary(&github_redirect_url(
&client_id,
&external_url,
&id,
)))
}
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 anyhow::Result;

Check failure on line 1 in ee/tabby-webserver/src/schema/github_repository_provider.rs

View workflow job for this annotation

GitHub Actions / ast-grep-lint

use-schema-result

Use schema::Result as API interface
use async_trait::async_trait;
use juniper::{GraphQLObject, ID};

#[derive(GraphQLObject)]
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(
&self,
id: ID,
access_token: String,
) -> Result<()>;
}
3 changes: 3 additions & 0 deletions ee/tabby-webserver/src/schema/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod auth;
pub mod email;
pub mod github_repository_provider;
pub mod job;
pub mod license;
pub mod repository;
Expand Down Expand Up @@ -33,6 +34,7 @@ use self::{
RequestPasswordResetEmailInput, UpdateOAuthCredentialInput,
},
email::{EmailService, EmailSetting, EmailSettingInput},
github_repository_provider::GithubRepositoryProviderService,
license::{IsLicenseValid, LicenseInfo, LicenseService, LicenseType},
repository::{Repository, RepositoryService},
setting::{
Expand All @@ -54,6 +56,7 @@ pub trait ServiceLocator: Send + Sync {
fn email(&self) -> Arc<dyn EmailService>;
fn setting(&self) -> Arc<dyn SettingService>;
fn license(&self) -> Arc<dyn LicenseService>;
fn github_repository_provider(&self) -> Arc<dyn GithubRepositoryProviderService>;
}

pub struct Context {
Expand Down
45 changes: 45 additions & 0 deletions ee/tabby-webserver/src/service/github_repository_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use anyhow::Result;
use async_trait::async_trait;
use juniper::ID;
use tabby_db::DbConn;

use crate::schema::github_repository_provider::{
GithubRepositoryProvider, GithubRepositoryProviderService,
};

use super::AsRowid;

struct GithubRepositoryProviderServiceImpl {
db: DbConn,
}

pub fn new_github_repository_provider_service(db: DbConn) -> impl GithubRepositoryProviderService {
GithubRepositoryProviderServiceImpl { db }
}

#[async_trait]
impl GithubRepositoryProviderService for GithubRepositoryProviderServiceImpl {
async fn get_github_repository_provider(&self, id: ID) -> Result<GithubRepositoryProvider> {
let provider = self.db.get_github_provider(id.as_rowid()? as i64).await?;
Ok(GithubRepositoryProvider {
display_name: provider.display_name,
application_id: provider.application_id,
})
}

async fn read_github_repository_provider_secret(&self, id: ID) -> Result<String> {
let provider = self.db.get_github_provider(id.as_rowid()? as i64).await?;
Ok(provider.secret)
}

async fn set_github_repository_provider_token(
&self,
id: ID,
access_token: String,
) -> Result<()> {
self.db
.update_github_provider_token(id.as_rowid()? as i64, access_token)
.await?;
Ok(())
}
}
Loading

0 comments on commit 9dbe8a3

Please sign in to comment.