Skip to content

Commit

Permalink
Add a registry authentication. (softprops#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
antage authored and softprops committed Mar 31, 2019
1 parent eb98b19 commit ac8789e
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 16 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -36,6 +36,7 @@ tokio-io = "0.1.11"
url = "1.7.2"
serde = { version = "1.0.87", features = ["derive"] }
serde_json = "1.0.38"
base64 = "0.10"

[dev-dependencies]
env_logger = "0.6.0"
Expand Down
28 changes: 28 additions & 0 deletions examples/imagepull_auth.rs
@@ -0,0 +1,28 @@
// cargo run --example imagepull_auth busybox username password

use shiplift::{Docker, PullOptions, RegistryAuth};
use std::env;
use tokio::prelude::{Future, Stream};

fn main() {
env_logger::init();
let docker = Docker::new();
let img = env::args()
.nth(1)
.expect("You need to specify an image name");
let username = env::args().nth(2).expect("You need to specify an username");
let password = env::args().nth(3).expect("You need to specify a password");
let auth = RegistryAuth::builder()
.username(username)
.password(password)
.build();
let fut = docker
.images()
.pull(&PullOptions::builder().image(img).auth(auth).build())
.for_each(|output| {
println!("{:?}", output);
Ok(())
})
.map_err(|e| eprintln!("Error: {}", e));
tokio::run(fut);
}
164 changes: 162 additions & 2 deletions src/builder.rs
Expand Up @@ -11,8 +11,116 @@ use std::{
};
use url::form_urlencoded;

#[derive(Clone, Serialize)]
#[serde(untagged)]
pub enum RegistryAuth {
Password {
username: String,
password: String,

#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,

#[serde(rename = "serveraddress")]
#[serde(skip_serializing_if = "Option::is_none")]
server_address: Option<String>,
},
Token {
#[serde(rename = "identitytoken")]
identity_token: String,
},
}

impl RegistryAuth {
/// return a new instance with token authentication
pub fn token<S>(token: S) -> RegistryAuth
where
S: Into<String>,
{
RegistryAuth::Token {
identity_token: token.into(),
}
}

/// return a new instance of a builder for authentication
pub fn builder() -> RegistryAuthBuilder {
RegistryAuthBuilder::default()
}

/// serialize authentication as JSON in base64
pub fn serialize(&self) -> String {
serde_json::to_string(self)
.map(|c| base64::encode(&c))
.unwrap()
}
}

#[derive(Default)]
pub struct RegistryAuthBuilder {
username: Option<String>,
password: Option<String>,
email: Option<String>,
server_address: Option<String>,
}

impl RegistryAuthBuilder {
pub fn username<I>(
&mut self,
username: I,
) -> &mut Self
where
I: Into<String>,
{
self.username = Some(username.into());
self
}

pub fn password<I>(
&mut self,
password: I,
) -> &mut Self
where
I: Into<String>,
{
self.password = Some(password.into());
self
}

pub fn email<I>(
&mut self,
email: I,
) -> &mut Self
where
I: Into<String>,
{
self.email = Some(email.into());
self
}

pub fn server_address<I>(
&mut self,
server_address: I,
) -> &mut Self
where
I: Into<String>,
{
self.server_address = Some(server_address.into());
self
}

pub fn build(&self) -> RegistryAuth {
RegistryAuth::Password {
username: self.username.clone().unwrap_or_else(String::new),
password: self.password.clone().unwrap_or_else(String::new),
email: self.email.clone(),
server_address: self.server_address.clone(),
}
}
}

#[derive(Default)]
pub struct PullOptions {
auth: Option<RegistryAuth>,
params: HashMap<&'static str, String>,
}

Expand All @@ -34,10 +142,15 @@ impl PullOptions {
)
}
}

pub(crate) fn auth_header(&self) -> Option<String> {
self.auth.clone().map(|a| a.serialize())
}
}

#[derive(Default)]
pub struct PullOptionsBuilder {
auth: Option<RegistryAuth>,
params: HashMap<&'static str, String>,
}

Expand Down Expand Up @@ -95,8 +208,17 @@ impl PullOptionsBuilder {
self
}

pub fn build(&self) -> PullOptions {
pub fn auth(
&mut self,
auth: RegistryAuth,
) -> &mut Self {
self.auth = Some(auth);
self
}

pub fn build(&mut self) -> PullOptions {
PullOptions {
auth: self.auth.take(),
params: self.params.clone(),
}
}
Expand Down Expand Up @@ -1370,7 +1492,7 @@ impl VolumeCreateOptionsBuilder {

#[cfg(test)]
mod tests {
use super::ContainerOptionsBuilder;
use super::{ContainerOptionsBuilder, RegistryAuth};

#[test]
fn container_options_simple() {
Expand Down Expand Up @@ -1463,4 +1585,42 @@ mod tests {
options.serialize().unwrap()
);
}

/// Test registry auth with token
#[test]
fn registry_auth_token() {
let options = RegistryAuth::token("abc");
assert_eq!(
base64::encode(r#"{"identitytoken":"abc"}"#),
options.serialize()
);
}

/// Test registry auth with username and password
#[test]
fn registry_auth_password_simple() {
let options = RegistryAuth::builder()
.username("user_abc")
.password("password_abc")
.build();
assert_eq!(
base64::encode(r#"{"username":"user_abc","password":"password_abc"}"#),
options.serialize()
);
}

/// Test registry auth with all fields
#[test]
fn registry_auth_password_all() {
let options = RegistryAuth::builder()
.username("user_abc")
.password("password_abc")
.email("email_abc")
.server_address("https://example.org")
.build();
assert_eq!(
base64::encode(r#"{"username":"user_abc","password":"password_abc","email":"email_abc","serveraddress":"https://example.org"}"#),
options.serialize()
);
}
}
27 changes: 19 additions & 8 deletions src/lib.rs
Expand Up @@ -29,8 +29,8 @@ pub use crate::{
builder::{
BuildOptions, ContainerConnectionOptions, ContainerFilter, ContainerListOptions,
ContainerOptions, EventsOptions, ExecContainerOptions, ImageFilter, ImageListOptions,
LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, RmContainerOptions,
VolumeCreateOptions,
LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, RegistryAuth,
RmContainerOptions, VolumeCreateOptions,
},
errors::Error,
};
Expand All @@ -55,7 +55,7 @@ use mime::Mime;
#[cfg(feature = "tls")]
use openssl::ssl::{SslConnector, SslFiletype, SslMethod};
use serde_json::Value;
use std::{borrow::Cow, env, path::Path, time::Duration};
use std::{borrow::Cow, env, iter, path::Path, time::Duration};
use tokio_codec::{FramedRead, LinesCodec};
use url::form_urlencoded;

Expand Down Expand Up @@ -141,7 +141,11 @@ impl<'a> Images<'a> {
match tarball::dir(&mut bytes, &opts.path[..]) {
Ok(_) => Box::new(
self.docker
.stream_post(&path.join("?"), Some((Body::from(bytes), tar())))
.stream_post(
&path.join("?"),
Some((Body::from(bytes), tar())),
None::<iter::Empty<_>>,
)
.and_then(|bytes| {
serde_json::from_slice::<'_, Value>(&bytes[..])
.map_err(Error::from)
Expand Down Expand Up @@ -194,8 +198,11 @@ impl<'a> Images<'a> {
if let Some(query) = opts.serialize() {
path.push(query);
}
let headers = opts
.auth_header()
.map(|a| iter::once(("X-Registry-Auth", a)));
self.docker
.stream_post::<Body>(&path.join("?"), None)
.stream_post::<Body, _>(&path.join("?"), None, headers)
// todo: give this a proper enum type
.map(|r| {
futures::stream::iter_result(
Expand Down Expand Up @@ -479,6 +486,7 @@ impl<'a, 'b> Container<'a, 'b> {
let chunk_stream = StreamReader::new(docker2.stream_post(
&format!("/exec/{}/start", id)[..],
Some((bytes, mime::APPLICATION_JSON)),
None::<iter::Empty<_>>,
));
FramedRead::new(chunk_stream, decoder)
})
Expand Down Expand Up @@ -1053,23 +1061,26 @@ impl Docker {
})
}

fn stream_post<B>(
fn stream_post<B, H>(
&self,
endpoint: &str,
body: Option<(B, Mime)>,
headers: Option<H>,
) -> impl Stream<Item = hyper::Chunk, Error = Error>
where
B: Into<Body>,
H: IntoIterator<Item = (&'static str, String)>,
{
self.transport.stream_chunks(Method::POST, endpoint, body)
self.transport
.stream_chunks(Method::POST, endpoint, body, headers)
}

fn stream_get(
&self,
endpoint: &str,
) -> impl Stream<Item = hyper::Chunk, Error = Error> {
self.transport
.stream_chunks::<Body>(Method::GET, endpoint, None)
.stream_chunks::<Body, iter::Empty<_>>(Method::GET, endpoint, None, None)
}

fn stream_post_upgrade_multiplexed<B>(
Expand Down

0 comments on commit ac8789e

Please sign in to comment.