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 1 commit
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
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
84 changes: 80 additions & 4 deletions src/client/cloud_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,42 @@ pub(crate) struct CredentialsCreateRequest {
access: Vec<Access>,
}

#[derive(Debug, Serialize)]
struct Access {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct Access {
privileges: Vec<String>,
resources: Resources,
}

impl Access {
pub fn privileges(&self) -> Vec<String> {
self.privileges.clone()
}

// Although resources holds a list of buckets the list only ever has one bucket in it,
// hence the hardcoded buckets[0] below
pub fn bucket(&self) -> String {
self.resources.buckets[0].name.clone()
}

pub fn scopes(&self) -> Vec<String> {
if let Some(scopes) = self.resources.buckets[0].scopes.clone() {
scopes.iter().map(|s| s.name.clone()).collect()
} else {
// If no scopes are listed in the response the access applies to all the scopes
vec!["*".to_string()]
}
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Resources {
buckets: Vec<Bucket>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Bucket {
name: String,
scopes: Option<Vec<Scope>>,
}

impl CredentialsCreateRequest {
Expand All @@ -518,11 +551,54 @@ impl CredentialsCreateRequest {
Self {
name,
password,
access: vec![Access { privileges }],
access: vec![Access {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was changed since the Access struct was changed to support reading the list credentials response. They have been hardcoded to values that preserve the behaviour from before the change, and this issue will allow users to specify scopes/buckets on credential creation: #576

privileges,
resources: Resources {
buckets: vec![Bucket {
name: "*".to_string(),
scopes: Some(vec![Scope {
name: "*".to_string(),
}]),
}],
},
}],
}
}
}

#[derive(Debug, Deserialize)]
pub(crate) struct CredentialsResponse {
data: Vec<Credential>,
}

impl CredentialsResponse {
pub fn data(&self) -> Vec<Credential> {
self.data.clone()
}
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub(crate) struct Credential {
id: String,
name: String,
audit: AuditData,
access: Vec<Access>,
}

impl Credential {
pub fn id(&self) -> String {
self.id.clone()
}

pub fn name(&self) -> String {
self.name.clone()
}

pub fn access(&self) -> Vec<Access> {
self.access.clone()
}
}

#[derive(Debug, Deserialize)]
pub struct ScopesResponse {
scopes: Vec<Scope>,
Expand All @@ -534,7 +610,7 @@ impl ScopesResponse {
}
}

#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Scope {
name: String,
}
Expand Down
Loading
Loading