1use crate::cloud::{
16 AccountId, AuthSecret, CloudAuthenticationConfig, CloudAuthenticationConfigData,
17};
18use crate::config::{Config, Profile, ProfileName};
19use crate::error::service::AnyhowMapServiceError;
20use crate::log::LogColorize;
21use anyhow::{anyhow, bail, Context};
22use colored::Colorize;
23use golem_cloud_client::api::{LoginClient, LoginClientLive, LoginOauth2WebFlowPollError};
24use golem_cloud_client::model::{Token, TokenSecret, UnsafeToken, WebFlowAuthorizeUrlResponse};
25use golem_cloud_client::Security;
26use indoc::printdoc;
27use std::path::Path;
28use tracing::info;
29use uuid::Uuid;
30
31impl From<&CloudAuthenticationConfig> for CloudAuthentication {
32 fn from(val: &CloudAuthenticationConfig) -> Self {
33 CloudAuthentication(UnsafeToken {
34 data: Token {
35 id: val.data.id,
36 account_id: val.data.account_id.to_string(),
37 created_at: val.data.created_at,
38 expires_at: val.data.expires_at,
39 },
40 secret: TokenSecret {
41 value: val.secret.0,
42 },
43 })
44 }
45}
46
47pub fn unsafe_token_to_auth_config(value: &UnsafeToken) -> CloudAuthenticationConfig {
48 CloudAuthenticationConfig {
49 data: CloudAuthenticationConfigData {
50 id: value.data.id,
51 account_id: value.data.account_id.to_string(),
52 created_at: value.data.created_at,
53 expires_at: value.data.expires_at,
54 },
55 secret: AuthSecret(value.secret.value),
56 }
57}
58
59pub struct Auth {
60 login_client: LoginClientLive,
61}
62
63impl Auth {
64 pub fn new(login_client: LoginClientLive) -> Self {
65 Self { login_client }
66 }
67
68 pub async fn authenticate(
69 &self,
70 token_override: Option<Uuid>,
71 auth_config: Option<&CloudAuthenticationConfig>,
72 config_dir: &Path,
73 profile_name: &ProfileName,
74 ) -> anyhow::Result<CloudAuthentication> {
75 if let Some(token_override) = token_override {
76 let secret = TokenSecret {
77 value: token_override,
78 };
79 let data = self.token_details(secret.clone()).await?;
80
81 Ok(CloudAuthentication(UnsafeToken { data, secret }))
82 } else {
83 self.profile_authentication(auth_config, config_dir, profile_name)
84 .await
85 }
86 }
87
88 fn save_auth(
89 &self,
90 token: &UnsafeToken,
91 profile_name: &ProfileName,
92 config_dir: &Path,
93 ) -> anyhow::Result<()> {
94 let profile = Config::get_profile(config_dir, profile_name)?.ok_or(anyhow!(
95 "Can't find profile {} in config",
96 profile_name.0.log_color_highlight()
97 ))?;
98
99 match profile.profile {
100 Profile::Golem(_) => Err(anyhow!(
101 "Profile {} is an OSS profile. Cloud profile expected.",
102 profile_name.0.log_color_highlight()
103 )),
104 Profile::GolemCloud(mut profile) => {
105 profile.auth = Some(unsafe_token_to_auth_config(token));
106 Config::set_profile(
107 profile_name.clone(),
108 Profile::GolemCloud(profile),
109 config_dir,
110 )
111 .with_context(|| "Failed to save auth token")?;
112
113 Ok(())
114 }
115 }
116 }
117
118 async fn oauth2(
119 &self,
120 profile_name: &ProfileName,
121 config_dir: &Path,
122 ) -> anyhow::Result<CloudAuthentication> {
123 let data = self.start_oauth2().await?;
124 inform_user(&data);
125 let token = self.complete_oauth2(data.state).await?;
126 self.save_auth(&token, profile_name, config_dir)?;
127 Ok(CloudAuthentication(token))
128 }
129
130 async fn profile_authentication(
131 &self,
132 auth_config: Option<&CloudAuthenticationConfig>,
133 config_dir: &Path,
134 profile_name: &ProfileName,
135 ) -> anyhow::Result<CloudAuthentication> {
136 if let Some(data) = auth_config {
137 Ok(data.into())
138 } else {
139 self.oauth2(profile_name, config_dir).await
140 }
141 }
142
143 async fn token_details(&self, token_secret: TokenSecret) -> anyhow::Result<Token> {
144 info!("Getting token info");
145 let mut context = self.login_client.context.clone();
146 context.security_token = Security::Bearer(token_secret.value.to_string());
147
148 let client = LoginClientLive { context };
149
150 client.current_login_token().await.map_service_error()
151 }
152
153 async fn start_oauth2(&self) -> anyhow::Result<WebFlowAuthorizeUrlResponse> {
154 info!("Start OAuth2 workflow");
155 self.login_client
156 .oauth_2_web_flow_start("github", Some("https://golem.cloud"))
157 .await
158 .map_service_error()
159 }
160
161 async fn complete_oauth2(&self, state: String) -> anyhow::Result<UnsafeToken> {
162 use tokio::time::{sleep, Duration};
163
164 info!("Complete OAuth2 workflow");
165 let mut attempts = 0;
166 let max_attempts = 60;
167 let delay = Duration::from_secs(1);
168
169 loop {
170 let status = self.login_client.oauth_2_web_flow_poll(&state).await;
171 match status {
172 Ok(token) => return Ok(token),
173 Err(err) => match err {
174 golem_cloud_client::Error::Item(LoginOauth2WebFlowPollError::Error202(_)) => {
175 attempts += 1;
176 if attempts >= max_attempts {
177 bail!("OAuth2 workflow timeout")
178 }
179
180 sleep(delay).await;
181 }
182 _ => return Err(err).map_service_error(),
183 },
184 }
185 }
186 }
187}
188
189fn inform_user(data: &WebFlowAuthorizeUrlResponse) {
190 let url = &data.url.underline();
191
192 printdoc! {
193 "
194 ┌────────────────────────────────────────┐
195 │ Authenticate with GitHub │
196 │ │
197 │ Visit the following URL in a browser │
198 │ │
199 └────────────────────────────────────────┘
200 {url}
201 ──────────────────────────────────────────
202 "
203 }
204
205 println!("Waiting for authentication...");
206}
207
208#[derive(Clone, PartialEq, Debug)]
209pub struct CloudAuthentication(pub UnsafeToken);
210
211impl CloudAuthentication {
212 pub fn header(&self) -> String {
213 token_header(&self.0.secret)
214 }
215
216 pub fn account_id(&self) -> AccountId {
217 AccountId(self.0.data.account_id.clone())
218 }
219}
220
221pub fn token_header(secret: &TokenSecret) -> String {
222 format!("bearer {}", secret.value)
223}