diff --git a/CHANGELOG.md b/CHANGELOG.md index 8838b759..44fe683a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ +## Unreleased + +- Add get audit events command + ## v0.21.1 + - Add more stream stats ## v0.21.0 + - Fix url used for fetching streams - Return `is_end_sequence` on stream fetch - Make `transform_tag` optional on `create bucket` diff --git a/api/src/lib.rs b/api/src/lib.rs index 19679baf..5380cdab 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -13,6 +13,7 @@ use reqwest::{ IntoUrl, Proxy, Result as ReqwestResult, }; use resources::{ + comment::CommentTimestampFilter, dataset::{ QueryRequestParams, QueryResponse, StatisticsRequestParams as DatasetStatisticsRequestParams, SummaryRequestParams, @@ -33,6 +34,7 @@ use std::{cell::Cell, fmt::Display, path::Path}; use url::Url; use crate::resources::{ + audit::{AuditQueryFilter, AuditQueryRequest, AuditQueryResponse}, bucket::{ CreateRequest as CreateBucketRequest, CreateResponse as CreateBucketResponse, GetAvailableResponse as GetAvailableBucketsResponse, GetResponse as GetBucketResponse, @@ -408,6 +410,27 @@ impl Client { ) } + pub fn get_audit_events( + &self, + minimum_timestamp: Option>, + maximum_timestamp: Option>, + continuation: Option, + ) -> Result { + self.post::<_, _, AuditQueryResponse>( + self.endpoints.audit_events_query()?, + AuditQueryRequest { + continuation, + filter: AuditQueryFilter { + timestamp: CommentTimestampFilter { + minimum: minimum_timestamp, + maximum: maximum_timestamp, + }, + }, + }, + Retry::Yes, + ) + } + pub fn get_validation( &self, dataset_name: &DatasetFullName, @@ -1314,6 +1337,10 @@ impl Endpoints { }) } + fn audit_events_query(&self) -> Result { + construct_endpoint(&self.base, &["api", "v1", "audit_events", "query"]) + } + fn validation( &self, dataset_name: &DatasetFullName, diff --git a/api/src/resources/audit.rs b/api/src/resources/audit.rs new file mode 100644 index 00000000..99546b7b --- /dev/null +++ b/api/src/resources/audit.rs @@ -0,0 +1,201 @@ +use chrono::{DateTime, Utc}; + +use crate::{Continuation, DatasetId, DatasetName, Email, ProjectName, UserId, Username}; + +use super::{comment::CommentTimestampFilter, project::Id as ProjectId}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AuditQueryFilter { + pub timestamp: CommentTimestampFilter, +} + +#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] +pub struct AuditEventId(pub String); + +#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] +pub struct AuditEventType(pub String); + +#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] +pub struct AuditTenantName(pub String); + +#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] +pub struct AuditTenantId(pub String); + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AuditQueryRequest { + pub filter: AuditQueryFilter, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub continuation: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AuditEvent { + actor_user_id: UserId, + actor_tenant_id: AuditTenantId, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + dataset_ids: Vec, + event_id: AuditEventId, + event_type: AuditEventType, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + project_ids: Vec, + tenant_ids: Vec, + timestamp: DateTime, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PrintableAuditEvent { + pub actor_email: Email, + pub actor_tenant_name: AuditTenantName, + pub event_type: AuditEventType, + pub dataset_names: Vec, + pub event_id: AuditEventId, + pub project_names: Vec, + pub tenant_names: Vec, + pub timestamp: DateTime, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct AuditDataset { + id: DatasetId, + name: DatasetName, + project_id: ProjectId, + title: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct AuditProject { + id: ProjectId, + name: ProjectName, + tenant_id: AuditTenantId, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct AuditTenant { + id: AuditTenantId, + name: AuditTenantName, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct AuditUser { + display_name: Username, + email: Email, + id: UserId, + tenant_id: AuditTenantId, + username: Username, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AuditQueryResponse { + audit_events: Vec, + projects: Vec, + pub continuation: Option, + datasets: Vec, + tenants: Vec, + users: Vec, +} + +impl AuditQueryResponse { + pub fn into_iter_printable(self) -> QueryResponseIterator { + QueryResponseIterator { + response: self, + index: 0, + } + } + + fn get_user(&self, user_id: &UserId) -> Option<&AuditUser> { + self.users.iter().find(|user| user.id == *user_id) + } + + fn get_dataset(&self, dataset_id: &DatasetId) -> Option<&AuditDataset> { + self.datasets + .iter() + .find(|dataset| dataset.id == *dataset_id) + } + + fn get_project(&self, project_id: &ProjectId) -> Option<&AuditProject> { + self.projects + .iter() + .find(|project| project.id == *project_id) + } + + fn get_tenant(&self, tenant_id: &AuditTenantId) -> Option<&AuditTenant> { + self.tenants.iter().find(|tenant| tenant.id == *tenant_id) + } +} + +pub struct QueryResponseIterator { + response: AuditQueryResponse, + index: usize, +} + +impl Iterator for QueryResponseIterator { + type Item = PrintableAuditEvent; + fn next(&mut self) -> Option { + let event = self.response.audit_events.get(self.index)?; + + let actor_email = &self + .response + .get_user(&event.actor_user_id) + .unwrap_or_else(|| panic!("Could not find user for id `{}`", event.actor_user_id.0)) + .email; + + let dataset_names = event + .dataset_ids + .iter() + .map(|dataset_id| { + &self + .response + .get_dataset(dataset_id) + .unwrap_or_else(|| panic!("Could not get dataset for id `{}`", dataset_id.0)) + .name + }) + .cloned() + .collect(); + + let project_names = event + .project_ids + .iter() + .map(|project_id| { + &self + .response + .get_project(project_id) + .unwrap_or_else(|| panic!("Could not get project for id `{}`", project_id.0)) + .name + }) + .cloned() + .collect(); + + let tenant_names = event + .tenant_ids + .iter() + .map(|tenant_id| { + &self + .response + .get_tenant(tenant_id) + .unwrap_or_else(|| panic!("Could not get tenant for id `{}`", tenant_id.0)) + .name + }) + .cloned() + .collect(); + + let actor_tenant_name = &self + .response + .get_tenant(&event.actor_tenant_id) + .unwrap_or_else(|| panic!("Could not get tenant for id `{}`", event.actor_tenant_id.0)) + .name; + + self.index += 1; + + Some(PrintableAuditEvent { + event_type: event.event_type.clone(), + actor_tenant_name: actor_tenant_name.clone(), + event_id: event.event_id.clone(), + timestamp: event.timestamp, + actor_email: actor_email.clone(), + dataset_names, + project_names, + tenant_names, + }) + } +} diff --git a/api/src/resources/mod.rs b/api/src/resources/mod.rs index 9ea27430..a90201dc 100644 --- a/api/src/resources/mod.rs +++ b/api/src/resources/mod.rs @@ -1,3 +1,4 @@ +pub mod audit; pub mod bucket; pub mod comment; pub mod dataset; diff --git a/api/src/resources/tenant_id.rs b/api/src/resources/tenant_id.rs index 997ded03..0131ebd4 100644 --- a/api/src/resources/tenant_id.rs +++ b/api/src/resources/tenant_id.rs @@ -1,13 +1,14 @@ use crate::{Error, Result}; +use serde::{Deserialize, Serialize}; use std::{fmt::Display, str::FromStr}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] pub struct ReinferTenantId(String); -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] pub struct UiPathTenantId(String); -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum TenantId { Reinfer(ReinferTenantId), UiPath(UiPathTenantId), diff --git a/cli/src/commands/get/audit_events.rs b/cli/src/commands/get/audit_events.rs new file mode 100644 index 00000000..6ff807af --- /dev/null +++ b/cli/src/commands/get/audit_events.rs @@ -0,0 +1,47 @@ +use anyhow::Result; +use chrono::{DateTime, Utc}; +use log::info; +use reinfer_client::{resources::audit::PrintableAuditEvent, Client}; +use structopt::StructOpt; + +use crate::printer::Printer; + +#[derive(Debug, StructOpt)] +pub struct GetAuditEventsArgs { + #[structopt(short = "m", long = "minimum")] + /// Minimum Timestamp for audit events + minimum_timestamp: Option>, + + #[structopt(short = "M", long = "maximum")] + /// Maximum Timestamp for audit events + maximum_timestamp: Option>, +} + +pub fn get(client: &Client, args: &GetAuditEventsArgs, printer: &Printer) -> Result<()> { + let GetAuditEventsArgs { + minimum_timestamp, + maximum_timestamp, + } = args; + + let mut continuation = None; + + let mut all_printable_events = Vec::new(); + + loop { + let audit_events = + client.get_audit_events(*minimum_timestamp, *maximum_timestamp, continuation)?; + let mut printable_events: Vec = + audit_events.clone().into_iter_printable().collect(); + + all_printable_events.append(&mut printable_events); + + if audit_events.continuation.is_none() { + break; + } else { + info!("Downloaded {} events", all_printable_events.len()); + continuation = audit_events.continuation + } + } + + printer.print_resources(all_printable_events.iter()) +} diff --git a/cli/src/commands/get/mod.rs b/cli/src/commands/get/mod.rs index 6820d797..f3c768f0 100644 --- a/cli/src/commands/get/mod.rs +++ b/cli/src/commands/get/mod.rs @@ -1,3 +1,4 @@ +mod audit_events; mod buckets; mod comments; mod datasets; @@ -13,6 +14,7 @@ use scoped_threadpool::Pool; use structopt::StructOpt; use self::{ + audit_events::GetAuditEventsArgs, buckets::GetBucketsArgs, comments::{GetManyCommentsArgs, GetSingleCommentArgs}, datasets::GetDatasetsArgs, @@ -72,6 +74,10 @@ pub enum GetArgs { #[structopt(name = "quotas")] /// List all quotas for current tenant Quotas, + + #[structopt(name = "audit-events")] + /// Get audit events for current tenant + AuditEvents(GetAuditEventsArgs), } pub fn run(args: &GetArgs, client: Client, printer: &Printer, pool: &mut Pool) -> Result<()> { @@ -88,5 +94,6 @@ pub fn run(args: &GetArgs, client: Client, printer: &Printer, pool: &mut Pool) - GetArgs::Users(args) => users::get(&client, args, printer), GetArgs::CurrentUser => users::get_current_user(&client, printer), GetArgs::Quotas => quota::get(&client, printer), + GetArgs::AuditEvents(args) => audit_events::get(&client, args, printer), } } diff --git a/cli/src/printer.rs b/cli/src/printer.rs index 5a8270c9..cb2bd360 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -2,7 +2,7 @@ use super::thousands::Thousands; use colored::Colorize; use prettytable::{format, row, Row, Table}; use reinfer_client::{ - resources::{dataset::DatasetAndStats, quota::Quota}, + resources::{audit::PrintableAuditEvent, dataset::DatasetAndStats, quota::Quota}, Bucket, Dataset, Project, Source, Statistics, Stream, User, }; use serde::{Serialize, Serializer}; @@ -255,6 +255,52 @@ impl DisplayTable for User { } } +impl DisplayTable for PrintableAuditEvent { + fn to_table_headers() -> Row { + row![bFg => "Timestamp", "Event Id", "Event Type", "Actor Email", "Actor Tenant", "Dataset Names", "Project Names", "Tenant Names"] + } + + fn to_table_row(&self) -> Row { + row![ + self.timestamp, + self.event_id.0, + self.event_type.0, + self.actor_email.0, + self.actor_tenant_name.0, + if self.dataset_names.is_empty() { + "none".dimmed() + } else { + self.dataset_names + .iter() + .map(|dataset| dataset.0.clone()) + .collect::>() + .join(" & ") + .normal() + }, + if self.project_names.is_empty() { + "none".dimmed() + } else { + self.project_names + .iter() + .map(|project| project.0.clone()) + .collect::>() + .join(" & ") + .normal() + }, + if self.tenant_names.is_empty() { + "none".dimmed() + } else { + self.tenant_names + .iter() + .map(|name| name.0.clone()) + .collect::>() + .join(" & ") + .normal() + } + ] + } +} + /// Helper trait to allow collection of resources to be converted into a table. pub trait IntoTable { fn into_table(self) -> Table;