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 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
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.
98 changes: 98 additions & 0 deletions ee/tabby-db/src/github_repository_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use anyhow::{anyhow, Result};
use sqlx::{prelude::FromRow, query, query_as};
use tabby_db_macros::query_paged_as;

use crate::{DbConn, SQLXResultExt};

#[derive(FromRow)]
pub struct GithubRepositoryProviderDAO {
pub id: i64,
pub display_name: String,
pub application_id: String,
pub secret: String,
pub access_token: Option<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 29 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#L17-L29

Added lines #L17 - L29 were not covered by tests

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

Check warning on line 40 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#L31-L40

Added lines #L31 - L40 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 50 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#L42-L50

Added lines #L42 - L50 were not covered by tests

pub async fn update_github_provider_access_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?;

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#L52-L63

Added lines #L52 - 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

pub async fn list_github_repository_providers(
&self,
limit: Option<usize>,
skip_id: Option<i32>,
backwards: bool,
) -> Result<Vec<GithubRepositoryProviderDAO>> {
let providers = query_paged_as!(
GithubRepositoryProviderDAO,
"github_repository_provider",
[
"id",
"display_name",
"application_id",
"secret",
"access_token"
],
limit,
skip_id,
backwards
)
.fetch_all(&self.pool)
.await?;
Ok(providers)
}

Check warning on line 97 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#L74-L97

Added lines #L74 - L97 were not covered by tests
}
2 changes: 2 additions & 0 deletions ee/tabby-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::anyhow;
use cache::Cache;
use chrono::{DateTime, NaiveDateTime, Utc};
pub use email_setting::EmailSettingDAO;
pub use github_repository_provider::GithubRepositoryProviderDAO;
pub use invitations::InvitationDAO;
pub use job_runs::JobRunDAO;
pub use oauth_credential::OAuthCredentialDAO;
Expand All @@ -17,6 +18,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
17 changes: 17 additions & 0 deletions ee/tabby-webserver/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ enum Language {
PYTHON
}

type GithubRepositoryProviderEdge {
node: GithubRepositoryProvider!
cursor: String!
}

type JobRun {
id: ID!
job: String!
Expand Down Expand Up @@ -44,6 +49,11 @@ type LicenseInfo {
expiresAt: DateTimeUtc
}

type GithubRepositoryProviderConnection {
edges: [GithubRepositoryProviderEdge!]!
pageInfo: PageInfo!
}

input SecuritySettingInput {
allowedRegisterDomainList: [String!]!
disableClientSideTelemetry: Boolean!
Expand Down Expand Up @@ -72,6 +82,12 @@ type ServerInfo {
allowSelfSignup: Boolean!
}

type GithubRepositoryProvider {
id: ID!
displayName: String!
applicationId: String!
}

input PasswordChangeInput {
oldPassword: String
newPassword1: String!
Expand Down Expand Up @@ -176,6 +192,7 @@ type Query {
me: User!
users(after: String, before: String, first: Int, last: Int): UserConnection!
invitations(after: String, before: String, first: Int, last: Int): InvitationConnection!
githubRepositoryProviders(after: String, before: String, first: Int, last: Int): GithubRepositoryProviderConnection!
jobRuns(ids: [ID!], jobs: [String!], after: String, before: String, first: Int, last: Int): JobRunConnection!
jobRunStats(jobs: [String!]): JobStats!
emailSetting: EmailSetting
Expand Down
6 changes: 5 additions & 1 deletion ee/tabby-webserver/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use tracing::{error, warn};

use crate::{
cron, hub, oauth,
cron, hub, integrations, oauth,
repositories::{self, RepositoryCache},
schema::{auth::AuthenticationService, create_schema, Schema, ServiceLocator},
service::{create_service_locator, event_logger::create_event_logger},
Expand Down Expand Up @@ -84,6 +84,10 @@
"/repositories",
repositories::routes(rs.clone(), ctx.auth()),
)
.nest(
"/integrations/github",
integrations::github::routes(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#L87-L90

Added lines #L87 - L90 were not covered by tests
.route(
"/avatar/:id",
routing::get(avatar)
Expand Down
1 change: 1 addition & 0 deletions ee/tabby-webserver/src/integrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod github;
152 changes: 152 additions & 0 deletions ee/tabby-webserver/src/integrations/github.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use std::sync::Arc;

use anyhow::Result;
use axum::{
extract::{Path, Query, State},
response::Redirect,
routing, Router,
};
use hyper::StatusCode;
use juniper::ID;
use serde::Deserialize;
use tracing::error;
use url::Url;

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

#[derive(Deserialize)]

Check warning on line 22 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L22

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

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

pub fn routes(
settings: Arc<dyn SettingService>,
github_repository_provider: Arc<dyn GithubRepositoryProviderService>,
) -> Router {
let state = IntegrationState {
settings,
github_repository_provider,
};
Router::new()
.route("/connect/:id", routing::get(connect))
.route("/callback", routing::get(callback))
.with_state(state)
}

Check warning on line 46 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L34-L46

Added lines #L34 - L46 were not covered by tests

fn get_authorize_url(client_id: &str, redirect_uri: &str, id: &ID) -> Result<Url> {
Comment on lines +47 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn get_authorize_url(client_id: &str, redirect_uri: &str, id: &ID) -> Result<Url> {
// FIXME(boxbeam): refactoring the logic using `/oauth/github.rs`
fn get_authorize_url(client_id: &str, redirect_uri: &str, id: &ID) -> Result<Url> {

Ok(Url::parse_with_params(
"https://github.com/login/oauth/authorize",
&[
("client_id", client_id),
("response_type", "code"),
("scope", "repo"),
(
"redirect_uri",
&format!("{redirect_uri}/integrations/github/callback"),
),
("state", &id.to_string()),
],
)?)
}

Check warning on line 62 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L48-L62

Added lines #L48 - L62 were not covered by tests

async fn exchange_access_token(
Comment on lines +63 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async fn exchange_access_token(
// FIXME(boxbeam): refactoring the logic using `/oauth/github.rs`
async fn exchange_access_token(

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

Check warning on line 68 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L64-L68

Added lines #L64 - L68 were not covered by tests

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

Check warning on line 73 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L70-L73

Added lines #L70 - L73 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 79 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L76-L79

Added lines #L76 - L79 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 93 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L81-L93

Added lines #L81 - L93 were not covered by tests

async fn callback(
State(state): State<IntegrationState>,
Query(params): Query<CallbackParams>,
) -> Result<Redirect, StatusCode> {
let response = match exchange_access_token(&state, &params).await {
Ok(response) => response,
Err(e) => {
error!("Failed to exchange access token: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);

Check warning on line 103 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L95-L103

Added lines #L95 - L103 were not covered by tests
}
};

if let Err(e) = state
.github_repository_provider
.update_github_repository_provider_access_token(params.state, response.access_token)
.await

Check warning on line 110 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L107-L110

Added lines #L107 - L110 were not covered by tests
{
error!("Failed to update github access token: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}

Ok(Redirect::temporary("/"))
}

Check warning on line 117 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L112-L117

Added lines #L112 - L117 were not covered by tests

async fn connect(
State(state): State<IntegrationState>,
Path(id): Path<ID>,
) -> Result<Redirect, StatusCode> {
let network_setting = match state.settings.read_network_setting().await {
Ok(setting) => setting,
Err(e) => {
error!("Failed to read network setting: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);

Check warning on line 127 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L119-L127

Added lines #L119 - L127 were not covered by tests
}
};
let external_url = network_setting.external_url;
let provider = match state
.github_repository_provider
.get_github_repository_provider(id.clone())
.await

Check warning on line 134 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L130-L134

Added lines #L130 - L134 were not covered by tests
{
Ok(provider) => provider,
Err(e) => {
error!("Github repository provider not found: {e}");
return Err(StatusCode::NOT_FOUND);

Check warning on line 139 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L136-L139

Added lines #L136 - L139 were not covered by tests
}
};

let redirect = match get_authorize_url(&provider.application_id, &external_url, &id) {
Ok(redirect) => redirect,
Err(e) => {
error!("Failed to generate callback URL: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);

Check warning on line 147 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L143-L147

Added lines #L143 - L147 were not covered by tests
}
};

Ok(Redirect::temporary(redirect.as_str()))
}

Check warning on line 152 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L151-L152

Added lines #L151 - L152 were not covered by tests
1 change: 1 addition & 0 deletions ee/tabby-webserver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod cron;
mod handler;
mod hub;
mod integrations;
mod oauth;
mod repositories;
mod schema;
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
Loading
Loading