Skip to content

Commit e400dca

Browse files
committed
feat: support custom credential helpers
Signed-off-by: Roman Volosatovs <[email protected]>
1 parent 1a5737e commit e400dca

File tree

8 files changed

+153
-31
lines changed

8 files changed

+153
-31
lines changed

src/cli/package/info.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use crate::drawbridge::{client, TagSpec};
44

5+
use std::ffi::OsString;
6+
57
use anyhow::Context;
68
use camino::Utf8PathBuf;
79
use clap::Args;
@@ -13,12 +15,19 @@ pub struct Options {
1315
ca_bundle: Option<Utf8PathBuf>,
1416
#[clap(long, env = "ENARX_INSECURE_AUTH_TOKEN")]
1517
insecure_auth_token: Option<String>,
18+
#[clap(long, env = "ENARX_CREDENTIAL_HELPER")]
19+
credential_helper: Option<OsString>,
1620
spec: TagSpec,
1721
}
1822

1923
impl Options {
2024
pub fn execute(self) -> anyhow::Result<()> {
21-
let cl = client(self.spec.host, self.insecure_auth_token, self.ca_bundle)?;
25+
let cl = client(
26+
&self.spec.host,
27+
&self.insecure_auth_token,
28+
&self.ca_bundle,
29+
&self.credential_helper,
30+
)?;
2231
let tag = cl.tag(&self.spec.ctx);
2332
let tag_entry = tag
2433
.get()

src/cli/package/publish.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use crate::drawbridge::{client, TagSpec};
44

5+
use std::ffi::OsString;
6+
57
use anyhow::Context;
68
use camino::Utf8PathBuf;
79
use clap::Args;
@@ -13,13 +15,20 @@ pub struct Options {
1315
ca_bundle: Option<Utf8PathBuf>,
1416
#[clap(long, env = "ENARX_INSECURE_AUTH_TOKEN")]
1517
insecure_auth_token: Option<String>,
18+
#[clap(long, env = "ENARX_CREDENTIAL_HELPER")]
19+
credential_helper: Option<OsString>,
1620
spec: TagSpec,
1721
path: Utf8PathBuf,
1822
}
1923

2024
impl Options {
2125
pub fn execute(self) -> anyhow::Result<()> {
22-
let cl = client(self.spec.host, self.insecure_auth_token, self.ca_bundle)?;
26+
let cl = client(
27+
&self.spec.host,
28+
&self.insecure_auth_token,
29+
&self.ca_bundle,
30+
&self.credential_helper,
31+
)?;
2332
let tag = cl.tag(&self.spec.ctx);
2433
let (_tag_created, _tree_created) = tag
2534
.create_from_path_unsigned(self.path)

src/cli/repo/info.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use crate::drawbridge::{client, RepoSpec};
44

5+
use std::ffi::OsString;
6+
57
use anyhow::Context;
68
use camino::Utf8PathBuf;
79
use clap::Args;
@@ -21,12 +23,19 @@ pub struct Options {
2123
ca_bundle: Option<Utf8PathBuf>,
2224
#[clap(long, env = "ENARX_INSECURE_AUTH_TOKEN")]
2325
insecure_auth_token: Option<String>,
26+
#[clap(long, env = "ENARX_CREDENTIAL_HELPER")]
27+
credential_helper: Option<OsString>,
2428
spec: RepoSpec,
2529
}
2630

2731
impl Options {
2832
pub fn execute(self) -> anyhow::Result<()> {
29-
let cl = client(self.spec.host, self.insecure_auth_token, self.ca_bundle)?;
33+
let cl = client(
34+
&self.spec.host,
35+
&self.insecure_auth_token,
36+
&self.ca_bundle,
37+
&self.credential_helper,
38+
)?;
3039
let repo = cl.repository(&self.spec.ctx);
3140
let config = repo
3241
.get()

src/cli/repo/register.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use crate::drawbridge::{client, RepoSpec};
44

5+
use std::ffi::OsString;
6+
57
use anyhow::Context;
68
use camino::Utf8PathBuf;
79
use clap::Args;
@@ -14,12 +16,19 @@ pub struct Options {
1416
ca_bundle: Option<Utf8PathBuf>,
1517
#[clap(long, env = "ENARX_INSECURE_AUTH_TOKEN")]
1618
insecure_auth_token: Option<String>,
19+
#[clap(long, env = "ENARX_CREDENTIAL_HELPER")]
20+
credential_helper: Option<OsString>,
1721
spec: RepoSpec,
1822
}
1923

2024
impl Options {
2125
pub fn execute(self) -> anyhow::Result<()> {
22-
let cl = client(self.spec.host, self.insecure_auth_token, self.ca_bundle)?;
26+
let cl = client(
27+
&self.spec.host,
28+
&self.insecure_auth_token,
29+
&self.ca_bundle,
30+
&self.credential_helper,
31+
)?;
2332
let repo = cl.repository(&self.spec.ctx);
2433
let repo_config = RepositoryConfig {
2534
// TODO: support deploying from private repos

src/cli/user/info.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use crate::drawbridge::{client, UserSpec};
44

5+
use std::ffi::OsString;
6+
57
use anyhow::Context;
68
use camino::Utf8PathBuf;
79
use clap::Args;
@@ -13,12 +15,19 @@ pub struct Options {
1315
ca_bundle: Option<Utf8PathBuf>,
1416
#[clap(long, env = "ENARX_INSECURE_AUTH_TOKEN")]
1517
insecure_auth_token: Option<String>,
18+
#[clap(long, env = "ENARX_CREDENTIAL_HELPER")]
19+
credential_helper: Option<OsString>,
1620
spec: UserSpec,
1721
}
1822

1923
impl Options {
2024
pub fn execute(self) -> anyhow::Result<()> {
21-
let cl = client(self.spec.host, self.insecure_auth_token, self.ca_bundle)?;
25+
let cl = client(
26+
&self.spec.host,
27+
&self.insecure_auth_token,
28+
&self.ca_bundle,
29+
&self.credential_helper,
30+
)?;
2231
let user = cl.user(&self.spec.ctx);
2332
let record = user
2433
.get()

src/cli/user/login.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use crate::drawbridge::login;
44

5+
use std::ffi::OsString;
6+
57
use clap::Args;
68
use oauth2::url::Url;
79

@@ -12,16 +14,19 @@ pub struct Options {
1214
oidc_domain: Url,
1315
#[clap(long, default_value = "4NuaJxkQv8EZBeJKE56R57gKJbxrTLG2")]
1416
oidc_client_id: String,
17+
#[clap(long, env = "ENARX_CREDENTIAL_HELPER")]
18+
credential_helper: Option<OsString>,
1519
}
1620

1721
impl Options {
1822
pub fn execute(self) -> anyhow::Result<()> {
1923
let Self {
20-
oidc_domain,
24+
ref oidc_domain,
2125
oidc_client_id,
26+
ref credential_helper,
2227
} = self;
2328

24-
login(oidc_domain, oidc_client_id)?;
29+
login(oidc_domain, oidc_client_id, credential_helper)?;
2530

2631
println!("Login successful.");
2732

src/cli/user/register.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use crate::drawbridge::{client, get_token, login, UserSpec};
44

5+
use std::ffi::OsString;
6+
57
use anyhow::Context;
68
use camino::Utf8PathBuf;
79
use clap::Args;
@@ -19,6 +21,8 @@ pub struct Options {
1921
ca_bundle: Option<Utf8PathBuf>,
2022
#[clap(long, env = "ENARX_INSECURE_AUTH_TOKEN")]
2123
insecure_auth_token: Option<String>,
24+
#[clap(long, env = "ENARX_CREDENTIAL_HELPER")]
25+
credential_helper: Option<OsString>,
2226
#[clap(long, default_value = "https://auth.profian.com/")]
2327
oidc_domain: Url,
2428
#[clap(long, default_value = "4NuaJxkQv8EZBeJKE56R57gKJbxrTLG2")]
@@ -28,25 +32,38 @@ pub struct Options {
2832

2933
impl Options {
3034
pub fn execute(self) -> anyhow::Result<()> {
35+
let Self {
36+
ref ca_bundle,
37+
ref insecure_auth_token,
38+
ref oidc_domain,
39+
oidc_client_id,
40+
ref spec,
41+
ref credential_helper,
42+
} = self;
43+
3144
// If we don't find a token saved locally, initiate an interactive login
32-
let token = match get_token(self.insecure_auth_token.clone()) {
45+
let token = match get_token(insecure_auth_token, credential_helper) {
3346
Ok(token) => token,
34-
_ => login(self.oidc_domain.clone(), self.oidc_client_id.clone())?,
47+
_ => login(oidc_domain, oidc_client_id.clone(), credential_helper)?,
3548
};
3649

37-
let cl = client(self.spec.host, Some(token.clone()), self.ca_bundle)?;
38-
let user = cl.user(&self.spec.ctx);
50+
let cl = client(
51+
&spec.host,
52+
&Some(token.clone()),
53+
ca_bundle,
54+
credential_helper,
55+
)?;
56+
let user = cl.user(&spec.ctx);
3957

4058
let provider_metadata = CoreProviderMetadata::discover(
41-
&IssuerUrl::new(self.oidc_domain.to_string())
42-
.context("Failed to construct issuer URL")?,
59+
&IssuerUrl::new(oidc_domain.to_string()).context("Failed to construct issuer URL")?,
4360
http_client,
4461
)
4562
.context("Failed to discover provider metadata")?;
4663

4764
let oidc_client = CoreClient::from_provider_metadata(
4865
provider_metadata,
49-
ClientId::new(self.oidc_client_id),
66+
ClientId::new(oidc_client_id),
5067
None,
5168
)
5269
.set_auth_type(AuthType::RequestBody);

src/drawbridge.rs

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
// SPDX-License-Identifier: Apache-2.0
22

3+
use std::borrow::Borrow;
4+
use std::ffi::OsStr;
35
use std::fs::File;
6+
use std::io::{stderr, Write};
7+
use std::process::{Command, Stdio};
48
use std::str::FromStr;
9+
use std::thread::spawn;
510
use std::time::Duration;
611

7-
use anyhow::Context;
12+
use anyhow::{bail, Context};
813
use camino::Utf8PathBuf;
914
use drawbridge_client::types::{RepositoryContext, TagContext, UserContext};
1015
use drawbridge_client::Client;
@@ -94,23 +99,41 @@ fn parse_user(slug: &str) -> (String, &str) {
9499
(host.to_string(), user)
95100
}
96101

97-
pub fn get_token(provided_token: Option<String>) -> anyhow::Result<String> {
98-
let token = match provided_token {
99-
Some(token) => token,
100-
None => keyring::Entry::new("enarx", "oidc_domain")
102+
pub fn get_token(
103+
provided_token: &Option<impl AsRef<str>>,
104+
helper: &Option<impl AsRef<OsStr>>,
105+
) -> anyhow::Result<String> {
106+
if let Some(token) = provided_token {
107+
Ok(token.as_ref().into())
108+
} else if let Some(helper) = helper {
109+
let output = Command::new(helper)
110+
.arg("show")
111+
.output()
112+
.context("Failed to execute credential helper")?;
113+
stderr()
114+
.write_all(&output.stderr)
115+
.context("Failed to write stderr")?;
116+
if output.status.success() {
117+
String::from_utf8(output.stdout).context("Credential helper stdout is not valid UTF-8")
118+
} else if let Some(code) = output.status.code() {
119+
bail!("Credential helper failed with exit code {code}")
120+
} else {
121+
bail!("Credential helper was killed")
122+
}
123+
} else {
124+
keyring::Entry::new("enarx", "oidc_domain")
101125
.get_password()
102-
.context("Failed to read credentials from keyring")?,
103-
};
104-
105-
Ok(token)
126+
.context("Failed to read credentials from keyring")
127+
}
106128
}
107129

108130
pub fn client(
109-
host: String,
110-
insecure_token: Option<String>,
111-
ca_bundle: Option<Utf8PathBuf>,
131+
host: &str,
132+
insecure_token: &Option<String>,
133+
ca_bundle: &Option<Utf8PathBuf>,
134+
helper: &Option<impl AsRef<OsStr>>,
112135
) -> anyhow::Result<Client> {
113-
let token = get_token(insecure_token)?;
136+
let token = get_token(insecure_token, helper)?;
114137

115138
let url = format!("https://{host}");
116139

@@ -147,7 +170,12 @@ pub fn client(
147170
Ok(cl)
148171
}
149172

150-
pub fn login(oidc_domain: Url, oidc_client_id: String) -> anyhow::Result<String> {
173+
pub fn login(
174+
oidc_domain: &impl Borrow<Url>,
175+
oidc_client_id: String,
176+
helper: &Option<impl AsRef<OsStr>>,
177+
) -> anyhow::Result<String> {
178+
let oidc_domain = oidc_domain.borrow();
151179
let dev_auth_url = DeviceAuthorizationUrl::new(format!("{oidc_domain}oauth/device/code"))
152180
.context("Failed to construct device authorization URL")?;
153181
let auth_url = AuthUrl::new(format!("{oidc_domain}authorize"))
@@ -188,9 +216,36 @@ pub fn login(oidc_domain: Url, oidc_client_id: String) -> anyhow::Result<String>
188216

189217
// TODO: graceful timeout, so that users are not forced to Ctrl+C if the server errors
190218
let secret = res.access_token().secret();
191-
keyring::Entry::new("enarx", "oidc_domain")
192-
.set_password(secret)
193-
.context("Failed to save user credentials")?;
219+
if let Some(helper) = helper {
220+
let mut helper = Command::new(helper)
221+
.stdin(Stdio::piped())
222+
.arg("insert")
223+
.spawn()
224+
.context("Failed to spawn credential helper command")?;
225+
let mut stdin = helper.stdin.take().context("Failed to open stdin")?;
226+
let secret = secret.clone();
227+
spawn(move || {
228+
stdin
229+
.write_all(secret.as_bytes())
230+
.context("Failed to write secret to credential helper stdin")
231+
})
232+
.join()
233+
.expect("Failed to join stdin pipe thread")?;
234+
let output = helper
235+
.wait()
236+
.context("Failed to wait for credential helper to exit")?;
237+
if !output.success() {
238+
if let Some(code) = output.code() {
239+
bail!("Credential helper failed with exit code {code}")
240+
} else {
241+
bail!("Credential helper was killed")
242+
}
243+
}
244+
} else {
245+
keyring::Entry::new("enarx", "oidc_domain")
246+
.set_password(secret)
247+
.context("Failed to save user credentials")?;
248+
}
194249
println!("Credentials saved locally.");
195250

196251
Ok(secret.to_string())

0 commit comments

Comments
 (0)