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

Add command to list credentials created on a Capella cluster #575

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 40 additions & 1 deletion docs/commands/credentials.adoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,43 @@
=== `credentials create`
=== `credentials`

The `credentials` commands are used to perform credential management operations against Capella clusters.

==== `credentials`

Lists all the credentials on the active cluster:

[options="nowrap"]
```
๐Ÿ‘ค Charlie ๐Ÿ  remote
> credentials
โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚ # โ”‚ id โ”‚ name โ”‚ cluster โ”‚ access โ”‚
โ”œโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 0 โ”‚ 354dd662-4d6a-4e1c-80f3-6c78eafa405a โ”‚ Administrator โ”‚ remote โ”‚ โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ # โ”‚ bucket โ”‚ scopes โ”‚ privileges โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 0 โ”‚ testing โ”‚ โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ•ฎ โ”‚ โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 0 โ”‚ * โ”‚ โ”‚ โ”‚ 0 โ”‚ data_reader โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ•ฏ โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 1 โ”‚ travel-sample โ”‚ โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 0 โ”‚ inventory โ”‚ โ”‚ โ”‚ 0 โ”‚ data_reader โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 1 โ”‚ tenant_agent_00 โ”‚ โ”‚ โ”‚ 1 โ”‚ data_writer โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚
โ”‚ 1 โ”‚ f0668638-0cf1-417c-8521-abbd1cbf0624 โ”‚ Administrator2 โ”‚ remote โ”‚ โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ # โ”‚ bucket โ”‚ scopes โ”‚ privileges โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 0 โ”‚ * โ”‚ โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ•ฎ โ”‚ โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 0 โ”‚ * โ”‚ โ”‚ โ”‚ 0 โ”‚ data_reader โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ•ฏ โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚
โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
```

In the access section the wild card `*` means that these credentials have access to all those types of resource.
To list the credentials of a cluster other than the active cluster you can use the `--clusters` flag.

==== `credentials create`

Creates credentials using the given username and password against the active Capella cluster.
Note that this command will not work against locally hosted Couchbase Server clusters.
Expand Down
161 changes: 161 additions & 0 deletions src/cli/credentials.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use crate::cli::client_error_to_shell_error;
use crate::cli::util::{
cluster_from_conn_str, cluster_identifiers_from, find_org_id, find_project_id,
get_active_cluster, NuValueMap,
};
use crate::state::State;
use nu_protocol::engine::{Call, Command, EngineState, Stack};
use nu_protocol::{
Category, IntoInterruptiblePipelineData, PipelineData, Record, ShellError, Signature,
SyntaxShape, Value,
};
use nu_utils::SharedCow;
use std::sync::{Arc, Mutex};

#[derive(Clone)]
pub struct Credentials {
state: Arc<Mutex<State>>,
}

impl Credentials {
pub fn new(state: Arc<Mutex<State>>) -> Self {
Self { state }
}
}

impl Command for Credentials {
fn name(&self) -> &str {
"credentials"
}

fn signature(&self) -> Signature {
Signature::build("credentials")
.named(
"clusters",
SyntaxShape::String,
"the clusters which should be contacted",
None,
)
.category(Category::Custom("couchbase".to_string()))
}

fn usage(&self) -> &str {
"Lists existing credentials on a Capella cluster"
}

fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
credentials(self.state.clone(), engine_state, stack, call, input)
}
}

fn credentials(
state: Arc<Mutex<State>>,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let span = call.head;
let signals = engine_state.signals().clone();

let cluster_identifiers = cluster_identifiers_from(engine_state, stack, &state, call, true)?;
let guard = state.lock().unwrap();

let mut results: Vec<Value> = vec![];
for identifier in cluster_identifiers {
let cluster = get_active_cluster(identifier.clone(), &guard, span)?;

let org = guard.named_or_active_org(cluster.capella_org())?;

let client = org.client();

let org_id = find_org_id(signals.clone(), &client, span)?;

let project_id = find_project_id(
signals.clone(),
guard.active_project().unwrap(),
&client,
span,
org_id.clone(),
)?;

let json_cluster = cluster_from_conn_str(
identifier.clone(),
signals.clone(),
cluster.hostnames().clone(),
&client,
span,
org_id.clone(),
project_id.clone(),
)?;

let credentials = client
.list_credentials(org_id, project_id, json_cluster.id(), signals.clone())
.map_err(|e| client_error_to_shell_error(e, span))?;

for creds in credentials.data() {
let mut collected = NuValueMap::default();
collected.add_string("id", creds.id(), span);
collected.add_string("name", creds.name(), span);
collected.add_string("cluster", identifier.clone(), span);

let mut access_records = vec![];
for acc in creds.access() {
let cols = vec![
"bucket".to_string(),
"scopes".to_string(),
"privileges".to_string(),
];
let mut vals = vec![];

vals.push(Value::String {
val: acc.bucket(),
internal_span: span,
});

let mut scope_values = vec![];
for scope in acc.scopes() {
scope_values.push(Value::String {
val: scope,
internal_span: span,
})
}

vals.push(Value::List {
vals: scope_values,
internal_span: span,
});

let mut privilege_values = vec![];
for privilege in acc.privileges() {
privilege_values.push(Value::String {
val: privilege,
internal_span: span,
})
}

vals.push(Value::List {
vals: privilege_values,
internal_span: span,
});

let access = Record::from_raw_cols_vals(cols, vals, span, span).unwrap();
access_records.push(Value::Record {
val: SharedCow::new(access),
internal_span: span,
});
}

collected.add_vec("access", access_records, span);
results.push(collected.into_value(span))
}
}

Ok(results.into_pipeline_data(span, signals))
}
2 changes: 2 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ mod columnar_clusters_create;
mod columnar_clusters_drop;
mod columnar_databases;
mod columnar_query;
mod credentials;
mod credentials_create;
mod ctrlc_future;
mod doc;
Expand Down Expand Up @@ -126,6 +127,7 @@ pub use columnar_clusters_create::ColumnarClustersCreate;
pub use columnar_clusters_drop::ColumnarClustersDrop;
pub use columnar_databases::ColumnarDatabases;
pub use columnar_query::ColumnarQuery;
pub use credentials::Credentials;
pub use credentials_create::CredentialsCreate;
pub use ctrlc_future::CtrlcFuture;
pub use doc::Doc;
Expand Down
8 changes: 8 additions & 0 deletions src/cli/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,14 @@ impl NuValueMap {
});
}

pub fn add_vec(&mut self, name: impl Into<String>, vec: Vec<Value>, span: Span) {
self.cols.push(name.into());
self.vals.push(Value::List {
vals: vec,
internal_span: span,
});
}

pub fn into_value(self, span: Span) -> Value {
Value::Record {
val: SharedCow::new(
Expand Down
48 changes: 45 additions & 3 deletions src/client/cloud.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::cli::CtrlcFuture;
use crate::client::cloud_json::{
Cluster, ClustersResponse, Collection, CollectionsResponse, ColumnarCluster,
ColumnarClustersResponse, OrganizationsResponse, ProjectsResponse, ScopesResponse,
ColumnarClustersResponse, CredentialsResponse, OrganizationsResponse, ProjectsResponse,
ScopesResponse,
};
use crate::client::error::ClientError;
use crate::client::http_handler::{HttpResponse, HttpVerb};
Expand Down Expand Up @@ -421,6 +422,31 @@ impl CapellaClient {
Ok(())
}

pub fn list_credentials(
&self,
org_id: String,
project_id: String,
cluster_id: String,
signals: Signals,
) -> Result<CredentialsResponse, ClientError> {
let request = CapellaRequest::CredentialsList {
org_id,
project_id,
cluster_id,
};
let response = self.capella_request(request, signals)?;

if response.status() != 200 {
return Err(ClientError::RequestFailed {
reason: Some(response.content().into()),
key: None,
});
}

let resp: CredentialsResponse = serde_json::from_str(response.content())?;
Ok(resp)
}

pub fn create_bucket(
&self,
org_id: String,
Expand Down Expand Up @@ -852,6 +878,11 @@ pub enum CapellaRequest {
cluster_id: String,
payload: String,
},
CredentialsList {
org_id: String,
project_id: String,
cluster_id: String,
},
}

impl CapellaRequest {
Expand Down Expand Up @@ -1086,6 +1117,16 @@ impl CapellaRequest {
org_id, project_id, cluster_id
)
}
Self::CredentialsList {
org_id,
project_id,
cluster_id,
} => {
format!(
"/v4/organizations/{}/projects/{}/clusters/{}/users",
org_id, project_id, cluster_id
)
}
}
}

Expand Down Expand Up @@ -1116,6 +1157,7 @@ impl CapellaRequest {
Self::CollectionDelete { .. } => HttpVerb::Delete,
Self::CollectionList { .. } => HttpVerb::Get,
Self::CredentialsCreate { .. } => HttpVerb::Post,
Self::CredentialsList { .. } => HttpVerb::Get,
}
}

Expand All @@ -1139,11 +1181,11 @@ impl CapellaRequest {
fn handle_cluster_management_response(response: HttpResponse) -> Result<(), ClientError> {
match response.status() {
202 => {Ok(())}
403 => return Err(ClientError::AccessDenied {
403 => Err(ClientError::AccessDenied {
reason: "Make sure that the API key has the Cluster Manager role enabled for the target project".to_string()
}),
_ => {
return Err(ClientError::RequestFailed {
Err(ClientError::RequestFailed {
reason: Some(response.content().into()),
key: None,
})
Expand Down
Loading
Loading