diff --git a/.gitignore b/.gitignore index 77cc8c7f..d1328865 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target config.toml +mappers.toml !.cargo/config.toml spec.toml diff --git a/Cargo.lock b/Cargo.lock index 07e6abe1..c05af861 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2739,6 +2739,7 @@ dependencies = [ "config", "cookie", "crc32c", + "diesel", "dropshot", "dropshot-authorization-header", "dropshot-verified-body", diff --git a/rfd-api-spec.json b/rfd-api-spec.json index 7f1dff04..cf087079 100644 --- a/rfd-api-spec.json +++ b/rfd-api-spec.json @@ -122,6 +122,93 @@ } } }, + "/api-user/{identifier}/group": { + "post": { + "operationId": "add_api_user_to_group", + "parameters": [ + { + "in": "path", + "name": "identifier", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddGroupBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiUser_for_ApiPermission" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/api-user/{identifier}/group/{group_id}": { + "delete": { + "operationId": "remove_api_user_from_group", + "parameters": [ + { + "in": "path", + "name": "group_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "identifier", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiUser_for_ApiPermission" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/api-user/{identifier}/token": { "get": { "summary": "List the active and expired API tokens for a given user", @@ -327,6 +414,140 @@ } } }, + "/group": { + "get": { + "operationId": "get_groups", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_AccessGroup_for_ApiPermission", + "type": "array", + "items": { + "$ref": "#/components/schemas/AccessGroup_for_ApiPermission" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "create_group", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroupUpdateParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroup_for_ApiPermission" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/group/{group_id}": { + "put": { + "operationId": "update_group", + "parameters": [ + { + "in": "path", + "name": "group_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroupUpdateParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroup_for_ApiPermission" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "delete_group", + "parameters": [ + { + "in": "path", + "name": "group_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroup_for_ApiPermission" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/login/oauth/{provider}/code/authorize": { "get": { "summary": "Generate the remote provider login url and redirect the user", @@ -951,6 +1172,56 @@ } }, "schemas": { + "AccessGroupUpdateParams": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions_for_ApiPermission" + } + }, + "required": [ + "name", + "permissions" + ] + }, + "AccessGroup_for_ApiPermission": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "deleted_at": { + "nullable": true, + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions_for_ApiPermission" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "created_at", + "id", + "name", + "permissions", + "updated_at" + ] + }, "AccessTokenExchangeRequest": { "type": "object", "properties": { @@ -971,6 +1242,18 @@ "grant_type" ] }, + "AddGroupBody": { + "type": "object", + "properties": { + "group_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "group_id" + ] + }, "AddOAuthClientRedirectBody": { "type": "object", "properties": { @@ -1034,6 +1317,8 @@ "CreateApiUser", "UpdateApiUserSelf", "UpdateApiUserAll", + "ListGroups", + "CreateGroup", "GetAllRfds", "GetAssignedRfds", "GetAllDiscussions", @@ -1110,6 +1395,58 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "UpdateGroup": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "UpdateGroup" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "AddToGroup": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "AddToGroup" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "RemoveFromGroup": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "RemoveFromGroup" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "DeleteGroup": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "DeleteGroup" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -1265,11 +1602,20 @@ "ApiUserUpdateParams": { "type": "object", "properties": { + "groups": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "uniqueItems": true + }, "permissions": { "$ref": "#/components/schemas/Permissions_for_ApiPermission" } }, "required": [ + "groups", "permissions" ] }, @@ -1285,6 +1631,14 @@ "type": "string", "format": "date-time" }, + "groups": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "uniqueItems": true + }, "id": { "type": "string", "format": "uuid" @@ -1299,6 +1653,7 @@ }, "required": [ "created_at", + "groups", "id", "permissions", "updated_at" diff --git a/rfd-api/Cargo.toml b/rfd-api/Cargo.toml index 11560b2d..26e4dbb7 100644 --- a/rfd-api/Cargo.toml +++ b/rfd-api/Cargo.toml @@ -12,6 +12,7 @@ chrono = { workspace = true, features = ["serde"] } config = { workspace = true } cookie = { workspace = true } crc32c = { workspace = true } +diesel = { workspace = true } dropshot = { workspace = true } dropshot-authorization-header = { path = "../dropshot-authorization-header" } dropshot-verified-body = { workspace = true, features = ["github"] } diff --git a/rfd-api/src/context.rs b/rfd-api/src/context.rs index d7ed2401..0b4fd6c8 100644 --- a/rfd-api/src/context.rs +++ b/rfd-api/src/context.rs @@ -259,9 +259,14 @@ impl ApiContext { let mut assigned_permissions = user.permissions.clone(); assigned_permissions.append(&mut group_permissions); + let token_permissions = permissions.expand(&user); + let combined_permissions = token_permissions.intersect(&assigned_permissions); + + tracing::info!(token = ?token_permissions, user = ?assigned_permissions, combined = ?combined_permissions, "Computed caller permissions"); + let caller = Caller { id: api_user_id, - permissions: permissions.expand(&user).intersect(&assigned_permissions), + permissions: combined_permissions, user, }; @@ -328,10 +333,13 @@ impl ApiContext { }?) } + #[instrument(skip(self), fields(user_id = ?user.id, groups = ?user.groups))] async fn get_user_group_permissions( &self, user: &ApiUser, ) -> Result, StoreError> { + tracing::debug!("Expanding groups into permissions"); + let groups = AccessGroupStore::list( &*self.storage, AccessGroupFilter { @@ -342,10 +350,14 @@ impl ApiContext { ) .await?; + tracing::debug!(?groups, "Found groups to map to permissions"); + let permissions = groups .into_iter() .fold(ApiPermissions::new(), |mut aggregate, group| { let mut expanded = group.permissions.expand(user); + + tracing::trace!(group_id = ?group.id, group_name = ?group.name, permissions = ?expanded, "Transformed group into permission set"); aggregate.append(&mut expanded); aggregate @@ -534,7 +546,7 @@ impl ApiContext { match api_user_providers.len() { 0 => { - tracing::info!("Did not find any existing users. Registering a new user."); + tracing::info!(?mapped_permissions, ?mapped_groups, "Did not find any existing users. Registering a new user."); let user = self .ensure_api_user(Uuid::new_v4(), mapped_permissions, mapped_groups) @@ -623,6 +635,7 @@ impl ApiContext { Ok((mapped_permissions, mapped_groups)) } + #[instrument(skip(self), err(Debug))] async fn ensure_api_user( &self, api_user_id: Uuid, @@ -719,6 +732,7 @@ impl ApiContext { ApiUserStore::list(&*self.storage, filter, pagination).await } + #[instrument(skip(self))] pub async fn update_api_user( &self, mut api_user: NewApiUser, @@ -1033,7 +1047,7 @@ impl ApiContext { // Mapper Operations pub async fn get_mappers(&self) -> Result, StoreError> { - Ok(MapperStore::list( + let mappers = MapperStore::list( &*self.storage, MapperFilter::default(), &ListPagination::default().limit(UNLIMITED), @@ -1041,7 +1055,11 @@ impl ApiContext { .await? .into_iter() .filter_map(|mapper| mapper.try_into().ok()) - .collect::>()) + .collect::>(); + + tracing::trace!(?mappers, "Fetched list of mappers to test"); + + Ok(mappers) } pub async fn add_mapper(&self, new_mapper: &NewMapper) -> Result { @@ -1049,6 +1067,11 @@ impl ApiContext { } async fn consume_mapping_activation(&self, mapping: &Mapping) -> Result<(), StoreError> { + // Activations are only incremented if the rule actually has a max activation value + let activations = mapping.max_activations.map(|_| { + mapping.activations.unwrap_or(0) + 1 + }); + Ok(MapperStore::upsert( &*self.storage, &NewMapper { @@ -1060,7 +1083,7 @@ impl ApiContext { // has become corrupted or something in the application has manipulated the rule. rule: serde_json::to_value(&mapping.rule) .expect("Store rules must be able to be re-serialized"), - activations: mapping.activations.map(|i| i + 1), + activations: activations, max_activations: mapping.max_activations, }, ) diff --git a/rfd-api/src/initial_data.rs b/rfd-api/src/initial_data.rs index 0842b580..4b8e02fd 100644 --- a/rfd-api/src/initial_data.rs +++ b/rfd-api/src/initial_data.rs @@ -1,7 +1,9 @@ use config::{Config, ConfigError, Environment, File}; -use rfd_model::{storage::StoreError, NewAccessGroup, NewMapper}; +use diesel::result::{Error as DieselError, DatabaseErrorKind}; +use rfd_model::{storage::{StoreError, PoolError, ConnectionError}, NewAccessGroup, NewMapper}; use serde::Deserialize; use thiserror::Error; +use tracing::Instrument; use uuid::Uuid; use crate::{context::ApiContext, mapper::MappingRules, ApiPermissions}; @@ -21,6 +23,7 @@ pub struct InitialGroup { #[derive(Debug, Deserialize)] pub struct InitialMapper { pub name: String, + #[serde(flatten)] pub rule: MappingRules, pub max_activations: Option, } @@ -38,8 +41,8 @@ pub enum InitError { impl InitialData { pub fn new() -> Result { let config = Config::builder() - .add_source(File::with_name("baseline.toml").required(false)) - .add_source(File::with_name("rfd-api/baseline.toml").required(false)) + .add_source(File::with_name("mappers.toml").required(false)) + .add_source(File::with_name("rfd-api/mappers.toml").required(false)) .add_source(Environment::default()) .build()?; @@ -48,25 +51,53 @@ impl InitialData { pub async fn initialize(self, ctx: &ApiContext) -> Result<(), InitError> { for group in self.groups { - ctx.create_group(NewAccessGroup { - id: Uuid::new_v4(), - name: group.name, - permissions: group.permissions, - }) - .await?; + let span = tracing::info_span!("Initializing group", group = ?group); + + async { + ctx.create_group(NewAccessGroup { + id: Uuid::new_v4(), + name: group.name, + permissions: group.permissions, + }) + .await + .map(|_| ()) + .or_else(handle_unique_violation_error) + }.instrument(span).await? } for mapper in self.mappers { - ctx.add_mapper(&NewMapper { - id: Uuid::new_v4(), - name: mapper.name, - rule: serde_json::to_value(&mapper.rule)?, - activations: None, - max_activations: mapper.max_activations.map(|i| i as i32), - }) - .await?; + let span = tracing::info_span!("Initializing mapper", mapper = ?mapper); + async { + let new_mapper = NewMapper { + id: Uuid::new_v4(), + name: mapper.name, + rule: serde_json::to_value(&mapper.rule)?, + activations: None, + max_activations: mapper.max_activations.map(|i| i as i32), + }; + + ctx.add_mapper(&new_mapper) + .await + .map(|_| ()) + .or_else(handle_unique_violation_error)?; + + Ok::<(), InitError>(()) + }.instrument(span).await?; } Ok(()) } } + +fn handle_unique_violation_error(err: StoreError) -> Result<(), StoreError> { + match err { + StoreError::Pool(PoolError::Connection(ConnectionError::Query(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, info)))) => { + tracing::info!(?info, "Record already exists. Skipping."); + Ok(()) + }, + err => { + tracing::error!(?err, "Failed to store record"); + Err(err) + } + } +} diff --git a/rfd-api/src/mapper/email_address.rs b/rfd-api/src/mapper/email_address.rs index ac0e026d..431ea2cc 100644 --- a/rfd-api/src/mapper/email_address.rs +++ b/rfd-api/src/mapper/email_address.rs @@ -10,14 +10,16 @@ use crate::{context::ApiContext, endpoints::login::UserInfo, ApiPermissions}; use super::MapperRule; #[derive(Debug, Deserialize, Serialize)] -pub struct EmailDomainMapper { +pub struct EmailAddressMapper { email: String, + #[serde(default)] permissions: ApiPermissions, + #[serde(default)] groups: Vec, } #[async_trait] -impl MapperRule for EmailDomainMapper { +impl MapperRule for EmailAddressMapper { async fn permissions_for( &self, _ctx: &ApiContext, diff --git a/rfd-api/src/mapper/email_domain.rs b/rfd-api/src/mapper/email_domain.rs index a91d72af..74477926 100644 --- a/rfd-api/src/mapper/email_domain.rs +++ b/rfd-api/src/mapper/email_domain.rs @@ -12,6 +12,9 @@ use super::MapperRule; #[derive(Debug, Deserialize, Serialize)] pub struct EmailDomainMapper { domain: String, + #[serde(default)] + permissions: ApiPermissions, + #[serde(default)] groups: Vec, } diff --git a/rfd-api/src/mapper/mod.rs b/rfd-api/src/mapper/mod.rs index f94f7f4a..bba33f00 100644 --- a/rfd-api/src/mapper/mod.rs +++ b/rfd-api/src/mapper/mod.rs @@ -8,7 +8,7 @@ use uuid::Uuid; use crate::{context::ApiContext, endpoints::login::UserInfo, ApiPermissions}; -use self::email_domain::EmailDomainMapper; +use self::{email_domain::EmailDomainMapper, email_address::EmailAddressMapper}; pub mod email_address; pub mod email_domain; @@ -55,7 +55,9 @@ impl TryFrom for Mapping { } #[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "rule", rename_all = "snake_case")] pub enum MappingRules { + EmailAddress(EmailAddressMapper), EmailDomain(EmailDomainMapper), } @@ -67,6 +69,7 @@ impl MapperRule for MappingRules { user: &UserInfo, ) -> Result { match self { + Self::EmailAddress(rule) => rule.permissions_for(ctx, user).await, Self::EmailDomain(rule) => rule.permissions_for(ctx, user).await, } } @@ -77,6 +80,7 @@ impl MapperRule for MappingRules { user: &UserInfo, ) -> Result, StoreError> { match self { + Self::EmailAddress(rule) => rule.groups_for(ctx, user).await, Self::EmailDomain(rule) => rule.groups_for(ctx, user).await, } } diff --git a/rfd-cli/src/generated/cli.rs b/rfd-cli/src/generated/cli.rs index 5e808d7c..efd7bccb 100644 --- a/rfd-cli/src/generated/cli.rs +++ b/rfd-cli/src/generated/cli.rs @@ -22,11 +22,17 @@ impl Cli { CliCommand::CreateApiUser => Self::cli_create_api_user(), CliCommand::GetApiUser => Self::cli_get_api_user(), CliCommand::UpdateApiUser => Self::cli_update_api_user(), + CliCommand::AddApiUserToGroup => Self::cli_add_api_user_to_group(), + CliCommand::RemoveApiUserFromGroup => Self::cli_remove_api_user_from_group(), CliCommand::ListApiUserTokens => Self::cli_list_api_user_tokens(), CliCommand::CreateApiUserToken => Self::cli_create_api_user_token(), CliCommand::GetApiUserToken => Self::cli_get_api_user_token(), CliCommand::DeleteApiUserToken => Self::cli_delete_api_user_token(), CliCommand::GithubWebhook => Self::cli_github_webhook(), + CliCommand::GetGroups => Self::cli_get_groups(), + CliCommand::CreateGroup => Self::cli_create_group(), + CliCommand::UpdateGroup => Self::cli_update_group(), + CliCommand::DeleteGroup => Self::cli_delete_group(), CliCommand::AuthzCodeRedirect => Self::cli_authz_code_redirect(), CliCommand::AuthzCodeCallback => Self::cli_authz_code_callback(), CliCommand::AuthzCodeExchange => Self::cli_authz_code_exchange(), @@ -105,6 +111,52 @@ impl Cli { .about("Update the permissions assigned to a given user") } + pub fn cli_add_api_user_to_group() -> clap::Command { + clap::Command::new("") + .arg( + clap::Arg::new("group-id") + .long("group-id") + .value_parser(clap::value_parser!(uuid::Uuid)) + .required_unless_present("json-body"), + ) + .arg( + clap::Arg::new("identifier") + .long("identifier") + .value_parser(clap::value_parser!(uuid::Uuid)) + .required(true), + ) + .arg( + clap::Arg::new("json-body") + .long("json-body") + .value_name("JSON-FILE") + .required(false) + .value_parser(clap::value_parser!(std::path::PathBuf)) + .help("Path to a file that contains the full json body."), + ) + .arg( + clap::Arg::new("json-body-template") + .long("json-body-template") + .action(clap::ArgAction::SetTrue) + .help("XXX"), + ) + } + + pub fn cli_remove_api_user_from_group() -> clap::Command { + clap::Command::new("") + .arg( + clap::Arg::new("group-id") + .long("group-id") + .value_parser(clap::value_parser!(uuid::Uuid)) + .required(true), + ) + .arg( + clap::Arg::new("identifier") + .long("identifier") + .value_parser(clap::value_parser!(uuid::Uuid)) + .required(true), + ) + } + pub fn cli_list_api_user_tokens() -> clap::Command { clap::Command::new("") .arg( @@ -202,6 +254,73 @@ impl Cli { ) } + pub fn cli_get_groups() -> clap::Command { + clap::Command::new("") + } + + pub fn cli_create_group() -> clap::Command { + clap::Command::new("") + .arg( + clap::Arg::new("name") + .long("name") + .value_parser(clap::value_parser!(String)) + .required_unless_present("json-body"), + ) + .arg( + clap::Arg::new("json-body") + .long("json-body") + .value_name("JSON-FILE") + .required(true) + .value_parser(clap::value_parser!(std::path::PathBuf)) + .help("Path to a file that contains the full json body."), + ) + .arg( + clap::Arg::new("json-body-template") + .long("json-body-template") + .action(clap::ArgAction::SetTrue) + .help("XXX"), + ) + } + + pub fn cli_update_group() -> clap::Command { + clap::Command::new("") + .arg( + clap::Arg::new("group-id") + .long("group-id") + .value_parser(clap::value_parser!(uuid::Uuid)) + .required(true), + ) + .arg( + clap::Arg::new("name") + .long("name") + .value_parser(clap::value_parser!(String)) + .required_unless_present("json-body"), + ) + .arg( + clap::Arg::new("json-body") + .long("json-body") + .value_name("JSON-FILE") + .required(true) + .value_parser(clap::value_parser!(std::path::PathBuf)) + .help("Path to a file that contains the full json body."), + ) + .arg( + clap::Arg::new("json-body-template") + .long("json-body-template") + .action(clap::ArgAction::SetTrue) + .help("XXX"), + ) + } + + pub fn cli_delete_group() -> clap::Command { + clap::Command::new("").arg( + clap::Arg::new("group-id") + .long("group-id") + .value_parser(clap::value_parser!(uuid::Uuid)) + .required(true), + ) + } + pub fn cli_authz_code_redirect() -> clap::Command { clap::Command::new("") .arg( @@ -554,6 +673,12 @@ impl Cli { CliCommand::UpdateApiUser => { self.execute_update_api_user(matches).await; } + CliCommand::AddApiUserToGroup => { + self.execute_add_api_user_to_group(matches).await; + } + CliCommand::RemoveApiUserFromGroup => { + self.execute_remove_api_user_from_group(matches).await; + } CliCommand::ListApiUserTokens => { self.execute_list_api_user_tokens(matches).await; } @@ -569,6 +694,18 @@ impl Cli { CliCommand::GithubWebhook => { self.execute_github_webhook(matches).await; } + CliCommand::GetGroups => { + self.execute_get_groups(matches).await; + } + CliCommand::CreateGroup => { + self.execute_create_group(matches).await; + } + CliCommand::UpdateGroup => { + self.execute_update_group(matches).await; + } + CliCommand::DeleteGroup => { + self.execute_delete_group(matches).await; + } CliCommand::AuthzCodeRedirect => { self.execute_authz_code_redirect(matches).await; } @@ -676,6 +813,54 @@ impl Cli { } } + pub async fn execute_add_api_user_to_group(&self, matches: &clap::ArgMatches) { + let mut request = self.client.add_api_user_to_group(); + if let Some(value) = matches.get_one::("group-id") { + request = request.body_map(|body| body.group_id(value.clone())) + } + + if let Some(value) = matches.get_one::("identifier") { + request = request.identifier(value.clone()); + } + + if let Some(value) = matches.get_one::("json-body") { + let body_txt = std::fs::read_to_string(value).unwrap(); + let body_value = serde_json::from_str::(&body_txt).unwrap(); + request = request.body(body_value); + } + + self.over + .execute_add_api_user_to_group(matches, &mut request) + .unwrap(); + let result = request.send().await; + match result { + Ok(r) => self.output.output_add_api_user_to_group(Ok(r.into_inner())), + Err(r) => self.output.output_add_api_user_to_group(Err(r)), + } + } + + pub async fn execute_remove_api_user_from_group(&self, matches: &clap::ArgMatches) { + let mut request = self.client.remove_api_user_from_group(); + if let Some(value) = matches.get_one::("group-id") { + request = request.group_id(value.clone()); + } + + if let Some(value) = matches.get_one::("identifier") { + request = request.identifier(value.clone()); + } + + self.over + .execute_remove_api_user_from_group(matches, &mut request) + .unwrap(); + let result = request.send().await; + match result { + Ok(r) => self + .output + .output_remove_api_user_from_group(Ok(r.into_inner())), + Err(r) => self.output.output_remove_api_user_from_group(Err(r)), + } + } + pub async fn execute_list_api_user_tokens(&self, matches: &clap::ArgMatches) { let mut request = self.client.list_api_user_tokens(); if let Some(value) = matches.get_one::("identifier") { @@ -781,6 +966,82 @@ impl Cli { } } + pub async fn execute_get_groups(&self, matches: &clap::ArgMatches) { + let mut request = self.client.get_groups(); + self.over.execute_get_groups(matches, &mut request).unwrap(); + let result = request.send().await; + match result { + Ok(r) => self.output.output_get_groups(Ok(r.into_inner())), + Err(r) => self.output.output_get_groups(Err(r)), + } + } + + pub async fn execute_create_group(&self, matches: &clap::ArgMatches) { + let mut request = self.client.create_group(); + if let Some(value) = matches.get_one::("name") { + request = request.body_map(|body| body.name(value.clone())) + } + + if let Some(value) = matches.get_one::("json-body") { + let body_txt = std::fs::read_to_string(value).unwrap(); + let body_value = + serde_json::from_str::(&body_txt).unwrap(); + request = request.body(body_value); + } + + self.over + .execute_create_group(matches, &mut request) + .unwrap(); + let result = request.send().await; + match result { + Ok(r) => self.output.output_create_group(Ok(r.into_inner())), + Err(r) => self.output.output_create_group(Err(r)), + } + } + + pub async fn execute_update_group(&self, matches: &clap::ArgMatches) { + let mut request = self.client.update_group(); + if let Some(value) = matches.get_one::("group-id") { + request = request.group_id(value.clone()); + } + + if let Some(value) = matches.get_one::("name") { + request = request.body_map(|body| body.name(value.clone())) + } + + if let Some(value) = matches.get_one::("json-body") { + let body_txt = std::fs::read_to_string(value).unwrap(); + let body_value = + serde_json::from_str::(&body_txt).unwrap(); + request = request.body(body_value); + } + + self.over + .execute_update_group(matches, &mut request) + .unwrap(); + let result = request.send().await; + match result { + Ok(r) => self.output.output_update_group(Ok(r.into_inner())), + Err(r) => self.output.output_update_group(Err(r)), + } + } + + pub async fn execute_delete_group(&self, matches: &clap::ArgMatches) { + let mut request = self.client.delete_group(); + if let Some(value) = matches.get_one::("group-id") { + request = request.group_id(value.clone()); + } + + self.over + .execute_delete_group(matches, &mut request) + .unwrap(); + let result = request.send().await; + match result { + Ok(r) => self.output.output_delete_group(Ok(r.into_inner())), + Err(r) => self.output.output_delete_group(Err(r)), + } + } + pub async fn execute_authz_code_redirect(&self, matches: &clap::ArgMatches) { let mut request = self.client.authz_code_redirect(); if let Some(value) = matches.get_one::("client-id") { @@ -1157,6 +1418,22 @@ pub trait CliOverride { Ok(()) } + fn execute_add_api_user_to_group( + &self, + matches: &clap::ArgMatches, + request: &mut builder::AddApiUserToGroup, + ) -> Result<(), String> { + Ok(()) + } + + fn execute_remove_api_user_from_group( + &self, + matches: &clap::ArgMatches, + request: &mut builder::RemoveApiUserFromGroup, + ) -> Result<(), String> { + Ok(()) + } + fn execute_list_api_user_tokens( &self, matches: &clap::ArgMatches, @@ -1197,6 +1474,38 @@ pub trait CliOverride { Ok(()) } + fn execute_get_groups( + &self, + matches: &clap::ArgMatches, + request: &mut builder::GetGroups, + ) -> Result<(), String> { + Ok(()) + } + + fn execute_create_group( + &self, + matches: &clap::ArgMatches, + request: &mut builder::CreateGroup, + ) -> Result<(), String> { + Ok(()) + } + + fn execute_update_group( + &self, + matches: &clap::ArgMatches, + request: &mut builder::UpdateGroup, + ) -> Result<(), String> { + Ok(()) + } + + fn execute_delete_group( + &self, + matches: &clap::ArgMatches, + request: &mut builder::DeleteGroup, + ) -> Result<(), String> { + Ok(()) + } + fn execute_authz_code_redirect( &self, matches: &clap::ArgMatches, @@ -1347,6 +1656,18 @@ pub trait CliOutput { ) { } + fn output_add_api_user_to_group( + &self, + response: Result>, + ) { + } + + fn output_remove_api_user_from_group( + &self, + response: Result>, + ) { + } + fn output_list_api_user_tokens( &self, response: Result, progenitor_client::Error>, @@ -1373,6 +1694,42 @@ pub trait CliOutput { fn output_github_webhook(&self, response: Result<(), progenitor_client::Error>) {} + fn output_get_groups( + &self, + response: Result< + Vec, + progenitor_client::Error, + >, + ) { + } + + fn output_create_group( + &self, + response: Result< + types::AccessGroupForApiPermission, + progenitor_client::Error, + >, + ) { + } + + fn output_update_group( + &self, + response: Result< + types::AccessGroupForApiPermission, + progenitor_client::Error, + >, + ) { + } + + fn output_delete_group( + &self, + response: Result< + types::AccessGroupForApiPermission, + progenitor_client::Error, + >, + ) { + } + fn output_authz_code_redirect(&self, response: Result<(), progenitor_client::Error<()>>) {} fn output_authz_code_callback( @@ -1430,7 +1787,10 @@ pub trait CliOutput { fn output_create_oauth_client_secret( &self, - response: Result>, + response: Result< + types::InitialOAuthClientSecretResponse, + progenitor_client::Error, + >, ) { } @@ -1472,11 +1832,17 @@ pub enum CliCommand { CreateApiUser, GetApiUser, UpdateApiUser, + AddApiUserToGroup, + RemoveApiUserFromGroup, ListApiUserTokens, CreateApiUserToken, GetApiUserToken, DeleteApiUserToken, GithubWebhook, + GetGroups, + CreateGroup, + UpdateGroup, + DeleteGroup, AuthzCodeRedirect, AuthzCodeCallback, AuthzCodeExchange, @@ -1501,11 +1867,17 @@ impl CliCommand { CliCommand::CreateApiUser, CliCommand::GetApiUser, CliCommand::UpdateApiUser, + CliCommand::AddApiUserToGroup, + CliCommand::RemoveApiUserFromGroup, CliCommand::ListApiUserTokens, CliCommand::CreateApiUserToken, CliCommand::GetApiUserToken, CliCommand::DeleteApiUserToken, CliCommand::GithubWebhook, + CliCommand::GetGroups, + CliCommand::CreateGroup, + CliCommand::UpdateGroup, + CliCommand::DeleteGroup, CliCommand::AuthzCodeRedirect, CliCommand::AuthzCodeCallback, CliCommand::AuthzCodeExchange, diff --git a/rfd-cli/src/main.rs b/rfd-cli/src/main.rs index 3d1ad76a..c3b1c128 100644 --- a/rfd-cli/src/main.rs +++ b/rfd-cli/src/main.rs @@ -93,6 +93,7 @@ impl<'a> Tree<'a> { fn cmd_path<'a>(cmd: &CliCommand) -> Option<&'a str> { match cmd { + // User commands CliCommand::CreateApiUser => Some("user create"), CliCommand::CreateApiUserToken => Some("user token create"), CliCommand::DeleteApiUserToken => Some("user token delete"), @@ -105,6 +106,16 @@ fn cmd_path<'a>(cmd: &CliCommand) -> Option<&'a str> { CliCommand::ListApiUserTokens => Some("user token list"), CliCommand::UpdateApiUser => Some("user update"), + // Group commands + CliCommand::GetGroups => Some("group get"), + CliCommand::CreateGroup => Some("group create"), + CliCommand::UpdateGroup => Some("group update"), + CliCommand::DeleteGroup => Some("group delete"), + + // User admin commands + CliCommand::AddApiUserToGroup => Some("group membership add"), + CliCommand::RemoveApiUserFromGroup => Some("group membership remove"), + // OAuth client commands CliCommand::ListOauthClients => None, CliCommand::CreateOauthClient => None, diff --git a/rfd-model/migrations/2023-09-28-151800_access_group/up.sql b/rfd-model/migrations/2023-09-28-151800_access_group/up.sql index 8915cbc2..bc9940a7 100644 --- a/rfd-model/migrations/2023-09-28-151800_access_group/up.sql +++ b/rfd-model/migrations/2023-09-28-151800_access_group/up.sql @@ -1,6 +1,6 @@ CREATE TABLE access_groups ( id UUID PRIMARY KEY, - name VARCHAR NOT NULL, + name VARCHAR UNIQUE NOT NULL, permissions JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), diff --git a/rfd-model/src/storage/postgres.rs b/rfd-model/src/storage/postgres.rs index 1a5a4c28..28e6b6f8 100644 --- a/rfd-model/src/storage/postgres.rs +++ b/rfd-model/src/storage/postgres.rs @@ -14,6 +14,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, time::Duration, }; +use tracing::instrument; use uuid::Uuid; use crate::{ @@ -547,8 +548,9 @@ where .collect()) } + #[instrument(skip(self), fields(id = ?user.id, permissions = ?user.permissions, groups = ?user.groups))] async fn upsert(&self, user: NewApiUser) -> Result, StoreError> { - tracing::info!(id = ?user.id, permissions = ?user.permissions, "Upserting user"); + tracing::trace!("Upserting user"); let user_m: ApiUserModel = insert_into(api_user::dsl::api_user) .values(( @@ -1314,12 +1316,16 @@ where #[async_trait] impl MapperStore for PostgresStore { + + #[instrument(skip(self), err(Debug))] async fn get( &self, id: &Uuid, depleted: bool, deleted: bool, ) -> Result, StoreError> { + tracing::trace!("Get mapper"); + let client = MapperStore::list( self, MapperFilter { @@ -1335,11 +1341,14 @@ impl MapperStore for PostgresStore { Ok(client.into_iter().nth(0)) } + #[instrument(skip(self), err(Debug))] async fn list( &self, filter: MapperFilter, pagination: &ListPagination, ) -> Result, StoreError> { + tracing::trace!("Listing mappers"); + let mut query = mapper::dsl::mapper.into_boxed(); let MapperFilter { @@ -1374,7 +1383,13 @@ impl MapperStore for PostgresStore { Ok(results.into_iter().map(|model| model.into()).collect()) } + + #[instrument(skip(self), err(Debug))] async fn upsert(&self, new_mapper: &NewMapper) -> Result { + tracing::trace!("Upserting mapper"); + + let depleted = new_mapper.max_activations.map(|max| new_mapper.activations.unwrap_or(0) == max).unwrap_or(false); + let mapper_m: MapperModel = insert_into(mapper::dsl::mapper) .values(( mapper::id.eq(new_mapper.id), @@ -1382,11 +1397,7 @@ impl MapperStore for PostgresStore { mapper::rule.eq(new_mapper.rule.clone()), mapper::activations.eq(new_mapper.activations), mapper::max_activations.eq(new_mapper.max_activations), - mapper::depleted_at.eq(if new_mapper.activations == new_mapper.max_activations { - Some(Utc::now()) - } else { - None - }), + mapper::depleted_at.eq(if depleted { Some(Utc::now()) } else { None }), )) .on_conflict(mapper::id) .do_update() @@ -1399,7 +1410,11 @@ impl MapperStore for PostgresStore { Ok(mapper_m.into()) } + + #[instrument(skip(self), err(Debug))] async fn delete(&self, id: &Uuid) -> Result, StoreError> { + tracing::trace!("Deleting mapper"); + let _ = update(mapper::dsl::mapper) .filter(mapper::id.eq(*id)) .set(mapper::deleted_at.eq(Utc::now())) diff --git a/rfd-sdk/src/generated/sdk.rs b/rfd-sdk/src/generated/sdk.rs index af5b8764..1c1a0adf 100644 --- a/rfd-sdk/src/generated/sdk.rs +++ b/rfd-sdk/src/generated/sdk.rs @@ -9,6 +9,47 @@ pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] use std::convert::TryFrom; + #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] + pub struct AccessGroupForApiPermission { + pub created_at: chrono::DateTime, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deleted_at: Option>, + pub id: uuid::Uuid, + pub name: String, + pub permissions: PermissionsForApiPermission, + pub updated_at: chrono::DateTime, + } + + impl From<&AccessGroupForApiPermission> for AccessGroupForApiPermission { + fn from(value: &AccessGroupForApiPermission) -> Self { + value.clone() + } + } + + impl AccessGroupForApiPermission { + pub fn builder() -> builder::AccessGroupForApiPermission { + builder::AccessGroupForApiPermission::default() + } + } + + #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] + pub struct AccessGroupUpdateParams { + pub name: String, + pub permissions: PermissionsForApiPermission, + } + + impl From<&AccessGroupUpdateParams> for AccessGroupUpdateParams { + fn from(value: &AccessGroupUpdateParams) -> Self { + value.clone() + } + } + + impl AccessGroupUpdateParams { + pub fn builder() -> builder::AccessGroupUpdateParams { + builder::AccessGroupUpdateParams::default() + } + } + #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] pub struct AccessTokenExchangeRequest { pub device_code: String, @@ -29,6 +70,23 @@ pub mod types { } } + #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] + pub struct AddGroupBody { + pub group_id: uuid::Uuid, + } + + impl From<&AddGroupBody> for AddGroupBody { + fn from(value: &AddGroupBody) -> Self { + value.clone() + } + } + + impl AddGroupBody { + pub fn builder() -> builder::AddGroupBody { + builder::AddGroupBody::default() + } + } + #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] pub struct AddOAuthClientRedirectBody { pub redirect_uri: String, @@ -95,6 +153,8 @@ pub mod types { CreateApiUser, UpdateApiUserSelf, UpdateApiUserAll, + ListGroups, + CreateGroup, GetAllRfds, GetAssignedRfds, GetAllDiscussions, @@ -109,6 +169,10 @@ pub mod types { GetApiUserToken(uuid::Uuid), DeleteApiUserToken(uuid::Uuid), UpdateApiUser(uuid::Uuid), + UpdateGroup(uuid::Uuid), + AddToGroup(uuid::Uuid), + RemoveFromGroup(uuid::Uuid), + DeleteGroup(uuid::Uuid), GetRfd(i32), GetRfds(Vec), GetDiscussion(i32), @@ -132,6 +196,7 @@ pub mod types { pub created_at: chrono::DateTime, #[serde(default, skip_serializing_if = "Option::is_none")] pub deleted_at: Option>, + pub groups: Vec, pub id: uuid::Uuid, pub permissions: PermissionsForApiPermission, pub updated_at: chrono::DateTime, @@ -151,6 +216,7 @@ pub mod types { #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] pub struct ApiUserUpdateParams { + pub groups: Vec, pub permissions: PermissionsForApiPermission, } @@ -377,6 +443,25 @@ pub mod types { } } + #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] + pub struct InitialOAuthClientSecretResponse { + pub created_at: chrono::DateTime, + pub id: uuid::Uuid, + pub key: String, + } + + impl From<&InitialOAuthClientSecretResponse> for InitialOAuthClientSecretResponse { + fn from(value: &InitialOAuthClientSecretResponse) -> Self { + value.clone() + } + } + + impl InitialOAuthClientSecretResponse { + pub fn builder() -> builder::InitialOAuthClientSecretResponse { + builder::InitialOAuthClientSecretResponse::default() + } + } + #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] pub struct ListRfd { #[serde(default, skip_serializing_if = "Option::is_none")] @@ -632,6 +717,176 @@ pub mod types { } pub mod builder { + #[derive(Clone, Debug)] + pub struct AccessGroupForApiPermission { + created_at: Result, String>, + deleted_at: Result>, String>, + id: Result, + name: Result, + permissions: Result, + updated_at: Result, String>, + } + + impl Default for AccessGroupForApiPermission { + fn default() -> Self { + Self { + created_at: Err("no value supplied for created_at".to_string()), + deleted_at: Ok(Default::default()), + id: Err("no value supplied for id".to_string()), + name: Err("no value supplied for name".to_string()), + permissions: Err("no value supplied for permissions".to_string()), + updated_at: Err("no value supplied for updated_at".to_string()), + } + } + } + + impl AccessGroupForApiPermission { + pub fn created_at(mut self, value: T) -> Self + where + T: std::convert::TryInto>, + T::Error: std::fmt::Display, + { + self.created_at = value + .try_into() + .map_err(|e| format!("error converting supplied value for created_at: {}", e)); + self + } + pub fn deleted_at(mut self, value: T) -> Self + where + T: std::convert::TryInto>>, + T::Error: std::fmt::Display, + { + self.deleted_at = value + .try_into() + .map_err(|e| format!("error converting supplied value for deleted_at: {}", e)); + self + } + pub fn id(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.id = value + .try_into() + .map_err(|e| format!("error converting supplied value for id: {}", e)); + self + } + pub fn name(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {}", e)); + self + } + pub fn permissions(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.permissions = value + .try_into() + .map_err(|e| format!("error converting supplied value for permissions: {}", e)); + self + } + pub fn updated_at(mut self, value: T) -> Self + where + T: std::convert::TryInto>, + T::Error: std::fmt::Display, + { + self.updated_at = value + .try_into() + .map_err(|e| format!("error converting supplied value for updated_at: {}", e)); + self + } + } + + impl std::convert::TryFrom for super::AccessGroupForApiPermission { + type Error = String; + fn try_from(value: AccessGroupForApiPermission) -> Result { + Ok(Self { + created_at: value.created_at?, + deleted_at: value.deleted_at?, + id: value.id?, + name: value.name?, + permissions: value.permissions?, + updated_at: value.updated_at?, + }) + } + } + + impl From for AccessGroupForApiPermission { + fn from(value: super::AccessGroupForApiPermission) -> Self { + Self { + created_at: Ok(value.created_at), + deleted_at: Ok(value.deleted_at), + id: Ok(value.id), + name: Ok(value.name), + permissions: Ok(value.permissions), + updated_at: Ok(value.updated_at), + } + } + } + + #[derive(Clone, Debug)] + pub struct AccessGroupUpdateParams { + name: Result, + permissions: Result, + } + + impl Default for AccessGroupUpdateParams { + fn default() -> Self { + Self { + name: Err("no value supplied for name".to_string()), + permissions: Err("no value supplied for permissions".to_string()), + } + } + } + + impl AccessGroupUpdateParams { + pub fn name(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {}", e)); + self + } + pub fn permissions(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.permissions = value + .try_into() + .map_err(|e| format!("error converting supplied value for permissions: {}", e)); + self + } + } + + impl std::convert::TryFrom for super::AccessGroupUpdateParams { + type Error = String; + fn try_from(value: AccessGroupUpdateParams) -> Result { + Ok(Self { + name: value.name?, + permissions: value.permissions?, + }) + } + } + + impl From for AccessGroupUpdateParams { + fn from(value: super::AccessGroupUpdateParams) -> Self { + Self { + name: Ok(value.name), + permissions: Ok(value.permissions), + } + } + } + #[derive(Clone, Debug)] pub struct AccessTokenExchangeRequest { device_code: Result, @@ -703,6 +958,49 @@ pub mod types { } } + #[derive(Clone, Debug)] + pub struct AddGroupBody { + group_id: Result, + } + + impl Default for AddGroupBody { + fn default() -> Self { + Self { + group_id: Err("no value supplied for group_id".to_string()), + } + } + } + + impl AddGroupBody { + pub fn group_id(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.group_id = value + .try_into() + .map_err(|e| format!("error converting supplied value for group_id: {}", e)); + self + } + } + + impl std::convert::TryFrom for super::AddGroupBody { + type Error = String; + fn try_from(value: AddGroupBody) -> Result { + Ok(Self { + group_id: value.group_id?, + }) + } + } + + impl From for AddGroupBody { + fn from(value: super::AddGroupBody) -> Self { + Self { + group_id: Ok(value.group_id), + } + } + } + #[derive(Clone, Debug)] pub struct AddOAuthClientRedirectBody { redirect_uri: Result, @@ -878,6 +1176,7 @@ pub mod types { pub struct ApiUserForApiPermission { created_at: Result, String>, deleted_at: Result>, String>, + groups: Result, String>, id: Result, permissions: Result, updated_at: Result, String>, @@ -888,6 +1187,7 @@ pub mod types { Self { created_at: Err("no value supplied for created_at".to_string()), deleted_at: Ok(Default::default()), + groups: Err("no value supplied for groups".to_string()), id: Err("no value supplied for id".to_string()), permissions: Err("no value supplied for permissions".to_string()), updated_at: Err("no value supplied for updated_at".to_string()), @@ -916,6 +1216,16 @@ pub mod types { .map_err(|e| format!("error converting supplied value for deleted_at: {}", e)); self } + pub fn groups(mut self, value: T) -> Self + where + T: std::convert::TryInto>, + T::Error: std::fmt::Display, + { + self.groups = value + .try_into() + .map_err(|e| format!("error converting supplied value for groups: {}", e)); + self + } pub fn id(mut self, value: T) -> Self where T: std::convert::TryInto, @@ -954,6 +1264,7 @@ pub mod types { Ok(Self { created_at: value.created_at?, deleted_at: value.deleted_at?, + groups: value.groups?, id: value.id?, permissions: value.permissions?, updated_at: value.updated_at?, @@ -966,6 +1277,7 @@ pub mod types { Self { created_at: Ok(value.created_at), deleted_at: Ok(value.deleted_at), + groups: Ok(value.groups), id: Ok(value.id), permissions: Ok(value.permissions), updated_at: Ok(value.updated_at), @@ -975,18 +1287,30 @@ pub mod types { #[derive(Clone, Debug)] pub struct ApiUserUpdateParams { + groups: Result, String>, permissions: Result, } impl Default for ApiUserUpdateParams { fn default() -> Self { Self { + groups: Err("no value supplied for groups".to_string()), permissions: Err("no value supplied for permissions".to_string()), } } } impl ApiUserUpdateParams { + pub fn groups(mut self, value: T) -> Self + where + T: std::convert::TryInto>, + T::Error: std::fmt::Display, + { + self.groups = value + .try_into() + .map_err(|e| format!("error converting supplied value for groups: {}", e)); + self + } pub fn permissions(mut self, value: T) -> Self where T: std::convert::TryInto, @@ -1003,6 +1327,7 @@ pub mod types { type Error = String; fn try_from(value: ApiUserUpdateParams) -> Result { Ok(Self { + groups: value.groups?, permissions: value.permissions?, }) } @@ -1011,6 +1336,7 @@ pub mod types { impl From for ApiUserUpdateParams { fn from(value: super::ApiUserUpdateParams) -> Self { Self { + groups: Ok(value.groups), permissions: Ok(value.permissions), } } @@ -1923,13 +2249,86 @@ pub mod types { } #[derive(Clone, Debug)] - pub struct ListRfd { - authors: Result, String>, - commit: Result, - committed_at: Result, String>, - discussion: Result, String>, + pub struct InitialOAuthClientSecretResponse { + created_at: Result, String>, id: Result, - link: Result, String>, + key: Result, + } + + impl Default for InitialOAuthClientSecretResponse { + fn default() -> Self { + Self { + created_at: Err("no value supplied for created_at".to_string()), + id: Err("no value supplied for id".to_string()), + key: Err("no value supplied for key".to_string()), + } + } + } + + impl InitialOAuthClientSecretResponse { + pub fn created_at(mut self, value: T) -> Self + where + T: std::convert::TryInto>, + T::Error: std::fmt::Display, + { + self.created_at = value + .try_into() + .map_err(|e| format!("error converting supplied value for created_at: {}", e)); + self + } + pub fn id(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.id = value + .try_into() + .map_err(|e| format!("error converting supplied value for id: {}", e)); + self + } + pub fn key(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.key = value + .try_into() + .map_err(|e| format!("error converting supplied value for key: {}", e)); + self + } + } + + impl std::convert::TryFrom + for super::InitialOAuthClientSecretResponse + { + type Error = String; + fn try_from(value: InitialOAuthClientSecretResponse) -> Result { + Ok(Self { + created_at: value.created_at?, + id: value.id?, + key: value.key?, + }) + } + } + + impl From for InitialOAuthClientSecretResponse { + fn from(value: super::InitialOAuthClientSecretResponse) -> Self { + Self { + created_at: Ok(value.created_at), + id: Ok(value.id), + key: Ok(value.key), + } + } + } + + #[derive(Clone, Debug)] + pub struct ListRfd { + authors: Result, String>, + commit: Result, + committed_at: Result, String>, + discussion: Result, String>, + id: Result, + link: Result, String>, rfd_number: Result, sha: Result, state: Result, String>, @@ -2806,6 +3205,32 @@ impl Client { builder::UpdateApiUser::new(self) } + /// Sends a `POST` request to `/api-user/{identifier}/group` + /// + /// ```ignore + /// let response = client.add_api_user_to_group() + /// .identifier(identifier) + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn add_api_user_to_group(&self) -> builder::AddApiUserToGroup { + builder::AddApiUserToGroup::new(self) + } + + /// Sends a `DELETE` request to `/api-user/{identifier}/group/{group_id}` + /// + /// ```ignore + /// let response = client.remove_api_user_from_group() + /// .identifier(identifier) + /// .group_id(group_id) + /// .send() + /// .await; + /// ``` + pub fn remove_api_user_from_group(&self) -> builder::RemoveApiUserFromGroup { + builder::RemoveApiUserFromGroup::new(self) + } + /// List the active and expired API tokens for a given user /// /// Sends a `GET` request to `/api-user/{identifier}/token` @@ -2861,6 +3286,54 @@ impl Client { builder::DeleteApiUserToken::new(self) } + /// Sends a `GET` request to `/group` + /// + /// ```ignore + /// let response = client.get_groups() + /// .send() + /// .await; + /// ``` + pub fn get_groups(&self) -> builder::GetGroups { + builder::GetGroups::new(self) + } + + /// Sends a `POST` request to `/group` + /// + /// ```ignore + /// let response = client.create_group() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn create_group(&self) -> builder::CreateGroup { + builder::CreateGroup::new(self) + } + + /// Sends a `PUT` request to `/group/{group_id}` + /// + /// ```ignore + /// let response = client.update_group() + /// .group_id(group_id) + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn update_group(&self) -> builder::UpdateGroup { + builder::UpdateGroup::new(self) + } + + /// Sends a `DELETE` request to `/group/{group_id}` + /// + /// ```ignore + /// let response = client.delete_group() + /// .group_id(group_id) + /// .send() + /// .await; + /// ``` + pub fn delete_group(&self) -> builder::DeleteGroup { + builder::DeleteGroup::new(self) + } + /// Generate the remote provider login url and redirect the user /// /// Sends a `GET` request to `/login/oauth/{provider}/code/authorize` @@ -3340,6 +3813,176 @@ pub mod builder { } } + /// Builder for [`Client::add_api_user_to_group`] + /// + /// [`Client::add_api_user_to_group`]: super::Client::add_api_user_to_group + #[derive(Debug, Clone)] + pub struct AddApiUserToGroup<'a> { + client: &'a super::Client, + identifier: Result, + body: Result, + } + + impl<'a> AddApiUserToGroup<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + identifier: Err("identifier was not initialized".to_string()), + body: Ok(types::builder::AddGroupBody::default()), + } + } + + pub fn identifier(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.identifier = value + .try_into() + .map_err(|_| "conversion to `uuid :: Uuid` for identifier failed".to_string()); + self + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.body = value + .try_into() + .map(From::from) + .map_err(|_| "conversion to `AddGroupBody` for body failed".to_string()); + self + } + + pub fn body_map(mut self, f: F) -> Self + where + F: std::ops::FnOnce(types::builder::AddGroupBody) -> types::builder::AddGroupBody, + { + self.body = self.body.map(f); + self + } + + /// Sends a `POST` request to `/api-user/{identifier}/group` + pub async fn send( + self, + ) -> Result, Error> { + let Self { + client, + identifier, + body, + } = self; + let identifier = identifier.map_err(Error::InvalidRequest)?; + let body = body + .and_then(std::convert::TryInto::::try_into) + .map_err(Error::InvalidRequest)?; + let url = format!( + "{}/api-user/{}/group", + client.baseurl, + encode_path(&identifier.to_string()), + ); + let request = client + .client + .post(url) + .header( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ) + .json(&body) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => ResponseValue::from_response(response).await, + 400u16..=499u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + 500u16..=599u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + /// Builder for [`Client::remove_api_user_from_group`] + /// + /// [`Client::remove_api_user_from_group`]: super::Client::remove_api_user_from_group + #[derive(Debug, Clone)] + pub struct RemoveApiUserFromGroup<'a> { + client: &'a super::Client, + identifier: Result, + group_id: Result, + } + + impl<'a> RemoveApiUserFromGroup<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + identifier: Err("identifier was not initialized".to_string()), + group_id: Err("group_id was not initialized".to_string()), + } + } + + pub fn identifier(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.identifier = value + .try_into() + .map_err(|_| "conversion to `uuid :: Uuid` for identifier failed".to_string()); + self + } + + pub fn group_id(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.group_id = value + .try_into() + .map_err(|_| "conversion to `uuid :: Uuid` for group_id failed".to_string()); + self + } + + /// Sends a `DELETE` request to + /// `/api-user/{identifier}/group/{group_id}` + pub async fn send( + self, + ) -> Result, Error> { + let Self { + client, + identifier, + group_id, + } = self; + let identifier = identifier.map_err(Error::InvalidRequest)?; + let group_id = group_id.map_err(Error::InvalidRequest)?; + let url = format!( + "{}/api-user/{}/group/{}", + client.baseurl, + encode_path(&identifier.to_string()), + encode_path(&group_id.to_string()), + ); + let request = client + .client + .delete(url) + .header( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => ResponseValue::from_response(response).await, + 400u16..=499u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + 500u16..=599u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + /// Builder for [`Client::list_api_user_tokens`] /// /// [`Client::list_api_user_tokens`]: super::Client::list_api_user_tokens @@ -3722,6 +4365,276 @@ pub mod builder { } } + /// Builder for [`Client::get_groups`] + /// + /// [`Client::get_groups`]: super::Client::get_groups + #[derive(Debug, Clone)] + pub struct GetGroups<'a> { + client: &'a super::Client, + } + + impl<'a> GetGroups<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { client } + } + + /// Sends a `GET` request to `/group` + pub async fn send( + self, + ) -> Result>, Error> + { + let Self { client } = self; + let url = format!("{}/group", client.baseurl,); + let request = client + .client + .get(url) + .header( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => ResponseValue::from_response(response).await, + 400u16..=499u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + 500u16..=599u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + /// Builder for [`Client::create_group`] + /// + /// [`Client::create_group`]: super::Client::create_group + #[derive(Debug, Clone)] + pub struct CreateGroup<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> CreateGroup<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + body: Ok(types::builder::AccessGroupUpdateParams::default()), + } + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.body = value + .try_into() + .map(From::from) + .map_err(|_| "conversion to `AccessGroupUpdateParams` for body failed".to_string()); + self + } + + pub fn body_map(mut self, f: F) -> Self + where + F: std::ops::FnOnce( + types::builder::AccessGroupUpdateParams, + ) -> types::builder::AccessGroupUpdateParams, + { + self.body = self.body.map(f); + self + } + + /// Sends a `POST` request to `/group` + pub async fn send( + self, + ) -> Result, Error> + { + let Self { client, body } = self; + let body = body + .and_then(std::convert::TryInto::::try_into) + .map_err(Error::InvalidRequest)?; + let url = format!("{}/group", client.baseurl,); + let request = client + .client + .post(url) + .header( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ) + .json(&body) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => ResponseValue::from_response(response).await, + 400u16..=499u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + 500u16..=599u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + /// Builder for [`Client::update_group`] + /// + /// [`Client::update_group`]: super::Client::update_group + #[derive(Debug, Clone)] + pub struct UpdateGroup<'a> { + client: &'a super::Client, + group_id: Result, + body: Result, + } + + impl<'a> UpdateGroup<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + group_id: Err("group_id was not initialized".to_string()), + body: Ok(types::builder::AccessGroupUpdateParams::default()), + } + } + + pub fn group_id(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.group_id = value + .try_into() + .map_err(|_| "conversion to `uuid :: Uuid` for group_id failed".to_string()); + self + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.body = value + .try_into() + .map(From::from) + .map_err(|_| "conversion to `AccessGroupUpdateParams` for body failed".to_string()); + self + } + + pub fn body_map(mut self, f: F) -> Self + where + F: std::ops::FnOnce( + types::builder::AccessGroupUpdateParams, + ) -> types::builder::AccessGroupUpdateParams, + { + self.body = self.body.map(f); + self + } + + /// Sends a `PUT` request to `/group/{group_id}` + pub async fn send( + self, + ) -> Result, Error> + { + let Self { + client, + group_id, + body, + } = self; + let group_id = group_id.map_err(Error::InvalidRequest)?; + let body = body + .and_then(std::convert::TryInto::::try_into) + .map_err(Error::InvalidRequest)?; + let url = format!( + "{}/group/{}", + client.baseurl, + encode_path(&group_id.to_string()), + ); + let request = client + .client + .put(url) + .header( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ) + .json(&body) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => ResponseValue::from_response(response).await, + 400u16..=499u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + 500u16..=599u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + /// Builder for [`Client::delete_group`] + /// + /// [`Client::delete_group`]: super::Client::delete_group + #[derive(Debug, Clone)] + pub struct DeleteGroup<'a> { + client: &'a super::Client, + group_id: Result, + } + + impl<'a> DeleteGroup<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + group_id: Err("group_id was not initialized".to_string()), + } + } + + pub fn group_id(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.group_id = value + .try_into() + .map_err(|_| "conversion to `uuid :: Uuid` for group_id failed".to_string()); + self + } + + /// Sends a `DELETE` request to `/group/{group_id}` + pub async fn send( + self, + ) -> Result, Error> + { + let Self { client, group_id } = self; + let group_id = group_id.map_err(Error::InvalidRequest)?; + let url = format!( + "{}/group/{}", + client.baseurl, + encode_path(&group_id.to_string()), + ); + let request = client + .client + .delete(url) + .header( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => ResponseValue::from_response(response).await, + 400u16..=499u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + 500u16..=599u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + /// Builder for [`Client::authz_code_redirect`] /// /// [`Client::authz_code_redirect`]: super::Client::authz_code_redirect @@ -4512,7 +5425,8 @@ pub mod builder { /// Sends a `POST` request to `/oauth/client/{client_id}/secret` pub async fn send( self, - ) -> Result, Error> { + ) -> Result, Error> + { let Self { client, client_id } = self; let client_id = client_id.map_err(Error::InvalidRequest)?; let url = format!(