Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

builder origin rbac subcommand #7841

Merged
merged 21 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ed81c10
builder origin rbac operations
jeremymv2 Apr 17, 2020
73db411
a rebase merge conflict fix and some improved error handling and cli …
jeremymv2 Jul 28, 2020
520ca21
hab client subcommand for origin rbac operations
jeremymv2 Apr 17, 2020
39092e8
correcting typo in error message
jeremymv2 Apr 17, 2020
f9ec5a7
clippy issues
jeremymv2 Jul 28, 2020
0c8864c
adding missing derive ConfigOpt
jeremymv2 Jul 28, 2020
640e2ed
fix rbac subcommand help to match configopt
jeremymv2 Jul 28, 2020
7016aa0
add missing discription for set configopt variant
jeremymv2 Jul 28, 2020
dd3c979
truing up structops with cli args and options
jeremymv2 Jul 29, 2020
7b1f484
an implementation with structopt that is suitable
jeremymv2 Aug 11, 2020
4c94bd8
fix rebase merge conflict
jeremymv2 Aug 11, 2020
6381ea0
adding origin and text_render utility module in core
jeremymv2 Aug 11, 2020
787dd7d
moving text render traits into core::util::text_render
jeremymv2 Aug 11, 2020
43ca9dd
removing WIP intermediary file artifact
jeremymv2 Aug 11, 2020
aff6e0e
removing unused Error entry for the builder-api-client
jeremymv2 Aug 11, 2020
633e750
origin member role constants
jeremymv2 Aug 11, 2020
6efd8ad
adding TabWriterIntoInnerFailed error variant
jeremymv2 Aug 12, 2020
3568a83
validator for parsing Url from args, environment, toml or default
jeremymv2 Aug 12, 2020
80dffb7
wire up type primitives all the way to backend builder-api-client set…
jeremymv2 Aug 12, 2020
9aee65a
fixup constants and adjust help text
jeremymv2 Aug 12, 2020
78f9d5e
moving constants into impl OriginMemberRole and better error handling
jeremymv2 Aug 12, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion components/builder-api-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ reqwest = { version = "*", features = ["blocking", "json", "stream"] }
serde = "*"
serde_derive = "*"
serde_json = { version = "*", features = [ "preserve_order" ] }
tabwriter = "*"
tee = "*"
tokio = { version = "*", features = ["full"] }
tokio-util = "*"
Expand Down
50 changes: 50 additions & 0 deletions components/builder-api-client/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{allow_std_io::AllowStdIo,
DisplayProgress,
OriginInfoResponse,
OriginKeyIdent,
OriginMemberRoleResponse,
OriginSecret,
Package,
PendingOriginInvitationsResponse,
Expand All @@ -22,6 +23,8 @@ use habitat_core::{crypto::keys::box_key_pair::WrappedSealedBox,
DEFAULT_CACHED_ARTIFACT_PERMISSIONS,
DEFAULT_PUBLIC_KEY_PERMISSIONS,
DEFAULT_SECRET_KEY_PERMISSIONS},
origin::{Origin,
OriginMemberRole},
package::{Identifiable,
PackageArchive,
PackageIdent,
Expand Down Expand Up @@ -1374,6 +1377,53 @@ impl BuilderAPIClient {
_ => Err(response::err_from_response(resp).await),
}
}

/// Get an origin member's role
///
/// # Failures
///
/// * Remote Builder is not available
pub async fn get_member_role(&self,
origin: Origin,
token: &str,
member_account: &str)
-> Result<OriginMemberRoleResponse> {
debug!("Getting member {} role from origin {}",
member_account, origin);

let path = format!("depot/origins/{}/users/{}/role", origin, member_account);
let resp = self.0.get(&path).bearer_auth(token).send().await?;
let resp = response::ok_if(resp, &[StatusCode::OK]).await?;

Ok(resp.json().await?)
}

/// Update an origin member's role
///
/// # Failures
///
/// * Remote Builder is not available
/// * Unprocessable role
/// * Insufficient Privileges
pub async fn update_member_role(&self,
origin: Origin,
token: &str,
member_account: &str,
role: OriginMemberRole)
-> Result<()> {
debug!("Updating member {} role to '{}' in origin {}",
member_account, role, origin);

let path = format!("depot/origins/{}/users/{}/role", origin, member_account);
response::ok_if_unit(self.0
.put_with_custom_url(&path, |url| {
url.query_pairs_mut().append_pair("role", &role.to_string());
})
.bearer_auth(token)
.send()
.await?,
&[StatusCode::NO_CONTENT]).await
}
}

fn origin_keys_path(origin: &str) -> String { format!("depot/origins/{}/keys", origin) }
Expand Down
49 changes: 12 additions & 37 deletions components/builder-api-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use habitat_core::{self as hab_core,
util};
util,
util::text_render::{tabify,
tabw,
TabularText}};
use habitat_http_client as hab_http;

#[macro_use]
Expand Down Expand Up @@ -29,8 +32,6 @@ use chrono::{DateTime,
Utc};
use reqwest::IntoUrl;
use serde::Serialize;
use serde_json::Value as Json;
use tabwriter::TabWriter;

use crate::hab_core::package::PackageIdent;
pub use crate::{builder::BuilderAPIClient,
Expand Down Expand Up @@ -228,35 +229,8 @@ mod json_date_format {
}
}

fn convert_to_json<T>(src: &T) -> Result<Json>
where T: Serialize
{
serde_json::to_value(src).map_err(|e| habitat_core::Error::RenderContextSerialization(e).into())
}

// Returns a library object that implements elastic tabstops
fn tabw() -> TabWriter<Vec<u8>> { TabWriter::new(Vec::new()) }

// Given a TabWriter object and a str slice, return a Result
// where the Ok() variant comprises a String with nicely tab aligned columns
fn tabify(mut tw: TabWriter<Vec<u8>>, s: &str) -> Result<String> {
write!(&mut tw, "{}", s)?;
tw.flush()?;
String::from_utf8(tw.into_inner().expect("TABWRITER into_inner")).map_err(|e| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this function already returns a Result, could we remove this expect in favor of propagating the Err?

Copy link
Contributor Author

@jeremymv2 jeremymv2 Aug 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 78f9d5e

habitat_core::Error::StringFromUtf8Error(e).into()
})
}

pub trait PortableText {
fn as_json(&self) -> Result<Json>;
}

pub trait TabularText {
fn as_tabbed(&self) -> Result<String>;
}

impl TabularText for UserOriginInvitationsResponse {
fn as_tabbed(&self) -> Result<String> {
fn as_tabbed(&self) -> std::result::Result<String, habitat_core::error::Error> {
let tw = tabw().padding(2).minwidth(5);
if !self.0.is_empty() {
let mut body = Vec::new();
Expand All @@ -278,7 +252,7 @@ impl TabularText for UserOriginInvitationsResponse {
}

impl TabularText for PendingOriginInvitationsResponse {
fn as_tabbed(&self) -> Result<String> {
fn as_tabbed(&self) -> std::result::Result<String, habitat_core::error::Error> {
let tw = tabw().padding(2).minwidth(5);
if !self.invitations.is_empty() {
let mut body = Vec::new();
Expand All @@ -298,7 +272,7 @@ impl TabularText for PendingOriginInvitationsResponse {
}

impl TabularText for OriginInfoResponse {
fn as_tabbed(&self) -> Result<String> {
fn as_tabbed(&self) -> std::result::Result<String, habitat_core::error::Error> {
let tw = tabw().padding(2).minwidth(5);
let mut body = Vec::new();
body.push(String::from("Owner Id\tOwner Account\tPrivate Key\tPackage Visibility"));
Expand All @@ -311,10 +285,6 @@ impl TabularText for OriginInfoResponse {
}
}

impl PortableText for OriginInfoResponse {
fn as_json(&self) -> Result<Json> { convert_to_json(&self) }
}

#[derive(Clone, Deserialize)]
pub struct OriginSecret {
pub id: String,
Expand Down Expand Up @@ -380,6 +350,11 @@ impl Client {
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct OriginMemberRoleResponse {
davidMcneil marked this conversation as resolved.
Show resolved Hide resolved
pub role: String,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
4 changes: 4 additions & 0 deletions components/common/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ pub enum Status {
Skipping,
Transferred,
Transferring,
Updating,
Updated,
Uploaded,
Uploading,
Using,
Expand Down Expand Up @@ -282,6 +284,8 @@ impl Status {
Status::Skipping => (Glyph::Elipses, "Skipping".into(), Color::Info),
Status::Transferred => (Glyph::CheckMark, "Transferred".into(), Color::Info),
Status::Transferring => (Glyph::RightArrow, "Transferring".into(), Color::Info),
Status::Updating => (Glyph::UpArrow, "Updating".into(), Color::Info),
Status::Updated => (Glyph::CheckMark, "Updated".into(), Color::Info),
Status::Uploaded => (Glyph::CheckMark, "Uploaded".into(), Color::Info),
Status::Uploading => (Glyph::UpArrow, "Uploading".into(), Color::Info),
Status::Using => (Glyph::RightArrow, "Using".into(), Color::Info),
Expand Down
1 change: 1 addition & 0 deletions components/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ serde = "*"
serde_derive = "*"
serde_json = { version = "*", features = [ "preserve_order" ] }
sodiumoxide = "*"
tabwriter = "*"
tar = "*"
tempfile = "*"
toml = { version = "*", features = [ "preserve_order" ] }
Expand Down
34 changes: 21 additions & 13 deletions components/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum Error {
BadBindingMode(String),
/// An invalid path to a keyfile was given.
BadKeyPath(String),
/// An invalid Builder origin member role
BadOriginMemberRole(String),
/// An operation expected a composite package
CompositePackageExpected(String),
/// Error reading raw contents of configuration file.
Expand Down Expand Up @@ -79,6 +81,8 @@ pub enum Error {
FullyQualifiedPackageIdentRequired(String),
/// Occurs when a service binding cannot be successfully parsed.
InvalidBinding(String),
/// Occurs when an origin is in an invalid format
InvalidOrigin(String),
/// Occurs when a package identifier string cannot be successfully parsed.
InvalidPackageIdent(String),
/// Occurs when a package target string cannot be successfully parsed.
Expand All @@ -87,12 +91,12 @@ pub enum Error {
InvalidPackageType(String),
/// Occurs when a port is not parsable.
InvalidPort(ParseIntError),
/// Occurs when a service group string cannot be successfully parsed.
InvalidServiceGroup(String),
/// Occurs when an origin is in an invalid format
InvalidOrigin(String),
/// Occurs when an OsString path cannot be converted to a String
InvalidPathString(ffi::OsString),
/// Occurs when a service group string cannot be successfully parsed.
InvalidServiceGroup(String),
/// Occurs when a Url is in an invalid format.
InvalidUrl(String),
/// Occurs when making lower level IO calls.
IO(io::Error),
/// Errors when joining paths :)
Expand Down Expand Up @@ -169,6 +173,9 @@ impl fmt::Display for Error {
format!("Invalid keypath: {}. Specify an absolute path to a file on disk.",
e)
}
Error::BadOriginMemberRole(ref value) => {
format!("Unknown origin member role '{}'", value)
}
Error::CompositePackageExpected(ref ident) => {
format!("The package is not a composite: {}", ident)
}
Expand Down Expand Up @@ -263,6 +270,12 @@ impl fmt::Display for Error {
<NAME> is a service name, and <SERVICE_GROUP> is a valid service group",
binding)
}
Error::InvalidOrigin(ref origin) => {
format!("Invalid origin: {}. Origins must begin with a lowercase letter or \
number. Allowed characters include lowercase letters, numbers, -, and _. \
No more than 255 characters.",
origin)
}
Error::InvalidPackageIdent(ref e) => {
format!("Invalid package identifier: {:?}. A valid identifier is in the form \
origin/name (example: acme/redis)",
Expand All @@ -274,21 +287,16 @@ impl fmt::Display for Error {
e)
}
Error::InvalidPackageType(ref e) => format!("Invalid package type: {}.", e),
Error::InvalidPathString(ref s) => {
format!("Could not generate String from path: {:?}", s)
}
Error::InvalidPort(ref e) => format!("Invalid port: {}.", e),
Error::InvalidServiceGroup(ref e) => {
format!("Invalid service group: {}. A valid service group string is in the form \
service.group (example: redis.production)",
e)
}
Error::InvalidOrigin(ref origin) => {
format!("Invalid origin: {}. Origins must begin with a lowercase letter or \
number. Allowed characters include lowercase letters, numbers, -, and _. \
No more than 255 characters.",
origin)
}
Error::InvalidPathString(ref s) => {
format!("Could not generate String from path: {:?}", s)
}
Error::InvalidUrl(ref url) => format!("Invalid url: {}", url),
Error::IO(ref err) => format!("{}", err),
Error::JoinPathsError(ref err) => format!("{}", err),
Error::LogonTypeNotGranted => {
Expand Down
6 changes: 3 additions & 3 deletions components/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod env;
pub mod error;
pub mod fs;
pub mod locked_env_var;
pub mod origin;
pub mod os;
pub mod package;
pub mod service;
Expand All @@ -22,11 +23,10 @@ pub mod util;

use std::fmt;

use serde_derive::{Deserialize,
Serialize};

pub use crate::os::{filesystem,
users};
use serde_derive::{Deserialize,
Serialize};

pub const AUTH_TOKEN_ENVVAR: &str = "HAB_AUTH_TOKEN";

Expand Down
91 changes: 91 additions & 0 deletions components/core/src/origin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use crate::{error::Error,
package::ident::is_valid_origin_name};
use serde_derive::{Deserialize,
Serialize};
use std::{fmt,
result,
str::FromStr};

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Origin(String);

impl Origin {
#[allow(clippy::needless_pass_by_value)]
pub fn validate(value: String) -> result::Result<(), String> {
if is_valid_origin_name(&value) {
Ok(())
} else {
Err(format!("'{}' is not valid. A valid origin contains a-z, \
0-9, and _ or - after the first character",
&value))
}
}
}

impl FromStr for Origin {
type Err = Error;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Origin::validate(s.to_string()).map_or_else(|e| Err(Error::InvalidOrigin(e)),
|_| Ok(Origin(s.to_string())))
}
}

impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) }
}

impl std::convert::TryFrom<&str> for Origin {
type Error = Error;

fn try_from(s: &str) -> Result<Self, Self::Error> { Self::from_str(s) }
}

#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum OriginMemberRole {
ReadonlyMember,
Member,
Maintainer,
Administrator,
Owner,
}

impl OriginMemberRole {
pub const ADMINISTRATOR: &'static str = "administrator";
pub const MAINTAINER: &'static str = "maintainer";
pub const MEMBER: &'static str = "member";
pub const OWNER: &'static str = "owner";
pub const READONLY_MEMBER: &'static str = "readonly_member";
}

impl Default for OriginMemberRole {
fn default() -> OriginMemberRole { OriginMemberRole::ReadonlyMember }
}

impl fmt::Display for OriginMemberRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match *self {
OriginMemberRole::ReadonlyMember => OriginMemberRole::READONLY_MEMBER,
OriginMemberRole::Member => OriginMemberRole::MEMBER,
OriginMemberRole::Maintainer => OriginMemberRole::MAINTAINER,
OriginMemberRole::Administrator => OriginMemberRole::ADMINISTRATOR,
OriginMemberRole::Owner => OriginMemberRole::OWNER,
};
write!(f, "{}", value)
}
}

impl FromStr for OriginMemberRole {
type Err = Error;

fn from_str(value: &str) -> result::Result<Self, Self::Err> {
match value.to_lowercase().as_ref() {
OriginMemberRole::READONLY_MEMBER => Ok(OriginMemberRole::ReadonlyMember),
OriginMemberRole::MEMBER => Ok(OriginMemberRole::Member),
OriginMemberRole::MAINTAINER => Ok(OriginMemberRole::Maintainer),
OriginMemberRole::ADMINISTRATOR => Ok(OriginMemberRole::Administrator),
OriginMemberRole::OWNER => Ok(OriginMemberRole::Owner),
_ => Err(Error::BadOriginMemberRole(value.to_string())),
}
}
}
Loading