Skip to content

Commit

Permalink
associate a user with multiple git providers
Browse files Browse the repository at this point in the history
  • Loading branch information
lyang2821 committed Aug 24, 2024
1 parent 4f82de5 commit 6336ed5
Show file tree
Hide file tree
Showing 27 changed files with 1,073 additions and 133 deletions.
138 changes: 132 additions & 6 deletions lapdev-api/src/account.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::str::FromStr;
use std::{collections::HashMap, str::FromStr};

use axum::{
extract::{Path, State},
extract::{Host, Path, Query, State},
response::{IntoResponse, Response},
Json,
};
Expand All @@ -12,15 +12,15 @@ use axum_extra::{
use chrono::Utc;
use hyper::StatusCode;
use lapdev_common::{
console::{MeUser, Organization},
NewSshKey, SshKey, UserRole,
console::{MeUser, NewSessionResponse, Organization},
GitProvider, NewSshKey, SshKey, UserRole,
};
use lapdev_db::{api::DbApi, entities};
use lapdev_rpc::error::ApiError;
use russh::keys::PublicKeyBase64;
use sea_orm::{prelude::Uuid, ActiveModelTrait, ActiveValue};

use crate::state::CoreState;
use crate::{session::create_oauth_connection, state::CoreState};

pub async fn me(
State(state): State<CoreState>,
Expand All @@ -32,7 +32,6 @@ pub async fn me(
.await
.unwrap_or_default();
Ok(Json(MeUser {
login: user.provider_login,
avatar_url: user.avatar_url,
email: user.email,
name: user.name,
Expand Down Expand Up @@ -242,3 +241,130 @@ pub async fn delete_ssh_key(

Ok(StatusCode::NO_CONTENT.into_response())
}

pub async fn get_git_providers(
TypedHeader(cookie): TypedHeader<Cookie>,
State(state): State<CoreState>,
) -> Result<impl IntoResponse, ApiError> {
let user = state.authenticate(&cookie).await?;
let all_oauths = state.db.get_user_all_oauth(user.id).await?;

let mut git_providers = Vec::new();
for (auth_provider, (_, config)) in state.auth.clients.read().await.iter() {
let oauth = all_oauths
.iter()
.find(|o| o.provider == auth_provider.to_string());

let git_provider = GitProvider {
auth_provider: *auth_provider,
connected: oauth.is_some(),
avatar_url: oauth.as_ref().and_then(|o| o.avatar_url.clone()),
email: oauth.as_ref().and_then(|o| o.email.clone()),
name: oauth.as_ref().and_then(|o| o.name.clone()),
read_repo: oauth.as_ref().map(|o| o.read_repo),
scopes: config.scopes.iter().map(|s| s.to_string()).collect(),
all_scopes: config
.read_repo_scopes
.iter()
.map(|s| s.to_string())
.collect(),
};
git_providers.push(git_provider);
}

Ok(Json(git_providers))
}

pub async fn connect_git_provider(
TypedHeader(cookie): TypedHeader<Cookie>,
Host(hostname): Host,
Query(query): Query<HashMap<String, String>>,
State(state): State<CoreState>,
) -> Result<impl IntoResponse, ApiError> {
let user = state.authenticate(&cookie).await?;
let provider_name = query
.get("provider")
.ok_or_else(|| ApiError::InvalidRequest("no provider in query string".to_string()))?;

let oauth = state.db.get_user_oauth(user.id, provider_name).await?;
if oauth.is_some() {
return Err(ApiError::InvalidRequest(
"provider already connected".to_string(),
))?;
}

let (headers, url) =
create_oauth_connection(&state, Some(user.id), false, &hostname, &query).await?;

Ok((headers, Json(NewSessionResponse { url })).into_response())
}

pub async fn update_scope(
TypedHeader(cookie): TypedHeader<Cookie>,
Host(hostname): Host,
Query(query): Query<HashMap<String, String>>,
State(state): State<CoreState>,
) -> Result<impl IntoResponse, ApiError> {
let user = state.authenticate(&cookie).await?;
let all_oauths = state.db.get_user_all_oauth(user.id).await?;
let provider_name = query
.get("provider")
.ok_or_else(|| ApiError::InvalidRequest("no provider in query string".to_string()))?;
if !all_oauths.iter().any(|o| &o.provider == provider_name) {
return Err(ApiError::InvalidRequest(
"provider isn't connected".to_string(),
))?;
}

let read_repo = query
.get("read_repo")
.ok_or_else(|| ApiError::InvalidRequest("no read_repo in query string".to_string()))?;

let read_repo = match read_repo.as_str() {
"yes" => true,
"no" => false,
_ => {
return Err(ApiError::InvalidRequest(
"read_repo should be either yes or no".to_string(),
))
}
};

let (headers, url) =
create_oauth_connection(&state, Some(user.id), read_repo, &hostname, &query).await?;

Ok((headers, Json(NewSessionResponse { url })).into_response())
}

pub async fn disconnect_git_provider(
TypedHeader(cookie): TypedHeader<Cookie>,
Query(query): Query<HashMap<String, String>>,
State(state): State<CoreState>,
) -> Result<impl IntoResponse, ApiError> {
let user = state.authenticate(&cookie).await?;
let all_oauths = state.db.get_user_all_oauth(user.id).await?;
if all_oauths.len() < 2 {
return Err(ApiError::InvalidRequest(
"You can't disconnect all git providers".to_string(),
))?;
}

let provider_name = query
.get("provider")
.ok_or_else(|| ApiError::InvalidRequest("no provider in query string".to_string()))?;
let oauth = state
.db
.get_user_oauth(user.id, provider_name)
.await?
.ok_or_else(|| ApiError::InvalidRequest("provider isn't connected".to_string()))?;

entities::oauth_connection::ActiveModel {
id: ActiveValue::Set(oauth.id),
deleted_at: ActiveValue::Set(Some(Utc::now().into())),
..Default::default()
}
.update(&state.db.conn)
.await?;

Ok(())
}
11 changes: 7 additions & 4 deletions lapdev-api/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct AuthConfig {
pub auth_url: &'static str,
pub token_url: &'static str,
pub scopes: &'static [&'static str],
pub read_repo_scope: &'static str,
pub read_repo_scopes: &'static [&'static str],
}

Expand All @@ -27,6 +28,7 @@ impl AuthConfig {
auth_url: "https://github.com/login/oauth/authorize",
token_url: "https://github.com/login/oauth/access_token",
scopes: &["read:user", "user:email"],
read_repo_scope: "repo",
read_repo_scopes: &["read:user", "user:email", "repo"],
};
pub const GITLAB: Self = AuthConfig {
Expand All @@ -35,6 +37,7 @@ impl AuthConfig {
auth_url: "https://gitlab.com/oauth/authorize",
token_url: "https://gitlab.com/oauth/token",
scopes: &["read_user"],
read_repo_scope: "read_repository",
read_repo_scopes: &["read_user", "read_repository"],
};
}
Expand Down Expand Up @@ -88,17 +91,17 @@ impl Auth {
&self,
provider: AuthProvider,
redirect_url: &str,
no_read_repo: bool,
read_repo: bool,
) -> Result<(String, String)> {
let clients = self.clients.read().await;
let (client, config) = clients
.get(&provider)
.ok_or_else(|| anyhow::anyhow!("can't find provider"))?;
let mut client = client.authorize_url(oauth2::CsrfToken::new_random);
for scope in if no_read_repo {
config.scopes
} else {
for scope in if read_repo {
config.read_repo_scopes
} else {
config.scopes
} {
client = client.add_scope(oauth2::Scope::new(scope.to_string()));
}
Expand Down
17 changes: 4 additions & 13 deletions lapdev-api/src/organization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,10 @@ pub async fn create_organization(
let now = Utc::now();
let txn = state.db.conn.begin().await?;

let org = entities::organization::ActiveModel {
id: ActiveValue::Set(Uuid::new_v4()),
deleted_at: ActiveValue::Set(None),
name: ActiveValue::Set(name.to_string()),
auto_start: ActiveValue::Set(true),
allow_workspace_change_auto_start: ActiveValue::Set(true),
auto_stop: ActiveValue::Set(Some(3600)),
allow_workspace_change_auto_stop: ActiveValue::Set(true),
last_auto_stop_check: ActiveValue::Set(None),
usage_limit: ActiveValue::Set(30 * 60 * 60),
}
.insert(&txn)
.await?;
let org = state
.db
.create_new_organization(&txn, name.to_string())
.await?;

entities::organization_member::ActiveModel {
created_at: ActiveValue::Set(Utc::now().into()),
Expand Down
10 changes: 7 additions & 3 deletions lapdev-api/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,14 @@ pub async fn get_project_branches(
info: RequestInfo,
) -> Result<Response, ApiError> {
let (user, project) = state.get_project(&cookie, org_id, project_id).await?;
let auth = if let Ok(Some(user)) = state.db.get_user(project.created_by).await {
(user.provider_login, user.access_token)
let auth = if let Ok(Some(oauth)) = state.db.get_oauth(project.oauth_id).await {
(oauth.provider_login, oauth.access_token)
} else {
(user.provider_login.clone(), user.access_token.clone())
let oauth = state
.conductor
.find_match_oauth_for_repo(&user, &project.repo_url)
.await?;
(oauth.provider_login, oauth.access_token)
};
let branches = state
.conductor
Expand Down
13 changes: 13 additions & 0 deletions lapdev-api/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ fn v1_api_routes(additional_router: Option<Router<CoreState>>) -> Router<CoreSta
.route("/account/ssh_keys", post(account::create_ssh_key))
.route("/account/ssh_keys", get(account::all_ssh_keys))
.route("/account/ssh_keys/:key_id", delete(account::delete_ssh_key))
.route("/account/git_providers", get(account::get_git_providers))
.route(
"/account/git_providers/connect",
put(account::connect_git_provider),
)
.route(
"/account/git_providers/disconnect",
put(account::disconnect_git_provider),
)
.route(
"/account/git_providers/update_scope",
put(account::update_scope),
)
.route("/admin/workspace_hosts", get(admin::get_workspace_hosts))
.route("/admin/workspace_hosts", post(admin::create_workspace_host))
.route(
Expand Down
Loading

0 comments on commit 6336ed5

Please sign in to comment.