Skip to content

Commit

Permalink
use vc token if logging into Vercel
Browse files Browse the repository at this point in the history
  • Loading branch information
Zertsov committed Feb 7, 2024
1 parent 83a22a5 commit f1ea261
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 47 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ jobs:
needs: [determine_jobs, build_turborepo]
if: needs.determine_jobs.outputs.turborepo_integration == 'true'
runs-on: ${{ matrix.os.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -284,7 +285,7 @@ jobs:
key: prysk-venv-${{ matrix.os.runner }}

- name: Integration Tests
run: turbo run test --filter=turborepo-tests-integration --color --env-mode=strict --token=${{ secrets.TURBO_TOKEN }} --team=${{ vars.TURBO_TEAM }}
run: turbo run test --filter=turborepo-tests-integration --color --env-mode=strict --token=${{ secrets.TURBO_TOKEN }} --team=${{ vars.TURBO_TEAM }} -v --log-order=stream
env:
EXPERIMENTAL_RUST_CODEPATH: true

Expand Down
3 changes: 3 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/turborepo-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ chrono.workspace = true
hostname = "0.3.1"
lazy_static.workspace = true
reqwest.workspace = true
serde.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tempfile.workspace = true
thiserror = "1.0.38"
tokio.workspace = true
tracing.workspace = true
turborepo-api-client = { workspace = true }
turborepo-dirs = { version = "0.1.0", path = "../turborepo-dirs" }
turborepo-ui.workspace = true
turborepo-vercel-api = { workspace = true }
turborepo-vercel-api-mock = { workspace = true }
Expand Down
102 changes: 66 additions & 36 deletions crates/turborepo-auth/src/auth/login.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
use std::{borrow::Cow, sync::Arc};
use std::sync::Arc;

pub use error::Error;
use reqwest::Url;
use tokio::sync::OnceCell;
use tracing::warn;
use turborepo_api_client::Client;
use turborepo_ui::{start_spinner, BOLD, UI};
use turborepo_ui::{start_spinner, BOLD};

use crate::{error, server::LoginServer, ui};
use crate::{
auth::{check_token, extract_vercel_token},
error, ui, LoginOptions,
};

const DEFAULT_HOST_NAME: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 9789;

/// Login writes a token to disk at token_path. If a token is already present,
/// we do not overwrite it and instead log that we found an existing token.
pub async fn login<'a>(
api_client: &impl Client,
ui: &UI,
existing_token: Option<&'a str>,
login_url_configuration: &str,
login_server: &impl LoginServer,
) -> Result<Cow<'a, str>, Error> {
// Check if token exists first.
if let Some(token) = existing_token {
if let Ok(response) = api_client.get_user(token).await {
println!("{}", ui.apply(BOLD.apply_to("Existing token found!")));
ui::print_cli_authorized(&response.user.email, ui);
return Ok(token.into());
/// Token is the result of a successful login. It contains the token string and
/// a boolean indicating whether the token already existed on the filesystem.
#[derive(Debug)]
pub struct Token {
/// The actual token string.
pub token: String,
/// If this is `true`, it means this token already exists on the filesystem.
/// If `false`, this is a new token.
pub exists: bool,
}

/// Login returns a `Token` struct. If a token is already present,
/// we do not overwrite it and instead log that we found an existing token,
/// setting the `exists` field to `true`.
///
/// First checks if an existing option has been passed in, then if the login is
/// to Vercel, checks if the user has a Vercel CLI token on disk.
pub async fn login<T: Client>(options: &LoginOptions<'_, T>) -> Result<Token, Error> {
let (api_client, ui, login_url_configuration, login_server) = (
options.api_client,
options.ui,
options.login_url,
options.login_server,
);
// Check if passed in token exists first.
if let Some(token) = options.existing_token {
return check_token(token, ui, api_client, "Existing token found!").await;
}

// If the user is logging into Vercel, check for an existing `vc` token.
if login_url_configuration.contains("vercel.com") {
match extract_vercel_token() {
Ok(token) => {
println!(
"{}",
ui.apply(BOLD.apply_to("Existing Vercel token found!"))
);
return Ok(Token {
token,
exists: true,
});
}
Err(error) => {
// Only send the warning if we're debugging.
dbg!("Failed to extract Vercel token: ", error);
}
}
}

Expand Down Expand Up @@ -74,7 +108,10 @@ pub async fn login<'a>(

ui::print_cli_authorized(&user_response.user.email, ui);

Ok(token.to_string().into())
Ok(Token {
token: token.into(),
exists: false,
})
}

#[cfg(test)]
Expand All @@ -84,13 +121,15 @@ mod tests {
use async_trait::async_trait;
use reqwest::{Method, RequestBuilder, Response};
use turborepo_api_client::Client;
use turborepo_ui::UI;
use turborepo_vercel_api::{
CachingStatusResponse, Membership, Role, SpacesResponse, Team, TeamsResponse, User,
UserResponse, VerifiedSsoUser,
};
use turborepo_vercel_api_mock::start_test_server;

use super::*;
use crate::LoginServer;

struct MockLoginServer {
hits: Arc<AtomicUsize>,
Expand Down Expand Up @@ -271,28 +310,19 @@ mod tests {
let login_server = MockLoginServer {
hits: Arc::new(0.into()),
};
let mut options = LoginOptions::new(&ui, &url, &api_client, &login_server);

let token = login(&api_client, &ui, None, &url, &login_server)
.await
.unwrap();
let token = login(&options).await.unwrap();
assert!(!token.exists);

let got_token = Some(token.to_string());

// Token should be set now
assert_eq!(
got_token.as_deref(),
Some(turborepo_vercel_api_mock::EXPECTED_TOKEN)
);
let got_token = token.token.to_string();
assert_eq!(&got_token, turborepo_vercel_api_mock::EXPECTED_TOKEN);

// Call the login function a second time to test that we check for existing
// tokens. Total server hits should be 1.
let second_token = login(&api_client, &ui, got_token.as_deref(), &url, &login_server)
.await
.unwrap();

// We can confirm that we didn't fetch a new token because we're borrowing the
// existing token and not getting a new allocation.
assert!(second_token.is_borrowed());
options.existing_token = Some(&got_token);
let second_token = login(&options).await.unwrap();
assert!(second_token.exists);

api_server.abort();
assert_eq!(
Expand Down
74 changes: 74 additions & 0 deletions crates/turborepo-auth/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,77 @@ mod sso;
pub use login::*;
pub use logout::*;
pub use sso::*;
use turborepo_api_client::Client;
use turborepo_ui::{BOLD, UI};

use crate::{ui, LoginServer};

const VERCEL_TOKEN_DIR: &str = "com.vercel.cli";
const VERCEL_TOKEN_FILE: &str = "auth.json";

pub struct LoginOptions<'a, T>
where
T: Client,
{
pub ui: &'a UI,
pub login_url: &'a str,
pub api_client: &'a T,
pub login_server: &'a dyn LoginServer,

pub sso_team: Option<&'a str>,
pub existing_token: Option<&'a str>,
}
impl<'a, T> LoginOptions<'a, T>
where
T: Client,
{
pub fn new(
ui: &'a UI,
login_url: &'a str,
api_client: &'a T,
login_server: &'a dyn LoginServer,
) -> Self {
Self {
ui,
login_url,
api_client,
login_server,
sso_team: None,
existing_token: None,
}
}
}

async fn check_token(
token: &str,
ui: &UI,
api_client: &impl Client,
message: &str,
) -> Result<Token, Error> {
let response = api_client.get_user(token).await?;
println!("{}", ui.apply(BOLD.apply_to(message)));
ui::print_cli_authorized(&response.user.email, ui);
Ok(Token {
token: token.to_string(),
exists: true,
})
}

fn extract_vercel_token() -> Result<String, Error> {
let vercel_config_dir =
turborepo_dirs::vercel_config_dir().ok_or_else(|| Error::ConfigDirNotFound)?;

let vercel_token_path = vercel_config_dir
.join(VERCEL_TOKEN_DIR)
.join(VERCEL_TOKEN_FILE);
let contents = std::fs::read_to_string(vercel_token_path)?;

#[derive(serde::Deserialize)]
struct VercelToken {
// This isn't actually dead code, it's used by serde to deserialize the JSON.
#[allow(dead_code)]
token: String,
}

Ok(serde_json::from_str::<VercelToken>(&contents)?.token)
}
9 changes: 9 additions & 0 deletions crates/turborepo-auth/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
SerdeError(#[from] serde_json::Error),
#[error(transparent)]
APIError(#[from] turborepo_api_client::Error),

#[error(
"loginUrl is configured to \"{value}\", but cannot be a base URL. This happens in \
situations like using a `data:` URL."
Expand All @@ -19,4 +26,6 @@ pub enum Error {
FailedToValidateSSOToken(#[source] turborepo_api_client::Error),
#[error("failed to make sso token name")]
FailedToMakeSSOTokenName(#[source] io::Error),
#[error("config directory not found")]
ConfigDirNotFound,
}
21 changes: 12 additions & 9 deletions crates/turborepo-lib/src/commands/login.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use turborepo_api_client::APIClient;
use turborepo_auth::{
login as auth_login, sso_login as auth_sso_login, DefaultLoginServer, DefaultSSOLoginServer,
LoginOptions,
};
use turborepo_telemetry::events::command::{CommandEventBuilder, LoginMethod};

Expand Down Expand Up @@ -58,15 +59,17 @@ pub async fn login(base: &mut CommandBase, telemetry: CommandEventBuilder) -> Re
let api_client: APIClient = base.api_client()?;
let ui = base.ui;
let login_url_config = base.config()?.login_url().to_string();
let options = LoginOptions {
existing_token: base.config()?.token(),
..LoginOptions::new(&ui, &login_url_config, &api_client, &DefaultLoginServer)
};

let token = auth_login(
&api_client,
&ui,
base.config()?.token(),
&login_url_config,
&DefaultLoginServer,
)
.await?;
let token = auth_login(&options).await?;

// Don't write to disk if the token is already there
if token.exists {
return Ok(());
}

let global_config_path = base.global_config_path()?;
let before = global_config_path
Expand All @@ -75,7 +78,7 @@ pub async fn login(base: &mut CommandBase, telemetry: CommandEventBuilder) -> Re
config_path: global_config_path.clone(),
error: e,
})?;
let after = set_path(&before, &["token"], &format!("\"{}\"", token))?;
let after = set_path(&before, &["token"], &format!("\"{}\"", token.token))?;

global_config_path
.ensure_dir()
Expand Down
2 changes: 2 additions & 0 deletions packages/turbo-exe-stub/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ cp turbo.exe "${FIND_TURBO_FIXTURES_DIR}/unplugged_env_moved/.moved/unplugged/tu

cp turbo.exe "${FIND_TURBO_FIXTURES_DIR}/unplugged_moved/.moved/unplugged/turbo-windows-64-npm-1.0.0-520925a700/node_modules/turbo-windows-64/bin/"
cp turbo.exe "${FIND_TURBO_FIXTURES_DIR}/unplugged_moved/.moved/unplugged/turbo-windows-arm64-npm-1.0.0-520925a700/node_modules/turbo-windows-arm64/bin/"

echo "Done copying files"
35 changes: 35 additions & 0 deletions turborepo-tests/helpers/mock_existing_login.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash

# Create a mocked Vercel auth file
read -r -d '' AUTH <<- EOF
{
"// Note": "This is your Vercel credentials file. DO NOT SHARE!",
"// Docs": "https://vercel.com/docs/project-configuration#global-configuration/auth-json",
"token": "mock-token"
}
EOF

TMP_DIR=$(mktemp -d -t turbo-XXXXXXXXXX)

# duplicate over to XDG var so that turbo picks it up
export XDG_CONFIG_HOME=$TMP_DIR
export HOME=$TMP_DIR
export TURBO_CONFIG_DIR_PATH=$TMP_DIR
export TURBO_TELEMETRY_MESSAGE_DISABLED=1

# For Linux
mkdir -p "$TMP_DIR/com.vercel.cli"
export VERCEL_CONFIG_DIR_PATH="$TMP_DIR"
echo $AUTH > "$TMP_DIR/com.vercel.cli/auth.json"

# For macOS
MACOS_DIR="$TMP_DIR/Library/Application Support"
mkdir -p "$MACOS_DIR/com.vercel.cli"
export VERCEL_CONFIG_DIR_PATH="$MACOS_DIR"
echo "$AUTH" > "$MACOS_DIR/com.vercel.cli/auth.json"

# For Windows
WINDOWS_DIR="$TMP_DIR\\AppData\\Roaming"
mkdir -p "$WINDOWS_DIR\\com.vercel.cli"
export VERCEL_CONFIG_DIR_PATH="$WINDOWS_DIR"
echo "$AUTH" > "$WINDOWS_DIR\\com.vercel.cli\\auth.json"
5 changes: 5 additions & 0 deletions turborepo-tests/integration/tests/command-login.t
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ Setup
Login Test Run
$ ${TURBO} login --__test-run
Login test run successful

Login reuses Vercel CLI token
$ . ${TESTDIR}/../../helpers/mock_existing_login.sh
$ ${TURBO} login
Existing Vercel token found!

0 comments on commit f1ea261

Please sign in to comment.