diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index 6215447d5c..d8f4129754 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -1133,11 +1133,20 @@ pub mod shared_args { } #[derive(Debug, Args)] - pub struct AccountIdOptionalArg { + pub struct AccountScopeOptionalArgs { + /// Account email + #[arg(long, conflicts_with = "account_id")] + pub account: Option, /// Account ID - #[arg(long)] + #[arg(long, conflicts_with = "account")] pub account_id: Option, } + + impl AccountScopeOptionalArgs { + pub fn is_explicit(&self) -> bool { + self.account.is_some() || self.account_id.is_some() + } + } } pub mod exec { @@ -1338,64 +1347,6 @@ pub mod component { component_name: OptionalComponentNames, }, } - - pub mod plugin { - use crate::args::parse_key_val; - use crate::command::shared_args::OptionalComponentName; - use clap::Subcommand; - - #[derive(Debug, Subcommand)] - pub enum ComponentPluginSubcommand { - /// Install a plugin for selected component - Install { - #[command(flatten)] - component_name: OptionalComponentName, - /// The plugin to install - #[arg(long)] - plugin_name: String, - /// The version of the plugin to install - #[arg(long)] - plugin_version: String, - /// Priority of the plugin - largest priority is applied first - #[arg(long)] - priority: i32, - /// List of parameters (key-value pairs) passed to the plugin - #[arg(long, value_parser = parse_key_val, value_name = "KEY=VAL")] - param: Vec<(String, String)>, - }, - /// Get the installed plugins of the component - Get { - #[command(flatten)] - component_name: OptionalComponentName, - /// The revision of the component - revision: Option, - }, - /// Update component plugin - Update { - /// The component to update the plugin for - #[command(flatten)] - component_name: OptionalComponentName, - /// Priority of the plugin to update - #[arg(long)] - plugin_to_update: i32, - /// Updated priority of the plugin - largest priority is applied first - #[arg(long)] - priority: i32, - /// Updated list of parameters (key-value pairs) passed to the plugin - #[arg(long, value_parser = parse_key_val, value_name = "KEY=VAL")] - param: Vec<(String, String)>, - }, - /// Uninstall a plugin for selected component - Uninstall { - /// The component to uninstall the plugin from - #[command(flatten)] - component_name: OptionalComponentName, - /// Priority of the plugin to update - #[arg(long)] - plugin_to_update: i32, - }, - } - } } pub mod worker { @@ -2275,6 +2226,7 @@ pub mod retry_policy { } pub mod plugin { + use crate::command::shared_args::AccountScopeOptionalArgs; use crate::model::input::PathBufOrStdin; use clap::Subcommand; use uuid::Uuid; @@ -2283,16 +2235,38 @@ pub mod plugin { pub enum PluginSubcommand { /// List account plugins #[command(after_help = crate::command_examples::PLUGIN_LIST)] - List, + List { + #[command(flatten)] + account: AccountScopeOptionalArgs, + }, /// Get plugin details #[command(after_help = crate::command_examples::PLUGIN_GET)] Get { - /// Plugin ID - plugin_id: Uuid, // TODO: atomic: missing method for looking up by name + /// Plugin name. Must be used together with VERSION. + #[arg( + value_name = "NAME", + required_unless_present = "id", + conflicts_with = "id" + )] + name: Option, + /// Plugin version. Must be used together with NAME. + #[arg( + value_name = "VERSION", + required_unless_present = "id", + conflicts_with = "id" + )] + version: Option, + /// Plugin ID. Conflicts with NAME, VERSION, and account scope. + #[arg(long, required_unless_present_all = ["name", "version"], conflicts_with_all = ["name", "version", "account", "account_id"])] + id: Option, + #[command(flatten)] + account: AccountScopeOptionalArgs, }, /// Register a new plugin for the account #[command(after_help = crate::command_examples::PLUGIN_REGISTER)] Register { + #[command(flatten)] + account: AccountScopeOptionalArgs, #[arg( help = crate::command_glossary::PLUGIN_MANIFEST_SHORT, long_help = crate::command_glossary::PLUGIN_MANIFEST_LONG, @@ -2303,8 +2277,25 @@ pub mod plugin { /// Unregister a plugin #[command(after_help = crate::command_examples::PLUGIN_UNREGISTER)] Unregister { - /// Plugin ID - plugin_id: Uuid, // TODO: atomic: missing method for deleting by name + /// Plugin name. Must be used together with VERSION. + #[arg( + value_name = "NAME", + required_unless_present = "id", + conflicts_with = "id" + )] + name: Option, + /// Plugin version. Must be used together with NAME. + #[arg( + value_name = "VERSION", + required_unless_present = "id", + conflicts_with = "id" + )] + version: Option, + /// Plugin ID. Conflicts with NAME, VERSION, and account scope. + #[arg(long, required_unless_present_all = ["name", "version"], conflicts_with_all = ["name", "version", "account", "account_id"])] + id: Option, + #[command(flatten)] + account: AccountScopeOptionalArgs, }, } } @@ -2440,7 +2431,7 @@ pub mod api_token { } pub mod account { - use crate::command::shared_args::AccountIdOptionalArg; + use crate::command::shared_args::AccountScopeOptionalArgs; use clap::{Args, Subcommand}; use golem_common::model::account_usage::{ AccountUsagePeriod, DEFAULT_ACCOUNT_USAGE_HISTORY_PERIODS, @@ -2450,18 +2441,20 @@ pub mod account { #[derive(Debug, Subcommand)] pub enum AccountUsageSubcommand { /// Show account usage for the current or selected UTC billing period. + #[command(after_help = crate::command_examples::ACCOUNT_USAGE_SHOW)] Show { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// Billing period in YYYY-MM format. #[arg(long)] period: Option, }, /// Show sparse account usage for closed UTC billing periods, newest first. + #[command(after_help = crate::command_examples::ACCOUNT_USAGE_HISTORY)] History { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// Number of closed periods to show. #[arg(long, default_value_t = DEFAULT_ACCOUNT_USAGE_HISTORY_PERIODS)] @@ -2472,14 +2465,16 @@ pub mod account { #[derive(Debug, Subcommand)] pub enum AccountLimitsSubcommand { /// Show effective storage and memory limits. + #[command(after_help = crate::command_examples::ACCOUNT_LIMITS_SHOW)] Show { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, }, /// Set one storage or memory limit. + #[command(after_help = crate::command_examples::ACCOUNT_LIMITS_SET)] Set { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// Maximum storage per agent in bytes. Cannot exceed the plan ceiling. #[arg( @@ -2502,9 +2497,10 @@ pub mod account { monthly_memory_gb_seconds: Option, }, /// Clear selected overrides. With no flags, clears storage for compatibility. + #[command(after_help = crate::command_examples::ACCOUNT_LIMITS_UNSET)] Unset { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// Clear the maximum storage per-agent override. #[arg( @@ -2540,7 +2536,7 @@ pub mod account { #[command(after_help = crate::command_examples::ACCOUNT_PERMISSION_SHARE_LIST)] List { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// List permission shares targeting the account instead of owned by the account. #[arg(long)] @@ -2556,7 +2552,7 @@ pub mod account { #[command(after_help = crate::command_examples::ACCOUNT_PERMISSION_SHARE_GET_BY_NAME)] GetByName { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// Permission share name. name: String, @@ -2565,7 +2561,7 @@ pub mod account { #[command(after_help = crate::command_examples::ACCOUNT_PERMISSION_SHARE_NEW)] New { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// Target account email receiving the permissions. target_account_email: String, @@ -2603,7 +2599,7 @@ pub mod account { #[command(after_help = crate::command_examples::ACCOUNT_GET)] Get { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, }, /// Update some information about the account. /// @@ -2611,7 +2607,7 @@ pub mod account { #[command(after_help = crate::command_examples::ACCOUNT_UPDATE)] Update { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// New name to set for the account. account_name: String, }, @@ -2627,7 +2623,7 @@ pub mod account { #[command(after_help = crate::command_examples::ACCOUNT_DELETE)] Delete { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, }, /// Show current or historical account usage. Usage { @@ -2648,7 +2644,7 @@ pub mod account { } pub mod card { - use crate::command::shared_args::AccountIdOptionalArg; + use crate::command::shared_args::AccountScopeOptionalArgs; use crate::model::agent::RawAgentId; use clap::Subcommand; use golem_common::model::card::CardId; @@ -2659,10 +2655,10 @@ pub mod card { #[command(after_help = crate::command_examples::CARD_LIST)] List { #[command(flatten)] - account_id: AccountIdOptionalArg, + account: AccountScopeOptionalArgs, /// List cards in an agent's wallet instead of account-owned cards. Activates the agent if not already active. - #[arg(long, conflicts_with = "account_id")] + #[arg(long, conflicts_with_all = ["account", "account_id"])] agent: Option, /// Include account root cards. If no include flags are set, all account card kinds are included. @@ -3203,7 +3199,7 @@ mod test { AccountSubcommand::Usage { subcommand: AccountUsageSubcommand::Show { - account_id: selected_account_id, + account: selected_account_id, period, }, }, @@ -3349,6 +3345,133 @@ mod test { ); } + #[test] + fn account_scopes_accept_email_or_id_and_reject_both() { + let account_id = "00000000-0000-0000-0000-000000000001"; + let commands: &[&[&str]] = &[ + &["account", "get"], + &["account", "update", "new-name"], + &["account", "delete"], + &["account", "usage", "show"], + &["account", "usage", "history"], + &["account", "limits", "show"], + &["account", "limits", "set", "1024"], + &["account", "limits", "unset"], + &["account", "permission-share", "list"], + &["account", "permission-share", "get-by-name", "share"], + &[ + "account", + "permission-share", + "new", + "target@example.com", + "share", + ], + &["plugin", "list"], + &["plugin", "register", "-"], + &["card", "list"], + ]; + + for command in commands { + let base = std::iter::once("golem").chain(command.iter().copied()); + assert!( + GolemCliCommand::try_parse_from( + base.clone().chain(["--account", "owner@example.com"]) + ) + .is_ok(), + "email scope failed for {command:?}" + ); + assert!( + GolemCliCommand::try_parse_from(base.clone().chain(["--account-id", account_id])) + .is_ok(), + "ID scope failed for {command:?}" + ); + assert!( + GolemCliCommand::try_parse_from(base.chain([ + "--account", + "owner@example.com", + "--account-id", + account_id + ])) + .is_err(), + "conflicting scope accepted for {command:?}" + ); + } + } + + #[test] + fn plugin_identity_forms_are_complete_and_exclusive() { + let id = "8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890"; + for action in ["get", "unregister"] { + assert!( + GolemCliCommand::try_parse_from(["golem", "plugin", action, "name", "1.0.0"]) + .is_ok() + ); + assert!( + GolemCliCommand::try_parse_from(["golem", "plugin", action, "--id", id]).is_ok() + ); + assert!(GolemCliCommand::try_parse_from(["golem", "plugin", action, "name"]).is_err()); + assert!(GolemCliCommand::try_parse_from(["golem", "plugin", action, id]).is_err()); + assert!( + GolemCliCommand::try_parse_from([ + "golem", "plugin", action, "name", "1.0.0", "--id", id + ]) + .is_err() + ); + assert!( + GolemCliCommand::try_parse_from([ + "golem", + "plugin", + action, + "--id", + id, + "--account", + "owner@example.com" + ]) + .is_err() + ); + } + } + + #[test] + fn card_agent_conflicts_with_account_scope_and_include_filters() { + assert!( + GolemCliCommand::try_parse_from([ + "golem", + "card", + "list", + "--agent", + "shopping-cart/123" + ]) + .is_ok() + ); + for conflicting in [ + "--account", + "--account-id", + "--include-root", + "--include-permission-shares", + "--include-environment-defaults", + "--include-agent-initials", + ] { + let mut args = vec![ + "golem", + "card", + "list", + "--agent", + "shopping-cart/123", + conflicting, + ]; + if conflicting == "--account" { + args.push("owner@example.com"); + } else if conflicting == "--account-id" { + args.push("00000000-0000-0000-0000-000000000001"); + } + assert!( + GolemCliCommand::try_parse_from(args).is_err(), + "accepted --agent with {conflicting}" + ); + } + } + #[test] fn help_targets_to_subcommands_uses_valid_subcommands() { for target in ShowClapHelpTarget::iter() { diff --git a/cli/golem-cli/src/command_examples.rs b/cli/golem-cli/src/command_examples.rs index 27c6cbd549..8c3ff9f233 100644 --- a/cli/golem-cli/src/command_examples.rs +++ b/cli/golem-cli/src/command_examples.rs @@ -514,22 +514,37 @@ pub const API_DOMAIN_DELETE: &str = "Examples: pub const PLUGIN_LIST: &str = "Examples: # List all plugins registered for the current account - golem-cli plugin list"; + golem-cli plugin list + + # List plugins owned by another account + golem-cli plugin list --account owner@example.com + + # Account IDs are also accepted + golem-cli plugin list --account-id 2f6b30d9-bac2-4c67-9d4f-12ea89ba2211"; pub const PLUGIN_GET: &str = "Examples: - # Show details of a registered plugin - golem-cli plugin get 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890"; + # Show details by name and version + golem-cli plugin get my-plugin 1.0.0 + + # Show details by ID + golem-cli plugin get --id 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890"; pub const PLUGIN_REGISTER: &str = "Examples: # Register a plugin from a manifest file on disk golem-cli plugin register ./my-plugin.json # Read the manifest from stdin (e.g. piped from a generator) - cat my-plugin.json | golem-cli plugin register -"; + cat my-plugin.json | golem-cli plugin register - + + # Register for an explicitly selected account + golem-cli plugin register ./my-plugin.json --account owner@example.com"; pub const PLUGIN_UNREGISTER: &str = "Examples: - # Unregister a plugin by ID (use `plugin list` / `plugin get` to find IDs) - golem-cli plugin unregister 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890"; + # Unregister by name and version + golem-cli plugin unregister my-plugin 1.0.0 + + # Unregister by ID + golem-cli plugin unregister --id 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890"; // Profile commands --------------------------------------------------------------------------------- @@ -608,14 +623,17 @@ pub const ACCOUNT_GET: &str = "Examples: golem-cli account get # Show details of a specific account by ID - golem-cli account get --account-id acc-12345"; + golem-cli account get --account-id 2f6b30d9-bac2-4c67-9d4f-12ea89ba2211 + + # Show details by email + golem-cli account get --account owner@example.com"; pub const ACCOUNT_UPDATE: &str = "Examples: # Update the current account's name golem-cli account update 'Alice Smith' - # Update a specific account by ID - golem-cli account update 'Alice Smith' --account-id acc-12345"; + # Update a specific account by email + golem-cli account update 'Alice Smith' --account owner@example.com"; pub const ACCOUNT_NEW: &str = "Examples: # Add a new account @@ -625,15 +643,38 @@ pub const ACCOUNT_DELETE: &str = "Examples: # Delete the current account golem-cli account delete - # Delete a specific account by ID - golem-cli account delete --account-id acc-12345"; + # Delete a specific account by email + golem-cli account delete --account owner@example.com"; + +pub const ACCOUNT_USAGE_SHOW: &str = "Examples: + golem-cli account usage show + golem-cli account usage show --period 2026-08 --account owner@example.com"; + +pub const ACCOUNT_USAGE_HISTORY: &str = "Examples: + golem-cli account usage history + golem-cli account usage history --last 3 --account-id 2f6b30d9-bac2-4c67-9d4f-12ea89ba2211"; + +pub const ACCOUNT_LIMITS_SHOW: &str = "Examples: + golem-cli account limits show + golem-cli account limits show --account owner@example.com"; + +pub const ACCOUNT_LIMITS_SET: &str = "Examples: + golem-cli account limits set 1048576 + golem-cli account limits set --max-memory-per-agent 2097152 --account owner@example.com"; + +pub const ACCOUNT_LIMITS_UNSET: &str = "Examples: + golem-cli account limits unset + golem-cli account limits unset --max-memory-per-agent --account owner@example.com"; pub const ACCOUNT_PERMISSION_SHARE_LIST: &str = "Examples: # List permission shares owned by the current account golem-cli account permission-share list # List permission shares received by the current account - golem-cli account permission-share list --received"; + golem-cli account permission-share list --received + + # List shares owned by another account + golem-cli account permission-share list --account owner@example.com"; pub const ACCOUNT_PERMISSION_SHARE_GET: &str = "Examples: # Get a permission share by ID @@ -641,7 +682,10 @@ pub const ACCOUNT_PERMISSION_SHARE_GET: &str = "Examples: pub const ACCOUNT_PERMISSION_SHARE_GET_BY_NAME: &str = "Examples: # Get a permission share by name from the current account - golem-cli account permission-share get-by-name staging-access"; + golem-cli account permission-share get-by-name staging-access + + # Select its owner by email + golem-cli account permission-share get-by-name staging-access --account owner@example.com"; pub const ACCOUNT_PERMISSION_SHARE_NEW: &str = "Examples: # Share permissions with another account @@ -652,7 +696,10 @@ pub const ACCOUNT_PERMISSION_SHARE_NEW: &str = "Examples: # Add a lower negative grant by repeating the flag golem-cli account permission-share new target@example.com staging-access \ --lower-positive 'environment(my-account/my-app) @ target@example.com : view : staging' \ - --lower-negative 'component(my-account/my-app/staging) @ target@example.com : delete : *'"; + --lower-negative 'component(my-account/my-app/staging) @ target@example.com : delete : *' + + # Create the share for an explicitly selected owner + golem-cli account permission-share new target@example.com staging-access --account owner@example.com"; pub const ACCOUNT_PERMISSION_SHARE_UPDATE: &str = "Examples: # Replace lower permission grants on an existing share @@ -677,6 +724,9 @@ pub const CARD_LIST: &str = "Examples: # List cards owned by a specific account golem-cli card list --account-id 2f6b30d9-bac2-4c67-9d4f-12ea89ba2211 + # Account email is also accepted + golem-cli card list --account owner@example.com + # List only environment-default and agent-initial account cards golem-cli card list --include-environment-defaults --include-agent-initials diff --git a/cli/golem-cli/src/command_handler/account.rs b/cli/golem-cli/src/command_handler/account.rs index 88fc017abf..df2bf9edc3 100644 --- a/cli/golem-cli/src/command_handler/account.rs +++ b/cli/golem-cli/src/command_handler/account.rs @@ -16,6 +16,7 @@ use crate::command::account::{ AccountLimitsSubcommand, AccountSubcommand, AccountUsageSubcommand, PermissionShareGrantArgs, PermissionShareSubcommand, }; +use crate::command::shared_args::AccountScopeOptionalArgs; use crate::command_handler::Handlers; use crate::context::Context; use crate::error::NonSuccessfulExit; @@ -49,18 +50,16 @@ impl AccountCommandHandler { pub async fn handle_command(&self, subcommand: AccountSubcommand) -> anyhow::Result<()> { match subcommand { - AccountSubcommand::Get { account_id } => self.cmd_get(account_id.account_id).await, + AccountSubcommand::Get { account } => self.cmd_get(account).await, AccountSubcommand::Update { - account_id, + account, account_name, - } => self.cmd_update(account_id.account_id, account_name).await, + } => self.cmd_update(account, account_name).await, AccountSubcommand::New { account_name, account_email, } => self.cmd_new(account_name, account_email).await, - AccountSubcommand::Delete { account_id } => { - self.cmd_delete(account_id.account_id).await - } + AccountSubcommand::Delete { account } => self.cmd_delete(account).await, AccountSubcommand::Usage { subcommand } => self.handle_usage_command(subcommand).await, AccountSubcommand::Limits { subcommand } => { self.handle_limits_command(subcommand).await @@ -73,11 +72,11 @@ impl AccountCommandHandler { async fn handle_usage_command(&self, subcommand: AccountUsageSubcommand) -> anyhow::Result<()> { match subcommand { - AccountUsageSubcommand::Show { account_id, period } => { - self.cmd_usage_show(account_id.account_id, period).await + AccountUsageSubcommand::Show { account, period } => { + self.cmd_usage_show(account, period).await } - AccountUsageSubcommand::History { account_id, last } => { - self.cmd_usage_history(account_id.account_id, last).await + AccountUsageSubcommand::History { account, last } => { + self.cmd_usage_history(account, last).await } } } @@ -87,17 +86,15 @@ impl AccountCommandHandler { subcommand: AccountLimitsSubcommand, ) -> anyhow::Result<()> { match subcommand { - AccountLimitsSubcommand::Show { account_id } => { - self.cmd_limits_show(account_id.account_id).await - } + AccountLimitsSubcommand::Show { account } => self.cmd_limits_show(account).await, AccountLimitsSubcommand::Set { - account_id, + account, max_storage_per_agent, max_memory_per_agent, monthly_memory_gb_seconds, } => { self.cmd_limits_set( - account_id.account_id, + account, max_storage_per_agent, max_memory_per_agent, monthly_memory_gb_seconds, @@ -105,13 +102,13 @@ impl AccountCommandHandler { .await } AccountLimitsSubcommand::Unset { - account_id, + account, storage, max_memory_per_agent, monthly_memory_gb_seconds, } => { self.cmd_limits_unset( - account_id.account_id, + account, storage, max_memory_per_agent, monthly_memory_gb_seconds, @@ -126,33 +123,23 @@ impl AccountCommandHandler { subcommand: PermissionShareSubcommand, ) -> anyhow::Result<()> { match subcommand { - PermissionShareSubcommand::List { - account_id, - received, - } => { - self.cmd_permission_share_list(account_id.account_id, received) - .await + PermissionShareSubcommand::List { account, received } => { + self.cmd_permission_share_list(account, received).await } PermissionShareSubcommand::Get { permission_share_id, } => self.cmd_permission_share_get(permission_share_id).await, - PermissionShareSubcommand::GetByName { account_id, name } => { - self.cmd_permission_share_get_by_name(account_id.account_id, name) - .await + PermissionShareSubcommand::GetByName { account, name } => { + self.cmd_permission_share_get_by_name(account, name).await } PermissionShareSubcommand::New { - account_id, + account, target_account_email, name, grants, } => { - self.cmd_permission_share_new( - account_id.account_id, - target_account_email, - name, - grants, - ) - .await + self.cmd_permission_share_new(account, target_account_email, name, grants) + .await } PermissionShareSubcommand::Update { permission_share_id, @@ -168,8 +155,8 @@ impl AccountCommandHandler { } } - async fn cmd_get(&self, account_id: Option) -> anyhow::Result<()> { - let account = self.get(account_id).await?; + async fn cmd_get(&self, account: AccountScopeOptionalArgs) -> anyhow::Result<()> { + let account = self.get(account).await?; self.ctx.log_handler().log_output(AccountGetView(account))?; Ok(()) @@ -177,10 +164,10 @@ impl AccountCommandHandler { async fn cmd_update( &self, - account_id: Option, + account: AccountScopeOptionalArgs, account_name: String, ) -> anyhow::Result<()> { - let account = self.get(account_id).await?; + let account = self.get(account).await?; let account = self .ctx .golem_clients() @@ -222,8 +209,8 @@ impl AccountCommandHandler { Ok(()) } - async fn cmd_delete(&self, account_id: Option) -> anyhow::Result<()> { - let account = self.get(account_id).await?; + async fn cmd_delete(&self, account: AccountScopeOptionalArgs) -> anyhow::Result<()> { + let account = self.get(account).await?; if !self .ctx .interactive_handler() @@ -250,10 +237,10 @@ impl AccountCommandHandler { async fn cmd_usage_show( &self, - account_id: Option, + account: AccountScopeOptionalArgs, period: Option, ) -> anyhow::Result<()> { - let account_id = self.select_account_id_or_err(account_id).await?; + let account_id = self.select_account_id_or_err(account).await?; let period = period.map(|period| period.to_string()); let usage = self .ctx @@ -271,10 +258,10 @@ impl AccountCommandHandler { async fn cmd_usage_history( &self, - account_id: Option, + account: AccountScopeOptionalArgs, last: usize, ) -> anyhow::Result<()> { - let account_id = self.select_account_id_or_err(account_id).await?; + let account_id = self.select_account_id_or_err(account).await?; let last = last.try_into()?; let usage = self .ctx @@ -293,8 +280,8 @@ impl AccountCommandHandler { Ok(()) } - async fn cmd_limits_show(&self, account_id: Option) -> anyhow::Result<()> { - let account_id = self.select_account_id_or_err(account_id).await?; + async fn cmd_limits_show(&self, account: AccountScopeOptionalArgs) -> anyhow::Result<()> { + let account_id = self.select_account_id_or_err(account).await?; let clients = self.ctx.golem_clients().await?; let storage = clients .account @@ -321,7 +308,7 @@ impl AccountCommandHandler { async fn cmd_limits_set( &self, - account_id: Option, + account: AccountScopeOptionalArgs, storage: Option, max_memory: Option, monthly_memory: Option, @@ -333,7 +320,7 @@ impl AccountCommandHandler { { bail!("only one limit can be changed per command"); } - let account_id = self.select_account_id_or_err(account_id).await?; + let account_id = self.select_account_id_or_err(account).await?; let clients = self.ctx.golem_clients().await?; if let Some(value) = storage { clients @@ -374,12 +361,16 @@ impl AccountCommandHandler { .await .map_service_error()?; } - self.cmd_limits_show(Some(account_id)).await + self.cmd_limits_show(AccountScopeOptionalArgs { + account: None, + account_id: Some(account_id), + }) + .await } async fn cmd_limits_unset( &self, - account_id: Option, + account: AccountScopeOptionalArgs, storage: bool, max_memory: bool, monthly_memory: bool, @@ -387,7 +378,7 @@ impl AccountCommandHandler { if storage as u8 + max_memory as u8 + monthly_memory as u8 > 1 { bail!("only one limit can be changed per command"); } - let account_id = self.select_account_id_or_err(account_id).await?; + let account_id = self.select_account_id_or_err(account).await?; let clients = self.ctx.golem_clients().await?; if storage || (!max_memory && !monthly_memory) { clients @@ -410,15 +401,19 @@ impl AccountCommandHandler { .await .map_service_error()?; } - self.cmd_limits_show(Some(account_id)).await + self.cmd_limits_show(AccountScopeOptionalArgs { + account: None, + account_id: Some(account_id), + }) + .await } async fn cmd_permission_share_list( &self, - account_id: Option, + account: AccountScopeOptionalArgs, received: bool, ) -> anyhow::Result<()> { - let account_id = self.select_account_id_or_err(account_id).await?; + let account_id = self.select_account_id_or_err(account).await?; let shares = if received { self.ctx .golem_clients() @@ -460,10 +455,10 @@ impl AccountCommandHandler { async fn cmd_permission_share_get_by_name( &self, - account_id: Option, + account: AccountScopeOptionalArgs, name: String, ) -> anyhow::Result<()> { - let account_id = self.select_account_id_or_err(account_id).await?; + let account_id = self.select_account_id_or_err(account).await?; let share = self .ctx .golem_clients() @@ -482,12 +477,12 @@ impl AccountCommandHandler { async fn cmd_permission_share_new( &self, - account_id: Option, + account: AccountScopeOptionalArgs, target_account_email: String, name: String, grants: PermissionShareGrantArgs, ) -> anyhow::Result<()> { - let account_id = self.select_account_id_or_err(account_id).await?; + let account_id = self.select_account_id_or_err(account).await?; let share = self .ctx .golem_clients() @@ -565,15 +560,8 @@ impl AccountCommandHandler { Ok(()) } - async fn get(&self, account_id: Option) -> anyhow::Result { - Ok(self - .ctx - .golem_clients() - .await? - .account - .get_account(&self.select_account_id_or_err(account_id).await?.0) - .await - .map_service_error()?) + async fn get(&self, account: AccountScopeOptionalArgs) -> anyhow::Result { + self.select_account_or_err(account).await } async fn get_permission_share( @@ -596,15 +584,75 @@ impl AccountCommandHandler { pub async fn select_account_id_or_err( &self, - account_id: Option, + account: AccountScopeOptionalArgs, ) -> anyhow::Result { - match account_id { - Some(account_id) => Ok(account_id), - None => Ok(self.account_id_or_err().await?), + match (account.account, account.account_id) { + (Some(email), None) => Ok(self + .ctx + .golem_clients() + .await? + .account + .get_account_by_email(&email) + .await + .map_service_error()? + .id), + (None, Some(account_id)) => Ok(account_id), + (None, None) => Ok(self.account_id_or_err().await?), + (Some(_), Some(_)) => unreachable!("clap rejects conflicting account scope flags"), + } + } + + /// Resolves the account scope *without* turning an email into an id up front. + /// + /// Commands backed by a resource endpoint that also accepts the owner email (e.g. the + /// by-email plugin lookup) should use this and dispatch on the result, so that + /// `--account ` does not require `AccountVerb::View` the way resolving through + /// `get_account_by_email` would — keeping it on par with `--account-id`. + pub async fn select_account_scope_or_err( + &self, + account: AccountScopeOptionalArgs, + ) -> anyhow::Result { + match (account.account, account.account_id) { + (Some(email), None) => Ok(AccountScope::Email(email)), + (None, Some(account_id)) => Ok(AccountScope::Id(account_id)), + (None, None) => Ok(AccountScope::Id(self.account_id_or_err().await?)), + (Some(_), Some(_)) => unreachable!("clap rejects conflicting account scope flags"), + } + } + + pub async fn select_account_or_err( + &self, + account: AccountScopeOptionalArgs, + ) -> anyhow::Result { + let clients = self.ctx.golem_clients().await?; + match (account.account, account.account_id) { + (Some(email), None) => Ok(clients + .account + .get_account_by_email(&email) + .await + .map_service_error()?), + (None, Some(account_id)) => Ok(clients + .account + .get_account(&account_id.0) + .await + .map_service_error()?), + (None, None) => Ok(clients + .account + .get_account(&clients.account_id().0) + .await + .map_service_error()?), + (Some(_), Some(_)) => unreachable!("clap rejects conflicting account scope flags"), } } } +/// An account scope that has not been collapsed to an id, so callers can pick a by-email or +/// by-id resource endpoint. See [`AccountHandler::select_account_scope_or_err`]. +pub enum AccountScope { + Email(String), + Id(AccountId), +} + fn permission_share_data(grants: PermissionShareGrantArgs) -> PermissionShareData { PermissionShareData { lower_positive: grants.lower_positive.unwrap_or_default(), diff --git a/cli/golem-cli/src/command_handler/api/domain.rs b/cli/golem-cli/src/command_handler/api/domain.rs index ece2d1dd27..f1f19f3ce3 100644 --- a/cli/golem-cli/src/command_handler/api/domain.rs +++ b/cli/golem-cli/src/command_handler/api/domain.rs @@ -21,8 +21,7 @@ use crate::model::http_api::domain::{ use crate::command::api::domain::ApiDomainSubcommand; use crate::error::NonSuccessfulExit; -use crate::log::log_error; -use crate::log::{LogColorize, log_action, log_warn_action, logln}; +use crate::log::{LogColorize, log_action, log_error, log_warn_action, logln}; use crate::model::environment::EnvironmentResolveMode; use anyhow::bail; use golem_client::api::ApiDomainClient; @@ -114,15 +113,20 @@ impl ApiDomainCommandHandler { let clients = self.ctx.golem_clients().await?; - let domains = self.list_domains(&environment.environment_id).await?; + let domain_to_delete = clients + .api_domain + .get_environment_domain_registration(&environment.environment_id.0, &domain.0) + .await + .map_service_error_not_found_as_opt()?; - let Some(domain_to_delete) = domains.iter().find(|d| d.domain == domain).cloned() else { + let Some(domain_to_delete) = domain_to_delete else { log_error(format!( "Domain {} not found", domain.0.log_color_highlight() )); logln(""); + let domains = self.list_domains(&environment.environment_id).await?; if domains.is_empty() { logln(format!( "No domains are registered yet for {}", @@ -138,7 +142,7 @@ impl ApiDomainCommandHandler { .to_string(), ); for domain in domains { - logln(format!("- {}", domain.domain.0)) + logln(format!("- {}", domain.domain.0)); } } diff --git a/cli/golem-cli/src/command_handler/api/security_scheme.rs b/cli/golem-cli/src/command_handler/api/security_scheme.rs index 3c2656a977..b6108c2812 100644 --- a/cli/golem-cli/src/command_handler/api/security_scheme.rs +++ b/cli/golem-cli/src/command_handler/api/security_scheme.rs @@ -187,16 +187,13 @@ impl ApiSecuritySchemeCommandHandler { let clients = self.ctx.golem_clients().await?; - // TODO: atomic: missing client method to get by name - let Some(result) = clients + let result = clients .api_security - .list_environment_security_schemes(&environment.environment_id.0) + .get_environment_security_scheme(&environment.environment_id.0, &security_scheme_name.0) .await - .map_service_error()? - .values - .into_iter() - .find(|s| s.name == *security_scheme_name) - else { + .map_service_error_not_found_as_opt()?; + + let Some(result) = result else { log_error(format!( "HTTP API Security Scheme {} not found.", security_scheme_name.0 diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 036fbc5289..1ac4d788b5 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -2787,25 +2787,34 @@ impl AppCommandHandler { let account_id = self.ctx.account_id().await?; + self.get_or_create_server_application(&account_id, application_name) + .await + .map(Some) + } + + pub async fn get_or_create_server_application( + &self, + account_id: &AccountId, + application_name: &ApplicationName, + ) -> anyhow::Result { match self - .get_server_application(&account_id, application_name) + .get_server_application(account_id, application_name) .await? { - Some(application) => Ok(Some(application)), - None => Ok(Some( - self.ctx - .golem_clients() - .await? - .application - .create_application( - &account_id.0, - &ApplicationCreation { - name: application_name.clone(), - }, - ) - .await - .map_service_error()?, - )), + Some(application) => Ok(application), + None => Ok(self + .ctx + .golem_clients() + .await? + .application + .create_application( + &account_id.0, + &ApplicationCreation { + name: application_name.clone(), + }, + ) + .await + .map_service_error()?), } } diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index 2a196cce85..02c050ff58 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::command::card::CardSubcommand; +use crate::command::shared_args::AccountScopeOptionalArgs; use crate::command_handler::Handlers; use crate::command_handler::agent::AgentCommandHandler; use crate::context::Context; @@ -22,7 +23,6 @@ use crate::model::agent::RawAgentId; use crate::model::card::{CardGetView, CardListView, CardRevokeView}; use anyhow::bail; use golem_client::api::{CardClient, WorkerClient}; -use golem_common::model::account::AccountId; use golem_common::model::card::CardId; use std::sync::Arc; @@ -78,7 +78,7 @@ impl CardCommandHandler { pub async fn handle_command(&self, subcommand: CardSubcommand) -> anyhow::Result<()> { match subcommand { CardSubcommand::List { - account_id, + account, agent, include_root, include_permission_shares, @@ -86,7 +86,7 @@ impl CardCommandHandler { include_agent_initials, } => { self.cmd_list( - account_id.account_id, + account, agent, CardListFilter::from_flags( include_root, @@ -104,7 +104,7 @@ impl CardCommandHandler { async fn cmd_list( &self, - account_id: Option, + account: AccountScopeOptionalArgs, agent: Option, filter: CardListFilter, ) -> anyhow::Result<()> { @@ -115,7 +115,11 @@ impl CardCommandHandler { return self.cmd_list_agent_wallet(agent).await; } - let account_id = account_id.unwrap_or(*self.ctx.golem_clients().await?.account_id()); + let account_id = self + .ctx + .account_handler() + .select_account_id_or_err(account) + .await?; let cards = self .ctx .golem_clients() diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index ec6634da44..d4889d184b 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -49,7 +49,7 @@ use crate::model::environment::{ }; use crate::model::help::ComponentNameHelp; use crate::model::language::GuestLanguage; -use crate::model::plugin::PluginNameAndVersion; +use crate::model::plugin::PluginGrantKey; use crate::model::text_format::log_text_view; use crate::model::tool_deployment::{ DiscoveredToolImplementation, ToolEntityPath, ToolImplementationSource, ToolValidationCode, @@ -1382,18 +1382,19 @@ impl ComponentCommandHandler { &self, tool_name: &ToolName, provision: &ToolManifestProvisionConfig, - plugin_grants: &HashMap, + plugin_grants: &HashMap, ) -> anyhow::Result<(ToolProvisionConfig, Vec)> { let plugins = provision .plugins .iter() .enumerate() .map(|(index, plugin)| { - let grant = plugin_grants - .get(&PluginNameAndVersion { - name: plugin.name.clone(), - version: plugin.version.clone(), - }) + let grant = PluginGrantKey::resolve( + plugin_grants, + plugin.account.as_deref(), + &plugin.name, + &plugin.version, + )? .with_context(|| { format!( "Plugin {}/{} required by remote tool {} is not granted to this environment", @@ -1650,27 +1651,26 @@ impl ComponentCommandHandler { }) .collect(); - // TODO: atomic: cannot lookup by account email - // Look up plugin grants let plugins_by_grant_id = manifest_config .plugins .iter() .enumerate() .map(|(idx, p)| { - let grant = plugin_grants - .get(&PluginNameAndVersion { - name: p.name.clone(), - version: p.version.clone(), - }) - .ok_or_else(|| { - anyhow!( - "Plugin {}/{} is not available in this environment. \ + let grant = PluginGrantKey::resolve( + &plugin_grants, + p.account.as_deref(), + &p.name, + &p.version, + )? + .ok_or_else(|| { + anyhow!( + "Plugin {}/{} is not available in this environment. \ Use 'golem plugin list' to see available plugins, \ or grant the plugin to this environment first.", - p.name, - p.version - ) - })?; + p.name, + p.version + ) + })?; Ok(( grant.id.0, diff::PluginInstallation { @@ -1785,11 +1785,12 @@ impl ComponentCommandHandler { .iter() .enumerate() .map(|(index, plugin)| { - let grant = plugin_grants - .get(&PluginNameAndVersion { - name: plugin.name.clone(), - version: plugin.version.clone(), - }) + let grant = PluginGrantKey::resolve( + &plugin_grants, + plugin.account.as_deref(), + &plugin.name, + &plugin.version, + )? .ok_or_else(|| { anyhow!( "Plugin {}/{} is not available in this environment. Use 'golem plugin list' to see available plugins, or grant the plugin to this environment first.", diff --git a/cli/golem-cli/src/command_handler/component/staging.rs b/cli/golem-cli/src/command_handler/component/staging.rs index 44f0477af1..07ba4fe344 100644 --- a/cli/golem-cli/src/command_handler/component/staging.rs +++ b/cli/golem-cli/src/command_handler/component/staging.rs @@ -25,7 +25,7 @@ use crate::model::app_raw; use crate::model::component::initial_permission_recipient_context; use crate::model::component::{AgentTypeManifestProvisionConfig, ComponentDeployProperties}; use crate::model::environment::ResolvedEnvironmentIdentity; -use crate::model::plugin::PluginNameAndVersion; +use crate::model::plugin::PluginGrantKey; use anyhow::{Context as AnyhowContext, anyhow}; use golem_client::model::EnvironmentPluginGrantWithDetails; use golem_common::model::agent::AgentTypeName; @@ -219,7 +219,7 @@ pub struct ComponentStager<'a> { ctx: Arc, component_deploy_properties: &'a ComponentDeployProperties, diff: ComponentDiff, - plugin_grants: HashMap, + plugin_grants: HashMap, manifest_files_by_agent: OnceCell>>, manifest_files_by_tool: OnceCell>>, } @@ -228,7 +228,7 @@ impl<'a> ComponentStager<'a> { pub fn new( ctx: Arc, component_deploy_properties: &'a ComponentDeployProperties, - plugin_grants: HashMap, + plugin_grants: HashMap, // NOTE: none means ALL changed (e.g. new component) diff: Option<&diff::DiffForHashOf>, ) -> anyhow::Result { @@ -770,21 +770,21 @@ impl<'a> ComponentStager<'a> { .iter() .enumerate() .map(|(idx, p)| { - let grant = self - .plugin_grants - .get(&PluginNameAndVersion { - name: p.name.clone(), - version: p.version.clone(), - }) - .ok_or_else(|| { - anyhow!( - "Plugin {}/{} is not available in this environment. \ + let grant = PluginGrantKey::resolve( + &self.plugin_grants, + p.account.as_deref(), + &p.name, + &p.version, + )? + .ok_or_else(|| { + anyhow!( + "Plugin {}/{} is not available in this environment. \ Use 'golem plugin list' to see available plugins, \ or grant the plugin to this environment first.", - p.name, - p.version - ) - })?; + p.name, + p.version + ) + })?; Ok(PluginInstallation { environment_plugin_grant_id: grant.id, priority: PluginPriority(idx as i32), diff --git a/cli/golem-cli/src/command_handler/environment.rs b/cli/golem-cli/src/command_handler/environment.rs index a12c4d449e..ff944f43b1 100644 --- a/cli/golem-cli/src/command_handler/environment.rs +++ b/cli/golem-cli/src/command_handler/environment.rs @@ -28,7 +28,7 @@ use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; use crate::model::help::EnvironmentNameHelp; -use crate::model::plugin::PluginNameAndVersion; +use crate::model::plugin::PluginGrantKey; use crate::model::text_format::log_text_view; use anyhow::{anyhow, bail}; use golem_client::api::{EnvironmentClient, MeClient}; @@ -144,7 +144,7 @@ impl EnvironmentCommandHandler { match self.ctx.manifest_environment() { Some(env) => match &env.environment.account { Some(account) => { - let env_summary = self + let visible_environment = self .ctx .golem_clients() .await? @@ -154,30 +154,44 @@ impl EnvironmentCommandHandler { Some(&env.application_name.0), Some(&env.environment_name.0), ) - .await? + .await + .map_service_error()? .values .pop(); - match env_summary { - Some(env_summary) => { - Ok(ResolvedEnvironmentIdentity::from_summary(None, env_summary)) - } - None => { - // TODO: atomic: here we should try to create the env - // (especially that account might be the current one), - // but we cannot resolve account_id by email currently - log_error(format!( - "Environment {}/{}/{} not found", - account.log_color_highlight(), - env.application_name.0.log_color_highlight(), - env.environment_name.to_string().log_color_highlight() - )); - - self.show_available_application_environments().await?; - - bail!(NonSuccessfulExit); - } + if let Some(visible_environment) = visible_environment { + return Ok(ResolvedEnvironmentIdentity::from_summary( + None, + visible_environment, + )); } + + let account = self + .ctx + .account_handler() + .select_account_or_err( + crate::command::shared_args::AccountScopeOptionalArgs { + account: Some(account.clone()), + account_id: None, + }, + ) + .await?; + let application = self + .ctx + .app_handler() + .get_or_create_server_application(&account.id, &env.application_name) + .await?; + let environment = self + .get_or_create_server_environment_by_manifest( + &application.id, + &env.environment_name, + ) + .await?; + Ok(ResolvedEnvironmentIdentity::from_app_and_env( + None, + application, + environment, + )) } None => { let application = self @@ -498,7 +512,7 @@ impl EnvironmentCommandHandler { pub async fn plugin_grants( &self, environment: &ResolvedEnvironmentIdentity, - ) -> anyhow::Result> { + ) -> anyhow::Result> { self.ctx .caches() .plugin_grants @@ -516,7 +530,8 @@ impl EnvironmentCommandHandler { { result.values.into_iter().map(|p| { ( - PluginNameAndVersion { + PluginGrantKey { + account: p.plugin_account.email.to_string(), name: p.plugin.name.clone(), version: p.plugin.version.clone(), }, diff --git a/cli/golem-cli/src/command_handler/mod.rs b/cli/golem-cli/src/command_handler/mod.rs index 7063befda3..22152c6c37 100644 --- a/cli/golem-cli/src/command_handler/mod.rs +++ b/cli/golem-cli/src/command_handler/mod.rs @@ -689,13 +689,6 @@ impl Handlers for Arc { LogHandler::new(self.clone()) } - // TODO: atomic: - /* - fn plugin_installation_handler(&self) -> PluginInstallationHandler { - PluginInstallationHandler::new(self.clone()) - } - */ - fn plugin_handler(&self) -> PluginCommandHandler { PluginCommandHandler::new(self.clone()) } diff --git a/cli/golem-cli/src/command_handler/plugin.rs b/cli/golem-cli/src/command_handler/plugin.rs index a0bc89cf89..68a50405ef 100644 --- a/cli/golem-cli/src/command_handler/plugin.rs +++ b/cli/golem-cli/src/command_handler/plugin.rs @@ -13,7 +13,9 @@ // limitations under the License. use crate::command::plugin::PluginSubcommand; +use crate::command::shared_args::AccountScopeOptionalArgs; use crate::command_handler::Handlers; +use crate::command_handler::account::AccountScope; use crate::context::Context; use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action}; @@ -44,16 +46,33 @@ impl PluginCommandHandler { pub async fn handle_command(&self, subcommand: PluginSubcommand) -> anyhow::Result<()> { match subcommand { - PluginSubcommand::List => self.cmd_list().await, - PluginSubcommand::Get { plugin_id: id } => self.cmd_get(id).await, - PluginSubcommand::Register { manifest } => self.cmd_register(manifest).await, - PluginSubcommand::Unregister { plugin_id: id } => self.cmd_unregister(id).await, + PluginSubcommand::List { account } => self.cmd_list(account).await, + PluginSubcommand::Get { + name, + version, + id, + account, + } => self.cmd_get(name, version, id, account).await, + PluginSubcommand::Register { manifest, account } => { + self.cmd_register(manifest, account).await + } + PluginSubcommand::Unregister { + name, + version, + id, + account, + } => self.cmd_unregister(name, version, id, account).await, } } - async fn cmd_list(&self) -> anyhow::Result<()> { + async fn cmd_list(&self, account: AccountScopeOptionalArgs) -> anyhow::Result<()> { let clients = self.ctx.golem_clients().await?; - let account_id = self.ctx.account_id().await?; + let explicit_scope = account.is_explicit(); + let account_id = self + .ctx + .account_handler() + .select_account_id_or_err(account) + .await?; let own_plugins = clients .plugin @@ -70,11 +89,12 @@ impl PluginCommandHandler { }) .collect(); - if let Ok(environment) = self - .ctx - .environment_handler() - .resolve_environment(EnvironmentResolveMode::ManifestOnly) - .await + if !explicit_scope + && let Ok(environment) = self + .ctx + .environment_handler() + .resolve_environment(EnvironmentResolveMode::ManifestOnly) + .await && let Ok(grants) = self .ctx .environment_handler() @@ -109,14 +129,41 @@ impl PluginCommandHandler { Ok(()) } - async fn cmd_get(&self, id: Uuid) -> anyhow::Result<()> { + async fn cmd_get( + &self, + name: Option, + version: Option, + id: Option, + account: AccountScopeOptionalArgs, + ) -> anyhow::Result<()> { let client = self.ctx.golem_clients().await?; - - let result = client - .plugin - .get_plugin_by_id(&id) - .await - .map_service_error()?; + let result = if let Some(id) = id { + client + .plugin + .get_plugin_by_id(&id) + .await + .map_service_error()? + } else { + let name = name.unwrap(); + let version = version.unwrap(); + match self + .ctx + .account_handler() + .select_account_scope_or_err(account) + .await? + { + AccountScope::Email(email) => client + .plugin + .get_account_plugin_by_email(&email, &name, &version) + .await + .map_service_error()?, + AccountScope::Id(account_id) => client + .plugin + .get_account_plugin(&account_id.0, &name, &version) + .await + .map_service_error()?, + } + }; self.ctx .log_handler() @@ -124,7 +171,11 @@ impl PluginCommandHandler { Ok(()) } - async fn cmd_register(&self, manifest: PathBufOrStdin) -> anyhow::Result<()> { + async fn cmd_register( + &self, + manifest: PathBufOrStdin, + account: AccountScopeOptionalArgs, + ) -> anyhow::Result<()> { let manifest = manifest.read_to_string()?; let manifest: PluginManifest = serde_yaml::from_str(&manifest) .with_context(|| anyhow!("Failed to decode plugin manifest"))?; @@ -156,10 +207,15 @@ impl PluginCommandHandler { let clients = self.ctx.golem_clients().await?; + let account_id = self + .ctx + .account_handler() + .select_account_id_or_err(account) + .await?; let result = clients .plugin .create_plugin( - &self.ctx.account_id().await?.0, + &account_id.0, &PluginRegistrationCreation { name: manifest.name, version: manifest.version, @@ -180,9 +236,40 @@ impl PluginCommandHandler { } } - async fn cmd_unregister(&self, id: Uuid) -> anyhow::Result<()> { + async fn cmd_unregister( + &self, + name: Option, + version: Option, + id: Option, + account: AccountScopeOptionalArgs, + ) -> anyhow::Result<()> { let clients = self.ctx.golem_clients().await?; + let id = if let Some(id) = id { + id + } else { + let name = name.unwrap(); + let version = version.unwrap(); + let plugin = match self + .ctx + .account_handler() + .select_account_scope_or_err(account) + .await? + { + AccountScope::Email(email) => clients + .plugin + .get_account_plugin_by_email(&email, &name, &version) + .await + .map_service_error()?, + AccountScope::Id(account_id) => clients + .plugin + .get_account_plugin(&account_id.0, &name, &version) + .await + .map_service_error()?, + }; + plugin.id.0 + }; + let result = clients .plugin .delete_plugin(&id) diff --git a/cli/golem-cli/src/command_handler/resource_definition.rs b/cli/golem-cli/src/command_handler/resource_definition.rs index bd8f1750f6..f62ac1d55e 100644 --- a/cli/golem-cli/src/command_handler/resource_definition.rs +++ b/cli/golem-cli/src/command_handler/resource_definition.rs @@ -138,15 +138,13 @@ impl ResourceDefinitionCommandHandler { .resolve_environment(EnvironmentResolveMode::Any) .await?; - let Some(resource) = clients + let resource = clients .resources - .list_environment_resources(&environment.environment_id.0) + .get_environment_resource(&environment.environment_id.0, &name) .await - .map_service_error()? - .values - .into_iter() - .find(|r| r.name.0 == name) - else { + .map_service_error_not_found_as_opt()?; + + let Some(resource) = resource else { log_error(format!( "Resource definition '{name}' not found in environment" )); diff --git a/cli/golem-cli/src/command_handler/retry_policy.rs b/cli/golem-cli/src/command_handler/retry_policy.rs index 5c32e37c90..c962a0fd79 100644 --- a/cli/golem-cli/src/command_handler/retry_policy.rs +++ b/cli/golem-cli/src/command_handler/retry_policy.rs @@ -137,16 +137,14 @@ impl RetryPolicyCommandHandler { let clients = self.ctx.golem_clients().await?; - let Some(result) = clients + let result = clients .retry_policies - .list_environment_retry_policies(&environment.environment_id.0) + .get_environment_retry_policy(&environment.environment_id.0, &name) .await - .map_service_error()? - .values - .into_iter() - .find(|p| p.name == name) - else { - log_error(format!("Retry policy '{}' not found in environment", name)); + .map_service_error_not_found_as_opt()?; + + let Some(result) = result else { + log_error(format!("Retry policy '{name}' not found in environment")); bail!(NonSuccessfulExit); }; diff --git a/cli/golem-cli/src/command_handler/secret.rs b/cli/golem-cli/src/command_handler/secret.rs index 48b6ce34e0..4edcd0061b 100644 --- a/cli/golem-cli/src/command_handler/secret.rs +++ b/cli/golem-cli/src/command_handler/secret.rs @@ -77,23 +77,21 @@ impl SecretCommandHandler { let canonical = CanonicalAgentSecretPath::from_path_in_unknown_casing(&path.0); - let secrets = clients + let secret = clients .agent_secrets - .list_environment_agent_secrets(&environment.environment_id.0) + .get_environment_agent_secret(&environment.environment_id.0, &canonical.0) .await - .map_service_error()? - .values; - - match secrets.into_iter().find(|s| s.path == canonical) { - Some(secret) => Ok(secret), - None => { - log_error(format!( - "Agent secret with path '{}' not found in environment", - canonical - )); - bail!(NonSuccessfulExit); - } - } + .map_service_error_not_found_as_opt()?; + + let Some(secret) = secret else { + log_error(format!( + "Agent secret with path '{}' not found in environment", + canonical + )); + bail!(NonSuccessfulExit); + }; + + Ok(secret) } else if let Some(id) = id { Ok(clients .agent_secrets diff --git a/cli/golem-cli/src/context.rs b/cli/golem-cli/src/context.rs index 36f2a0b8a6..9d8fd8bdc8 100644 --- a/cli/golem-cli/src/context.rs +++ b/cli/golem-cli/src/context.rs @@ -37,7 +37,7 @@ use crate::model::config::server::ToFormattedServerContext; use crate::model::environment::{EnvironmentReference, SelectedManifestEnvironment}; use crate::model::format::Format; use crate::model::masking::MaskingConfig; -use crate::model::plugin::PluginNameAndVersion; +use crate::model::plugin::PluginGrantKey; use crate::model::repl::ReplLanguage; use anyhow::{anyhow, bail}; use colored::control::SHOULD_COLORIZE; @@ -958,7 +958,7 @@ pub struct Caches { pub plugin_grants: Cache< EnvironmentId, (), - HashMap, + HashMap, Arc, >, } diff --git a/cli/golem-cli/src/model/plugin.rs b/cli/golem-cli/src/model/plugin.rs index 9a9e2f7962..7153316930 100644 --- a/cli/golem-cli/src/model/plugin.rs +++ b/cli/golem-cli/src/model/plugin.rs @@ -21,6 +21,7 @@ use crate::model::text_format::{ use golem_common::model::component::ComponentRevision; use golem_common::model::plugin_registration::PluginRegistrationDto; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fmt::Debug; use std::path::PathBuf; use uuid::Uuid; @@ -50,11 +51,41 @@ pub struct PluginManifest { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct PluginNameAndVersion { +pub struct PluginGrantKey { + pub account: String, pub name: String, pub version: String, } +impl PluginGrantKey { + pub fn resolve<'a, T>( + grants: &'a HashMap, + account: Option<&str>, + name: &str, + version: &str, + ) -> anyhow::Result> { + if let Some(account) = account { + return Ok(grants.get(&Self { + account: account.to_string(), + name: name.to_string(), + version: version.to_string(), + })); + } + + let mut matches = grants + .iter() + .filter(|(key, _)| key.name == name && key.version == version) + .map(|(_, value)| value); + let result = matches.next(); + if matches.next().is_some() { + anyhow::bail!( + "Plugin {name}/{version} is granted by multiple accounts; set 'account' in the plugin manifest entry" + ); + } + Ok(result) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum PluginSource { Own, @@ -236,3 +267,63 @@ fn plugin_registration_fields(plugin: &PluginRegistrationDto) -> Vec<(String, St fields.build() } + +#[cfg(test)] +mod tests { + use super::PluginGrantKey; + use std::collections::HashMap; + use test_r::test; + + fn grants() -> HashMap { + HashMap::from([ + ( + PluginGrantKey { + account: "first@example.com".to_string(), + name: "plugin".to_string(), + version: "1.0.0".to_string(), + }, + 1, + ), + ( + PluginGrantKey { + account: "second@example.com".to_string(), + name: "plugin".to_string(), + version: "1.0.0".to_string(), + }, + 2, + ), + ]) + } + + #[test] + fn resolves_plugin_grant_by_account_name_and_version() { + let grants = grants(); + + assert_eq!( + PluginGrantKey::resolve(&grants, Some("second@example.com"), "plugin", "1.0.0") + .unwrap(), + Some(&2) + ); + } + + #[test] + fn resolves_unqualified_plugin_grant_when_unique() { + let mut grants = grants(); + grants.retain(|key, _| key.account == "first@example.com"); + + assert_eq!( + PluginGrantKey::resolve(&grants, None, "plugin", "1.0.0").unwrap(), + Some(&1) + ); + } + + #[test] + fn rejects_ambiguous_unqualified_plugin_grant() { + let error = PluginGrantKey::resolve(&grants(), None, "plugin", "1.0.0").unwrap_err(); + + assert_eq!( + error.to_string(), + "Plugin plugin/1.0.0 is granted by multiple accounts; set 'account' in the plugin manifest entry" + ); + } +} diff --git a/cli/golem-cli/tests/app/plugins.rs b/cli/golem-cli/tests/app/plugins.rs index aae416c42a..69d928ddea 100644 --- a/cli/golem-cli/tests/app/plugins.rs +++ b/cli/golem-cli/tests/app/plugins.rs @@ -32,8 +32,7 @@ use uuid::Uuid; inherit_test_dep!(Tracing); -// TODO: atomic: re-enable test -#[ignore] +#[ignore = "covers the retired imperative component-plugin workflow"] #[test] async fn plugin_installation_test1(_tracing: &Tracing) { let mut ctx = TestContext::new(); @@ -355,8 +354,7 @@ impl TestPlugin { } } -// TODO: atomic: re-enable test -#[ignore] +#[ignore = "covers the retired imperative component-plugin workflow"] #[test] #[timeout("2 minutes")] async fn plugin_installation_test2(_tracing: &Tracing) { diff --git a/docs/openapi/gen-openapi.ts b/docs/openapi/gen-openapi.ts index 9852f9de8a..d64bde357d 100644 --- a/docs/openapi/gen-openapi.ts +++ b/docs/openapi/gen-openapi.ts @@ -144,15 +144,13 @@ function convertItemToMarkdown( return [ `## ${operation.summary}`, overviewTable, - "", explanation, - "", queryParamsTable, - "", requestBody, - "", response, - ].join("\n") + ] + .filter(section => section !== undefined && section !== "") + .join("\n\n") } type MdTable = { diff --git a/docs/src/content/next/cli/plugins.mdx b/docs/src/content/next/cli/plugins.mdx index 1ca88ed18c..f0fce54f71 100644 --- a/docs/src/content/next/cli/plugins.mdx +++ b/docs/src/content/next/cli/plugins.mdx @@ -1,217 +1,104 @@ # Golem CLI Plugins -## Manage the available plugins +Plugins extend component and agent behavior. The `golem plugin` command manages the account-level plugin registry; plugin installation on components is declarative in `golem.yaml`. -To manage the plugins available for installation, use the `golem plugin` commands. +See [Plugins](/next/concepts/plugins) for the underlying concepts. -See the [Plugins page](/next/concepts/plugins) for general information about Golem plugins. +## List registered plugins -### List the available plugins - -To list the available plugins, use the `golem plugin list` command. +Without an account option, the command lists plugins owned by the authenticated account plus plugins granted to the selected environment: ```shell copy golem plugin list ``` -Plugins can be installed to different **scopes**. By default the command lists the plugins installed in the **global scope**. - -Use the following options to list plugins in other scopes: - -- `--project `: List plugins installed in the given project's scope -- `--component `: List plugins installed in the given component's scope. - -### Get information about a registered plugin - -To get more information about one of the available plugins, use the `golem plugin get` command: +An explicit account scope lists plugins owned by that account. Select it by email or ID; the options conflict: ```shell copy -golem plugin get +golem plugin list --account owner@example.com +golem plugin list --account-id 2f6b30d9-bac2-4c67-9d4f-12ea89ba2211 ``` -### Register a new plugin +## Get a plugin -Plugins are identified by their name and version. To register a new plugin, use the `golem plugin register` command: +Use the complete name/version identity, optionally scoped to an account: ```shell copy -golem plugin register +golem plugin get +golem plugin get --account owner@example.com ``` -The parameter should point to a **plugin manifest YAML** describing all the properties of the plugin. - -#### The plugin manifest - -The plugin manifest consists of the following required global fields: - -| field name | description | -|------------|-------------| -| `name` | The name of the plugin | -| `version` | The version of the plugin | -| `description` | A short description of the plugin | -| `icon` | Path to the plugin's icon | -| `homepage` | URL to the plugin's homepage | - -The details of the plugin are specified in the `specs` field, which is an object. - -The `specs.type` field specifies the type of the plugin. It can be one of the following: - -- `ComponentTransformer`: The plugin is a component transformer plugin. -- `OplogProcessor`: The plugin is an oplog processor plugin. -- `App`: The plugin is an application plugin. -- `Library`: The plugin is a library plugin. - -The rest of the fields of the `spec` object depend on the plugin type. - -**ComponentTransformer fields** - -| field name | required | description | -|------------|-------------|-------------| -| `providedWitPackage` | No | The path to the WIT file describing the extra provided interfaces the plugin adds | -| `jsonSchema` | No | The path to the JSON schema file describing the plugin's configuration fields | -| `validateUrl`| Yes| URL to the external component transformation service's validate endpoint | -| `transformUrl`| Yes| URL to the external component transformation service's transform endpoint | - -**OplogProcessor fields** - -| field name | required | description | -|------------|-------------|-------------| -| `component` | Yes | The path to the oplog processor component (WASM) | - -**App fields** - -| field name | required | description | -|------------|-------------|-------------| -| `component` | Yes | The path to the application component (WASM) | - -**Library fields** - -| field name | required | description | -|------------|-------------|-------------| -| `component` | Yes | The path to the library component (WASM) | - -### Unregister a plugin - -To unregister an available plugin, use the following command: +Alternatively, use its globally unique ID: ```shell copy -golem plugin unregister --plugin-name --version +golem plugin get --id ``` -## Manage a component's plugins using the app manifest - -The recommended way to apply plugins to components is to define them in the **app manifest**. - -The set of **installed plugins** can be set for each component in the app manifest under the `plugins` key: - -```yaml -components: - my:example: - template: ts - plugins: - - name: component-transformer-1 - version: v1 - parameters: - x: 1 - y: 2 -``` - -The `name` and `version` fields of each installed component must match one of the available components installed to the system with the `golem plugin` commands described above. - -The `parameters` field contains an arbitrary set of key-value pairs, their meaning depending on the actual plugin. - -It is possible to install *multiple plugins* to a component, and the order they are going to be applied is going to match the order of the elements in the app manifest. - -To apply the changes, use `golem deploy`. - -## Manage a component's plugins explicitly - -There are CLI commands to explicitly install, uninstall or update a component's set of installed plugins. These commands may get deprecated in the future in favor of using the app manifest as described above. +`NAME` and `VERSION` must be supplied together. `--id` conflicts with both positional arguments and with `--account`/`--account-id`. A UUID without `--id` is parsed as `NAME`, not as an ID, and is rejected because `VERSION` is missing. -A subset of the registered plugins can be installed for a component using te `golem component plugin` commands. +## Register a plugin -### Get the installed plugins - -To get the list of installed plugins for a component, use the following command: - -```shell copy -golem component plugin get -``` - -### Install a plugin - -To install a plugin for a component, use the following command: - -```shell copy -golem component plugin install --plugin-name --plugin-version --priority [COMPONENT_NAME] -``` - -Many plugins require per-installation **configuration**. These are key-value pairs that can be passed as arguments to the `install` command as `--param `. - -The **priority** is a number that determines the order in which plugins are applied. - -### Uninstall a plugin - -To uninstall a plugin from a component, use the following command: - -```shell copy -golem component plugin uninstall --installation-id [COMPONENT_NAME] -``` - -The **installation ID** is a unique identifier assigned every time a plugin is installed for a component. It is not enough to use the plugin name and version here, because one plugin can be installed multiple times (for exampel with different configuration) for a component. - -### Updating a plugin's priority or configuration - -To update a plugin's priority or set of configuration parameters, use the following command: +Register from a JSON or YAML manifest path, or use `-` to read it from standard input: ```shell copy -golem component plugin update --installation-id --priority --param [...--param ] [COMPONENT_NAME] +golem plugin register ./my-plugin.yaml +cat my-plugin.yaml | golem plugin register - ``` -The **installation ID** is a unique identifier assigned every time a plugin is installed to a given component version, or its configuration has been changed. - -## Manage a project's plugins +Use `--account` or `--account-id` to register for an explicitly selected account. Otherwise the authenticated account is used. -In _Golem Cloud_ it is also possible to install plugins for a **project**. Every component created in the project will get the installed plugins from the project. +The manifest has these fields: -### Get the installed plugins +| Field | Required | Description | +|---|---|---| +| `name` | Yes | Plugin name | +| `version` | Yes | Plugin version | +| `description` | Yes | Short description shown in plugin listings | +| `icon` | Yes | Path to the icon file uploaded with the registration | +| `homepage` | Yes | Plugin homepage URL | +| `specs` | Yes | Type-specific plugin definition | -To get the list of installed plugins for a project, use the following command: +The registry currently accepts oplog-processor plugins. Their `specs` object identifies the component and revision that process agent oplog entries: -```shell copy -golem project plugin get [PROJECT_NAME] +```yaml +name: my-oplog-processor +version: 1.0.0 +description: Exports selected oplog entries +icon: ./plugin.svg +homepage: https://example.com/my-oplog-processor +specs: + type: OplogProcessor + componentId: 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890 + componentRevision: 0 ``` -The `PROJECT_NAME` parameter can be either a project's name (if it is not ambigous), or it can be prefixed by the project's owner account's email address. +## Unregister a plugin -### Install a plugin - -To install a plugin for a project, use the following command: +Unregister with the same two supported identities: ```shell copy -golem project plugin install --plugin-name --plugin-version --priority --param [...--param ] [PROJECT_NAME] +golem plugin unregister +golem plugin unregister --id ``` -Many plugins require per-installation **configuration**. These are key-value pairs that can be passed as arguments to the `install` command as `--param `. - -The **priority** is a number that determines the order in which plugins are applied. +Account scope is valid only with the name/version form. The CLI resolves that natural identity before deleting the plugin by ID. -### Uninstall a plugin +## Install plugins through the app manifest -To uninstall a plugin from a project, use the following command: +Declare plugins on a component or agent in `golem.yaml`: -```shell copy -golem project plugin uninstall --installation-id [COMPONENT_NAME] -``` - -The **installation ID** is a unique identifier assigned every time a plugin is installed for a component. It is not enough to use the plugin name and version here, because one plugin can be installed multiple times (for exampel with different configuration) for a component. +```yaml +components: + my:example: + template: ts + plugins: + - name: golem-otlp-exporter + version: "1.1.5" + parameters: + endpoint: "http://localhost:4318" + signals: "traces,logs" ``` -### Updating a plugin's priority or configuration - -To update a plugin's priority or set of configuration parameters, use the following command: - -```shell copy -golem project plugin update --installation-id --priority --param [...--param ] [PROJECT_NAME] -``` +The `name` and `version` must identify a plugin available to the target environment. `parameters` are plugin-specific key/value pairs. Multiple entries are applied in manifest order. Run `golem deploy` to reconcile the declared installations with the deployed component. -The **installation ID** is a unique identifier assigned every time a plugin is installed to a given component version, or its configuration has been changed. +Manifest configuration is the source of truth; retired `component plugin` and `project plugin` commands are not part of the current CLI. diff --git a/docs/src/content/next/how-to-guides/common/golem-manage-plugins.mdx b/docs/src/content/next/how-to-guides/common/golem-manage-plugins.mdx index c16f3fdd20..30ab4c7aed 100644 --- a/docs/src/content/next/how-to-guides/common/golem-manage-plugins.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-manage-plugins.mdx @@ -145,56 +145,38 @@ environments: ### Listing Available Plugins ```shell -golem plugin list # List all registered plugins +golem plugin list +golem plugin list --account owner@example.com +golem plugin list --account-id 2f6b30d9-bac2-4c67-9d4f-12ea89ba2211 ``` -### Installing a Plugin on a Component (imperative) +With no account option, the list includes plugins owned by the authenticated account and plugins granted to the selected environment. An explicit account lists plugins owned by that account. `--account` and `--account-id` conflict. -```shell -golem component plugin install \ - --component-name my-app:service \ - --plugin-name golem-otlp-exporter \ - --plugin-version "1.1.5" \ - --priority 0 \ - --param endpoint=http://localhost:4318 \ - --param signals=traces,logs -``` - -### Viewing Installed Plugins +### Inspecting and unregistering registry plugins ```shell -golem component plugin get \ - --component-name my-app:service +golem plugin get my-plugin 1.0.0 +golem plugin get --id 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890 +golem plugin unregister my-plugin 1.0.0 +golem plugin unregister --id 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890 ``` -### Updating a Plugin +The name and version form requires both positional values and accepts `--account` or `--account-id`. The `--id` form conflicts with the positional identity and account scope. A positional UUID is a name, not an ID; use `--id` explicitly. -```shell -golem component plugin update \ - --component-name my-app:service \ - --plugin-to-update 0 \ - --priority 1 \ - --param endpoint=https://new-endpoint:4318 -``` - -### Uninstalling a Plugin +Register a plugin from a JSON or YAML manifest, optionally for an explicit account: ```shell -golem component plugin uninstall \ - --component-name my-app:service \ - --plugin-to-update 0 +golem plugin register ./my-plugin.yaml +golem plugin register ./my-plugin.yaml --account owner@example.com ``` -## Declarative vs Imperative - -- **Declarative (golem.yaml)**: Preferred for repeatable setups. Plugins are installed/updated on `golem deploy`. Configuration lives in version control. -- **Imperative (CLI)**: Useful for quick one-off installations, debugging, or environments where the manifest is not available. +The manifest requires `name`, `version`, `description`, `icon`, `homepage`, and `specs`. The registry currently accepts `OplogProcessor` specs, which identify the oplog-processor component with `componentId` and `componentRevision`. -When using `golem deploy`, the manifest is the source of truth — any plugins defined in `golem.yaml` are reconciled with the deployed state. +Plugin installation is declarative. The retired `component plugin` and `project plugin` workflows are not available. Define installations in `golem.yaml`; `golem deploy` reconciles the manifest with deployed state. ## Plugin Priority -When multiple plugins are installed, `priority` determines their execution order. Plugins with **higher priority values are applied first**. Priority is set explicitly via the CLI's `--priority` flag; in `golem.yaml`, the order in the `plugins` list determines priority (first entry = highest priority). +When multiple plugins are installed, the order in the manifest's `plugins` list determines priority (first entry = highest priority). ## Documentation diff --git a/docs/src/content/next/rest-api/account.mdx b/docs/src/content/next/rest-api/account.mdx index 2a0159d742..4969928143 100644 --- a/docs/src/content/next/rest-api/account.mdx +++ b/docs/src/content/next/rest-api/account.mdx @@ -1,14 +1,11 @@ # Account API The account API allows users to query and manipulate their own account data. ## Create a new account. The response is the created account data. + Path|Method|Protected ---|---|--- `/v1/accounts`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -37,16 +34,11 @@ Path|Method|Protected ``` ## Retrieve an account for a given Account ID + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -64,20 +56,17 @@ Path|Method|Protected ``` ## Delete an account. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - **Example Response JSON** ```json copy @@ -85,6 +74,7 @@ current_revision|integer|Yes|- ``` ## Update account + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}`|PATCH|Yes @@ -96,8 +86,6 @@ The account email is immutable after account creation. Changing the planId is not allowed and the request will be rejected. The response is the updated account data. - - **Example Request JSON** ```json copy { @@ -122,16 +110,33 @@ The response is the updated account data. } ``` -## Get an account's plan +## Retrieve an account by email address. + Path|Method|Protected ---|---|--- -`/v1/accounts/{account_id}/plan`|GET|Yes - - +`/v1/accounts/by-email/{account_email}`|GET|Yes +**Example Response JSON** +```json copy +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "revision": 0, + "name": "string", + "email": "string", + "planId": "b3f60ba2-c1fd-4b3a-a23d-8e876e0ef75d", + "roles": [ + "admin" + ], + "accountRootCardId": "dc9e74a3-7f70-412c-9cf0-c0704225960b" +} +``` +## Get an account's plan +Path|Method|Protected +---|---|--- +`/v1/accounts/{account_id}/plan`|GET|Yes **Example Response JSON** @@ -166,14 +171,11 @@ Path|Method|Protected ``` ## Set the plan of an account + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/plan`|PUT|Yes - - - - **Example Request JSON** ```json copy { @@ -200,16 +202,11 @@ Path|Method|Protected ## List all tokens of an account. The format of each element is the same as the data object in the oauth2 endpoint's response. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/tokens`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -226,6 +223,7 @@ Path|Method|Protected ``` ## Create new token + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/tokens`|POST|Yes @@ -234,8 +232,6 @@ Creates a new token with a given expiration date. The response not only contains the token data but also the secret which can be passed as a bearer token to the Authorization header to the Golem Cloud REST API. - - **Example Request JSON** ```json copy { @@ -256,6 +252,7 @@ The response not only contains the token data but also the secret which can be p ``` ## Get account usage for a UTC calendar month. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/usage`|GET|Yes @@ -273,8 +270,6 @@ Name|Type|Required|Description ---|---|---|--- period|string|No|Billing period in YYYY-MM format. Defaults to the current UTC calendar month. - - **Example Response JSON** ```json copy @@ -301,6 +296,7 @@ period|string|No|Billing period in YYYY-MM format. Defaults to the current UTC c ``` ## Get sparse account usage for closed UTC calendar months, newest first. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/usage/history`|GET|Yes @@ -314,8 +310,6 @@ Name|Type|Required|Description ---|---|---|--- last|integer|No|- - - **Example Response JSON** ```json copy @@ -344,16 +338,11 @@ last|integer|No|- ``` ## Get effective storage-per-agent override metadata for an account. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/max-storage-per-agent`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -367,14 +356,11 @@ Path|Method|Protected ``` ## Set a storage-per-agent override for an account. Setting an expiry requires an admin token. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/max-storage-per-agent`|PUT|Yes - - - - **Example Request JSON** ```json copy { @@ -396,16 +382,11 @@ Path|Method|Protected ``` ## Clear a storage-per-agent override for an account. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/max-storage-per-agent`|DELETE|Yes - - - - - - **Example Response JSON** ```json copy @@ -419,16 +400,11 @@ Path|Method|Protected ``` ## Get the effective maximum linear memory per agent. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/max-memory-per-agent`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -442,14 +418,11 @@ Path|Method|Protected ``` ## Set the maximum linear memory per agent. Setting an expiry requires an admin token. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/max-memory-per-agent`|PUT|Yes - - - - **Example Request JSON** ```json copy { @@ -471,16 +444,11 @@ Path|Method|Protected ``` ## Clear the maximum linear memory per-agent override. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/max-memory-per-agent`|DELETE|Yes - - - - - - **Example Response JSON** ```json copy @@ -494,16 +462,11 @@ Path|Method|Protected ``` ## Get the effective monthly memory GB-seconds allowance. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/monthly-memory-gb-seconds`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -517,14 +480,11 @@ Path|Method|Protected ``` ## Set the monthly memory GB-seconds allowance. Setting an expiry requires an admin token. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/monthly-memory-gb-seconds`|PUT|Yes - - - - **Example Request JSON** ```json copy { @@ -546,16 +506,11 @@ Path|Method|Protected ``` ## Clear the monthly memory GB-seconds allowance override. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/resource-overrides/monthly-memory-gb-seconds`|DELETE|Yes - - - - - - **Example Response JSON** ```json copy @@ -569,6 +524,7 @@ Path|Method|Protected ``` ## Create an impersonation token for a target account + Path|Method|Protected ---|---|--- `/v1/admin/impersonate/{account_id}`|POST|Yes @@ -579,8 +535,6 @@ target account, but audit writes (created_by fields) record the admin's account Only users with the `Admin` account role may call this endpoint. - - **Example Request JSON** ```json copy { diff --git a/docs/src/content/next/rest-api/agent-secrets.mdx b/docs/src/content/next/rest-api/agent-secrets.mdx index 28392ae0e4..1e9ed336a9 100644 --- a/docs/src/content/next/rest-api/agent-secrets.mdx +++ b/docs/src/content/next/rest-api/agent-secrets.mdx @@ -1,16 +1,11 @@ # Agent Secrets API ## List all agent secrets of the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/agent-secrets`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -79,14 +74,11 @@ Path|Method|Protected ``` ## Create a new agent secret + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/agent-secrets`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -209,16 +201,86 @@ Path|Method|Protected } ``` -## Get agent secret by id. +## Get an agent secret in an environment by its path segments. + Path|Method|Protected ---|---|--- -`/v1/agent-secrets/{agent_secret_id}`|GET|Yes +`/v1/envs/{environment_id}/agent-secrets/by-path`|GET|Yes +**Query Parameters** +Name|Type|Required|Description +---|---|---|--- +path|array|Yes|- +**Example Response JSON** +```json copy +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "environmentId": "19f5cc2e-7657-437a-9268-83cd3d563563", + "path": [ + "string" + ], + "revision": 0, + "secretType": { + "defs": [ + { + "id": "string", + "name": "string", + "body": { + "kind": "ref", + "value": { + "id": "string", + "metadata": { + "doc": "string", + "aliases": [ + "string" + ], + "examples": [ + "string" + ], + "deprecated": "string", + "role": { + "tag": "multimodal" + } + } + } + } + } + ], + "root": { + "kind": "ref", + "value": { + "id": "string", + "metadata": { + "doc": "string", + "aliases": [ + "string" + ], + "examples": [ + "string" + ], + "deprecated": "string", + "role": { + "tag": "multimodal" + } + } + } + } + }, + "secretValue": { + "kind": "bool", + "value": true + } +} +``` +## Get agent secret by id. +Path|Method|Protected +---|---|--- +`/v1/agent-secrets/{agent_secret_id}`|GET|Yes **Example Response JSON** @@ -284,14 +346,11 @@ Path|Method|Protected ``` ## Update agent secret + Path|Method|Protected ---|---|--- `/v1/agent-secrets/{agent_secret_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { @@ -370,20 +429,17 @@ Path|Method|Protected ``` ## Delete agent secret + Path|Method|Protected ---|---|--- `/v1/agent-secret/{agent_secret_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/agent.mdx b/docs/src/content/next/rest-api/agent.mdx index ffb66bb79c..70ee2e92fa 100644 --- a/docs/src/content/next/rest-api/agent.mdx +++ b/docs/src/content/next/rest-api/agent.mdx @@ -1,14 +1,11 @@ # Agent API API working on agent instances ## undefined + Path|Method|Protected ---|---|--- `/v1/agents/invoke-agent`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -100,6 +97,7 @@ Path|Method|Protected ``` ## Invoke an agent through an attached live streaming session + Path|Method|Protected ---|---|--- `/v1/agents/invoke-agent-session`|GET|Yes @@ -109,21 +107,12 @@ subprotocol. Text frames carry public v1 JSON lifecycle messages, while binary frames carry the public v1 binary envelope. The bearer token is authenticated before the upgrade and authorizes both start and resume. - - - - - - ## undefined + Path|Method|Protected ---|---|--- `/v1/agents/create-agent`|POST|Yes - - - - **Example Request JSON** ```json copy { diff --git a/docs/src/content/next/rest-api/api-deployment.mdx b/docs/src/content/next/rest-api/api-deployment.mdx index 583d886213..333df4ba6e 100644 --- a/docs/src/content/next/rest-api/api-deployment.mdx +++ b/docs/src/content/next/rest-api/api-deployment.mdx @@ -1,16 +1,11 @@ # Api Deployment API ## List http api deployment by domain in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/http-api-deployments`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -45,14 +40,11 @@ Path|Method|Protected ``` ## Create a new api-deployment in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/http-api-deployments`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -106,16 +98,11 @@ Path|Method|Protected ``` ## Get an api-deployment by id + Path|Method|Protected ---|---|--- `/v1/http-api-deployments/{http_api_deployment_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -146,31 +133,23 @@ Path|Method|Protected ``` ## Delete an api-deployment + Path|Method|Protected ---|---|--- `/v1/http-api-deployments/{http_api_deployment_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - - - ## Update an api-deployment + Path|Method|Protected ---|---|--- `/v1/http-api-deployments/{http_api_deployment_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { @@ -224,16 +203,11 @@ Path|Method|Protected ``` ## Get a specific http api deployment revision + Path|Method|Protected ---|---|--- `/v1/http-api-deployment/{http_api_deployment_id}/revisions/{revision}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -264,16 +238,11 @@ Path|Method|Protected ``` ## Get http api deployment by domain in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/http-api-deployments/{domain}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -304,16 +273,11 @@ Path|Method|Protected ``` ## Get http api deployment by domain in the deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/http-api-deployments/{domain}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -344,16 +308,11 @@ Path|Method|Protected ``` ## Get http api deployment by domain in the deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/http-api-deployments`|GET|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/api-domain.mdx b/docs/src/content/next/rest-api/api-domain.mdx index 8792ed7f41..783de900d0 100644 --- a/docs/src/content/next/rest-api/api-domain.mdx +++ b/docs/src/content/next/rest-api/api-domain.mdx @@ -1,16 +1,11 @@ # Api Domain API ## List all domain registrations in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/domain-registrations`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -26,14 +21,11 @@ Path|Method|Protected ``` ## Create a new domain registration in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/domain-registrations`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -51,16 +43,27 @@ Path|Method|Protected } ``` -## Get domain registration by id +## Get a domain registration in an environment by domain. + Path|Method|Protected ---|---|--- -`/v1/domain-registrations/{domain_registration_id}`|GET|Yes - - +`/v1/envs/{environment_id}/domain-registrations/{domain}`|GET|Yes +**Example Response JSON** +```json copy +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "environmentId": "19f5cc2e-7657-437a-9268-83cd3d563563", + "domain": "string" +} +``` +## Get domain registration by id +Path|Method|Protected +---|---|--- +`/v1/domain-registrations/{domain_registration_id}`|GET|Yes **Example Response JSON** @@ -73,17 +76,10 @@ Path|Method|Protected ``` ## Delete domain registration + Path|Method|Protected ---|---|--- `/v1/domain-registrations/{domain_registration_id}`|DELETE|Yes - - - - - - - - ## Api Domain API Errors Status Code|Description|Body ---|---|--- diff --git a/docs/src/content/next/rest-api/api-security.mdx b/docs/src/content/next/rest-api/api-security.mdx index 5bf89c644e..5db4801206 100644 --- a/docs/src/content/next/rest-api/api-security.mdx +++ b/docs/src/content/next/rest-api/api-security.mdx @@ -1,16 +1,11 @@ # Api Security API ## Get all security-schemes of the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/security-schemes`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -35,14 +30,11 @@ Path|Method|Protected ``` ## Create a new security scheme + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/security-schemes`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -78,16 +70,36 @@ Path|Method|Protected } ``` -## Get security scheme +## Get a security scheme in an environment by name. + Path|Method|Protected ---|---|--- -`/v1/security-schemes/{security_scheme_id}`|GET|Yes - - +`/v1/envs/{environment_id}/security-schemes/{security_scheme_name}`|GET|Yes +**Example Response JSON** +```json copy +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "revision": 0, + "name": "string", + "environmentId": "19f5cc2e-7657-437a-9268-83cd3d563563", + "providerType": { + "type": "Gitlab" + }, + "clientId": "string", + "redirectUrl": "string", + "scopes": [ + "string" + ] +} +``` +## Get security scheme +Path|Method|Protected +---|---|--- +`/v1/security-schemes/{security_scheme_id}`|GET|Yes **Example Response JSON** @@ -109,20 +121,17 @@ Path|Method|Protected ``` ## Delete security scheme + Path|Method|Protected ---|---|--- `/v1/security-schemes/{security_scheme_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - **Example Response JSON** ```json copy @@ -143,14 +152,11 @@ current_revision|integer|Yes|- ``` ## Update security scheme + Path|Method|Protected ---|---|--- `/v1/security-schemes/{security_scheme_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { diff --git a/docs/src/content/next/rest-api/application.mdx b/docs/src/content/next/rest-api/application.mdx index 089a5e9847..dae4f83ff4 100644 --- a/docs/src/content/next/rest-api/application.mdx +++ b/docs/src/content/next/rest-api/application.mdx @@ -1,16 +1,11 @@ # Application API ## List all applications in the account + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/apps`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -28,14 +23,11 @@ Path|Method|Protected ``` ## Create an application in the account + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/apps`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -56,16 +48,11 @@ Path|Method|Protected ``` ## Get application in the account by name + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/apps/{application_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -79,16 +66,11 @@ Path|Method|Protected ``` ## Get application by id. + Path|Method|Protected ---|---|--- `/v1/apps/{application_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -102,31 +84,23 @@ Path|Method|Protected ``` ## Update application by id. + Path|Method|Protected ---|---|--- `/v1/apps/{application_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - - - ## Update application by id. + Path|Method|Protected ---|---|--- `/v1/apps/{application_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { diff --git a/docs/src/content/next/rest-api/card.mdx b/docs/src/content/next/rest-api/card.mdx index aa2b4ece73..a197177709 100644 --- a/docs/src/content/next/rest-api/card.mdx +++ b/docs/src/content/next/rest-api/card.mdx @@ -1,12 +1,11 @@ # Card API ## List cards owned by an account. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/cards`|GET|Yes - - **Query Parameters** Name|Type|Required|Description @@ -16,8 +15,6 @@ include_permission_shares|boolean|No|- include_environment_defaults|boolean|No|- include_agent_initials|boolean|No|- - - **Example Response JSON** ```json copy @@ -52,16 +49,11 @@ include_agent_initials|boolean|No|- ``` ## Get a card by id. + Path|Method|Protected ---|---|--- `/v1/cards/{card_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -94,16 +86,11 @@ Path|Method|Protected ``` ## Revoke a card and all of its descendants. + Path|Method|Protected ---|---|--- `/v1/cards/{card_id}`|DELETE|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/component.mdx b/docs/src/content/next/rest-api/component.mdx index 64d373c999..fe0928a8f7 100644 --- a/docs/src/content/next/rest-api/component.mdx +++ b/docs/src/content/next/rest-api/component.mdx @@ -1,16 +1,11 @@ # Component API ## List all components in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/components`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -91,14 +86,13 @@ Path|Method|Protected ``` ## Create a new component in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/components`|POST|Yes The request body is encoded as multipart/form-data containing metadata and the WASM binary. - - **Request Form**: `multipart/form-data` > Make sure to include `Content-Type: multipart/form-data` Header @@ -194,16 +188,11 @@ The request body is encoded as multipart/form-data containing metadata and the W ``` ## Get a component in the environment by name + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/components/{component_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -280,16 +269,11 @@ Path|Method|Protected ``` ## List all components in a specific deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/components`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -370,16 +354,11 @@ Path|Method|Protected ``` ## Get component in a deployment by name + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/components/{component_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -456,16 +435,11 @@ Path|Method|Protected ``` ## Get a component by id + Path|Method|Protected ---|---|--- `/v1/components/{component_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -542,31 +516,25 @@ Path|Method|Protected ``` ## Delete the component + Path|Method|Protected ---|---|--- `/v1/components/{component_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - - - ## Update a component + Path|Method|Protected ---|---|--- `/v1/components/{component_id}`|PATCH|Yes The request body is encoded as multipart/form-data containing metadata and the WASM binary. - - **Request Form**: `multipart/form-data` > Make sure to include `Content-Type: multipart/form-data` Header @@ -1701,16 +1669,11 @@ The request body is encoded as multipart/form-data containing metadata and the W ``` ## Get specific revision of a component + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/revisions/{revision}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -1787,16 +1750,11 @@ Path|Method|Protected ``` ## Get the component wasm binary of a specific revision + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/revisions/{revision}/wasm`|GET|Yes - - - - - - **Response Body:** `WASM Binary File` ## Component API Errors Status Code|Description|Body diff --git a/docs/src/content/next/rest-api/environment-plugin-grants.mdx b/docs/src/content/next/rest-api/environment-plugin-grants.mdx index ddd5db3e19..be038fca8a 100644 --- a/docs/src/content/next/rest-api/environment-plugin-grants.mdx +++ b/docs/src/content/next/rest-api/environment-plugin-grants.mdx @@ -1,16 +1,11 @@ # Environment Plugin Grants API ## List all environment plugin grants in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/plugins`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -44,14 +39,11 @@ Path|Method|Protected ``` ## Create a new environment plugin grant + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/plugins`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -70,16 +62,11 @@ Path|Method|Protected ``` ## Get environment plugin grant by id + Path|Method|Protected ---|---|--- `/v1/environment-plugins/{environment_plugin_grant_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -109,17 +96,10 @@ Path|Method|Protected ``` ## Delete environment plugin grant + Path|Method|Protected ---|---|--- `/v1/environment-plugins/{environment_plugin_grant_id}`|DELETE|Yes - - - - - - - - ## Environment Plugin Grants API Errors Status Code|Description|Body ---|---|--- diff --git a/docs/src/content/next/rest-api/environment-shares.mdx b/docs/src/content/next/rest-api/environment-shares.mdx index 66f86c79d2..3fd045572a 100644 --- a/docs/src/content/next/rest-api/environment-shares.mdx +++ b/docs/src/content/next/rest-api/environment-shares.mdx @@ -5,12 +5,6 @@ Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/shares`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -34,10 +28,6 @@ Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/shares`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -67,12 +57,6 @@ Path|Method|Protected ---|---|--- `/v1/environment-shares/{environment_share_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -92,16 +76,12 @@ Path|Method|Protected ---|---|--- `/v1/environment-shares/{environment_share_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - **Example Response JSON** ```json copy @@ -121,10 +101,6 @@ Path|Method|Protected ---|---|--- `/v1/environment-shares/{environment_share_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { diff --git a/docs/src/content/next/rest-api/environment-tool-grants.mdx b/docs/src/content/next/rest-api/environment-tool-grants.mdx index 3361546438..a7707dc2ab 100644 --- a/docs/src/content/next/rest-api/environment-tool-grants.mdx +++ b/docs/src/content/next/rest-api/environment-tool-grants.mdx @@ -1,16 +1,11 @@ # Environment Tool Grants API ## List active tool grants in an environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/tools`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -451,14 +446,11 @@ Path|Method|Protected ``` ## Grant an exact published tool release to an environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/tools`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -943,14 +935,11 @@ Path|Method|Protected ``` ## Validate environment tool grants and publications without changing them + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/tools/validate`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -1425,16 +1414,11 @@ Path|Method|Protected ``` ## Get an active environment tool grant + Path|Method|Protected ---|---|--- `/v1/environment-tools/{grant_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -1908,14 +1892,11 @@ Path|Method|Protected ``` ## Delete an environment tool grant + Path|Method|Protected ---|---|--- `/v1/environment-tools/{grant_id}`|DELETE|Yes - - - - **Example Request JSON** ```json copy { @@ -1923,19 +1904,12 @@ Path|Method|Protected } ``` - - ## Restore a deleted environment tool grant + Path|Method|Protected ---|---|--- `/v1/environment-tools/{grant_id}/restore`|POST|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/environment.mdx b/docs/src/content/next/rest-api/environment.mdx index 5c1dabc0c9..d7776bdad4 100644 --- a/docs/src/content/next/rest-api/environment.mdx +++ b/docs/src/content/next/rest-api/environment.mdx @@ -1,14 +1,11 @@ # Environment API ## Upload a content-addressed initial agent file for deployment in this environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/initial-agent-files`|POST|Yes - - - - **Request Form**: `multipart/form-data` > Make sure to include `Content-Type: multipart/form-data` Header @@ -25,16 +22,11 @@ Path|Method|Protected ``` ## List all application environments + Path|Method|Protected ---|---|--- `/v1/apps/{application_id}/envs`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -64,14 +56,11 @@ Path|Method|Protected ``` ## Create an application environment + Path|Method|Protected ---|---|--- `/v1/apps/{application_id}/envs`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -107,16 +96,11 @@ Path|Method|Protected ``` ## Get application environment by name + Path|Method|Protected ---|---|--- `/v1/apps/{application_id}/envs/{environment_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -142,16 +126,11 @@ Path|Method|Protected ``` ## Get environment by id. + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -177,31 +156,23 @@ Path|Method|Protected ``` ## Delete environment by id. + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - - - ## Update environment by id. + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { @@ -238,16 +209,11 @@ Path|Method|Protected ``` ## Get the current deployment plan + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/plan`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -291,14 +257,11 @@ Path|Method|Protected ``` ## Rollback an environment to a previous deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/current-deployment`|PUT|Yes - - - - **Example Request JSON** ```json copy { @@ -321,20 +284,17 @@ Path|Method|Protected ``` ## List all deployments in this environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments`|GET|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- version|string|No|- - - **Example Response JSON** ```json copy @@ -351,14 +311,11 @@ version|string|No|- ``` ## Deploy the current staging area of this environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -425,16 +382,11 @@ Path|Method|Protected ``` ## Get the deployment summary of a deployed deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_id}/summary`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -478,16 +430,11 @@ Path|Method|Protected ``` ## List all registered agent types in a deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_id}/agent-types`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -943,16 +890,11 @@ Path|Method|Protected ``` ## Get a registered agent type in a deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_id}/agent-types/{agent_type_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -1406,16 +1348,11 @@ Path|Method|Protected ``` ## List all registered tools in a deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_id}/tools`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -1873,16 +1810,11 @@ Path|Method|Protected ``` ## Get a registered tool in a deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_id}/tools/{tool_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/http-api-definition.mdx b/docs/src/content/next/rest-api/http-api-definition.mdx index 1dde2be064..b287a65b74 100644 --- a/docs/src/content/next/rest-api/http-api-definition.mdx +++ b/docs/src/content/next/rest-api/http-api-definition.mdx @@ -5,12 +5,6 @@ Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/http-api-definitions`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -49,10 +43,6 @@ Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/http-api-definitions`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -109,12 +99,6 @@ Path|Method|Protected ---|---|--- `/v1/http-api-definitions/{http_api_definition_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -149,27 +133,17 @@ Path|Method|Protected ---|---|--- `/v1/http-api-definitions/{http_api_definition_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - - - ## Update http api definition Path|Method|Protected ---|---|--- `/v1/http-api-definitions/{http_api_definition_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { @@ -226,12 +200,6 @@ Path|Method|Protected ---|---|--- `/v1/http-api-definitions/{http_api_definition_id}/revisions/{revision}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -266,12 +234,6 @@ Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/http-api-definitions/{http_api_definition_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -306,12 +268,6 @@ Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/http-api-definitions/{http_api_definition_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -346,12 +302,6 @@ Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/http-api-definitions/{http_api_definition_name}/openapi`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -363,12 +313,6 @@ Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/http-api-definitions`|GET|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/login.mdx b/docs/src/content/next/rest-api/login.mdx index c26b1ff800..d677bfcc6f 100644 --- a/docs/src/content/next/rest-api/login.mdx +++ b/docs/src/content/next/rest-api/login.mdx @@ -1,6 +1,7 @@ # Login API The login endpoints are implementing an OAuth2 flow. ## Acquire token with OAuth2 authorization + Path|Method|Protected ---|---|--- `/v1/login/oauth2`|POST|No @@ -20,8 +21,6 @@ Name|Type|Required|Description provider|#/components/schemas/OAuth2Provider|Yes|Currently only `github` is supported. access-token|string|Yes|OAuth2 access token - - **Example Response JSON** ```json copy @@ -35,6 +34,7 @@ access-token|string|Yes|OAuth2 access token ``` ## Initiate OAuth2 Web Flow + Path|Method|Protected ---|---|--- `/v1/login/oauth2/web/authorize`|POST|No @@ -49,8 +49,6 @@ browser-based frontends. poll endpoint with the returned state id to retrieve the token once available. Intended for CLI tools and headless environments. - - **Example Request JSON** ```json copy { @@ -70,6 +68,7 @@ Intended for CLI tools and headless environments. ``` ## OAuth2 Web Flow callback + Path|Method|Protected ---|---|--- `/v1/login/oauth2/web/callback`|GET|No @@ -84,11 +83,8 @@ Name|Type|Required|Description code|string|Yes|The authorization code returned by GitHub state|string|Yes|The state parameter for CSRF protection - - - - ## Poll for OAuth2 Web Flow token + Path|Method|Protected ---|---|--- `/v1/login/oauth2/web/poll`|GET|No @@ -102,8 +98,6 @@ Name|Type|Required|Description ---|---|---|--- state|string|Yes|The state parameter for identifying the session - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/mcp-deployment.mdx b/docs/src/content/next/rest-api/mcp-deployment.mdx index 7cefd220c1..0f64a9d2da 100644 --- a/docs/src/content/next/rest-api/mcp-deployment.mdx +++ b/docs/src/content/next/rest-api/mcp-deployment.mdx @@ -1,16 +1,11 @@ # Mcp Deployment API ## List MCP deployments in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/mcp-deployments`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -37,14 +32,11 @@ Path|Method|Protected ``` ## Create a new MCP deployment in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/mcp-deployments`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -82,16 +74,11 @@ Path|Method|Protected ``` ## Get MCP deployment by domain in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/mcp-deployments/{domain}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -114,16 +101,11 @@ Path|Method|Protected ``` ## Get MCP deployment by ID + Path|Method|Protected ---|---|--- `/v1/mcp-deployments/{mcp_deployment_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -146,31 +128,23 @@ Path|Method|Protected ``` ## Delete MCP deployment + Path|Method|Protected ---|---|--- `/v1/mcp-deployments/{mcp_deployment_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - - - ## Update MCP deployment + Path|Method|Protected ---|---|--- `/v1/mcp-deployments/{mcp_deployment_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { @@ -208,16 +182,11 @@ Path|Method|Protected ``` ## Get a specific MCP deployment revision + Path|Method|Protected ---|---|--- `/v1/mcp-deployment/{mcp_deployment_id}/revisions/{revision}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -240,16 +209,11 @@ Path|Method|Protected ``` ## Get MCP deployment by domain in the deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/mcp-deployments/{domain}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -272,16 +236,11 @@ Path|Method|Protected ``` ## List MCP deployments by domain in the deployment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/deployments/{deployment_revision}/mcp-deployments`|GET|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/me.mdx b/docs/src/content/next/rest-api/me.mdx index d6348df614..458fff1aab 100644 --- a/docs/src/content/next/rest-api/me.mdx +++ b/docs/src/content/next/rest-api/me.mdx @@ -2,16 +2,11 @@ ## Gets information about the current token. The JSON is the same as the data object in the oauth2 endpoint's response. + Path|Method|Protected ---|---|--- `/v1/me/token`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -24,12 +19,11 @@ Path|Method|Protected ``` ## List all environments that are visible to the current user, either directly or through shares. + Path|Method|Protected ---|---|--- `/v1/me/visible-environments`|GET|Yes - - **Query Parameters** Name|Type|Required|Description @@ -38,8 +32,6 @@ account_email|string|No|- app_name|string|No|- env_name|string|No|- - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/permission-shares.mdx b/docs/src/content/next/rest-api/permission-shares.mdx index b97775b18d..087cf652b0 100644 --- a/docs/src/content/next/rest-api/permission-shares.mdx +++ b/docs/src/content/next/rest-api/permission-shares.mdx @@ -1,16 +1,11 @@ # Permission Shares API ## List permission shares owned by an account. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/permission-shares`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -43,14 +38,11 @@ Path|Method|Protected ``` ## Create a new permission share owned by an account. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/permission-shares`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -101,16 +93,11 @@ Path|Method|Protected ``` ## List permission shares targeting an account. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/received-permission-shares`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -143,16 +130,11 @@ Path|Method|Protected ``` ## Get permission share by id. + Path|Method|Protected ---|---|--- `/v1/permission-shares/{permission_share_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -181,20 +163,17 @@ Path|Method|Protected ``` ## Delete permission share. + Path|Method|Protected ---|---|--- `/v1/permission-shares/{permission_share_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - **Example Response JSON** ```json copy @@ -223,14 +202,11 @@ current_revision|integer|Yes|- ``` ## Update permission share data. + Path|Method|Protected ---|---|--- `/v1/permission-shares/{permission_share_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { @@ -281,16 +257,11 @@ Path|Method|Protected ``` ## Get permission share by owner account and name. + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/permission-shares/{name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/plugin.mdx b/docs/src/content/next/rest-api/plugin.mdx index fcfaeab389..1eb45c72c1 100644 --- a/docs/src/content/next/rest-api/plugin.mdx +++ b/docs/src/content/next/rest-api/plugin.mdx @@ -1,16 +1,11 @@ # Plugin API ## List all plugins registered in account + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/plugins`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -35,14 +30,11 @@ Path|Method|Protected ``` ## Register a new plugin + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/plugins`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -78,16 +70,40 @@ Path|Method|Protected } ``` -## Get a plugin by id +## Get an account plugin by name and version. + Path|Method|Protected ---|---|--- -`/v1/plugins/{plugin_id}`|GET|Yes - +`/v1/accounts/{account_id}/plugins/{plugin_name}/{plugin_version}`|GET|Yes +**Example Response JSON** +```json copy +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "accountId": "3d07c219-0a88-45be-9cfc-91e9d095a1e9", + "name": "string", + "version": "string", + "description": "string", + "icon": "string", + "homepage": "string", + "spec": { + "type": "OplogProcessor", + "componentId": "616ccd92-d666-4180-8349-8d125b269fac", + "componentRevision": 0 + } +} +``` +## Get an account plugin by the owner account's email, name and version. +Path|Method|Protected +---|---|--- +`/v1/accounts/by-email/{account_email}/plugins/{plugin_name}/{plugin_version}`|GET|Yes +Authorizes on the plugin permission alone — the same as the account-id form — resolving +the owner account from the email without an `AccountVerb::View` check, so the email and id +account scopes behave identically under granular sharing. **Example Response JSON** @@ -108,16 +124,36 @@ Path|Method|Protected } ``` -## Delete a plugin +## Get a plugin by id + Path|Method|Protected ---|---|--- -`/v1/plugins/{plugin_id}`|DELETE|Yes - - +`/v1/plugins/{plugin_id}`|GET|Yes +**Example Response JSON** +```json copy +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "accountId": "3d07c219-0a88-45be-9cfc-91e9d095a1e9", + "name": "string", + "version": "string", + "description": "string", + "icon": "string", + "homepage": "string", + "spec": { + "type": "OplogProcessor", + "componentId": "616ccd92-d666-4180-8349-8d125b269fac", + "componentRevision": 0 + } +} +``` +## Delete a plugin +Path|Method|Protected +---|---|--- +`/v1/plugins/{plugin_id}`|DELETE|Yes **Example Response JSON** diff --git a/docs/src/content/next/rest-api/resources.mdx b/docs/src/content/next/rest-api/resources.mdx index fc78573d47..dd7dc10bbf 100644 --- a/docs/src/content/next/rest-api/resources.mdx +++ b/docs/src/content/next/rest-api/resources.mdx @@ -1,16 +1,11 @@ # Resources API ## Get all resources defined in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/resources`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -36,14 +31,11 @@ Path|Method|Protected ``` ## Create a new resource in the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/resources`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -81,16 +73,11 @@ Path|Method|Protected ``` ## Get a resource in the environment by name + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/resources/{resource_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -112,16 +99,11 @@ Path|Method|Protected ``` ## Get a resource by id + Path|Method|Protected ---|---|--- `/v1/resources/{resource_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -143,31 +125,23 @@ Path|Method|Protected ``` ## Delete a resource + Path|Method|Protected ---|---|--- `/v1/resources/{resource_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - - - ## Update a resource + Path|Method|Protected ---|---|--- `/v1/resources/{resource_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { @@ -205,16 +179,11 @@ Path|Method|Protected ``` ## Get specific revision of a resource + Path|Method|Protected ---|---|--- `/v1/resources/{resource_id}/revisions/{revision}`|GET|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/retry-policies.mdx b/docs/src/content/next/rest-api/retry-policies.mdx index 6ef1f46734..77a5752334 100644 --- a/docs/src/content/next/rest-api/retry-policies.mdx +++ b/docs/src/content/next/rest-api/retry-policies.mdx @@ -1,16 +1,11 @@ # Retry Policies API ## Get all retry policies of the environment + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/retry-policies`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -30,14 +25,11 @@ Path|Method|Protected ``` ## Create a new retry policy + Path|Method|Protected ---|---|--- `/v1/envs/{environment_id}/retry-policies`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -62,16 +54,31 @@ Path|Method|Protected } ``` -## Get retry policy by id. +## Get a retry policy in an environment by name. + Path|Method|Protected ---|---|--- -`/v1/retry-policies/{retry_policy_id}`|GET|Yes - - +`/v1/envs/{environment_id}/retry-policies/{retry_policy_name}`|GET|Yes +**Example Response JSON** +```json copy +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "environmentId": "19f5cc2e-7657-437a-9268-83cd3d563563", + "name": "string", + "revision": 0, + "priority": 0, + "predicate": {}, + "policy": {} +} +``` +## Get retry policy by id. +Path|Method|Protected +---|---|--- +`/v1/retry-policies/{retry_policy_id}`|GET|Yes **Example Response JSON** @@ -88,20 +95,17 @@ Path|Method|Protected ``` ## Delete retry policy + Path|Method|Protected ---|---|--- `/v1/retry-policies/{retry_policy_id}`|DELETE|Yes - - **Query Parameters** Name|Type|Required|Description ---|---|---|--- current_revision|integer|Yes|- - - **Example Response JSON** ```json copy @@ -117,14 +121,11 @@ current_revision|integer|Yes|- ``` ## Update retry policy + Path|Method|Protected ---|---|--- `/v1/retry-policies/{retry_policy_id}`|PATCH|Yes - - - - **Example Request JSON** ```json copy { diff --git a/docs/src/content/next/rest-api/token.mdx b/docs/src/content/next/rest-api/token.mdx index 53f6847361..47bb2486d1 100644 --- a/docs/src/content/next/rest-api/token.mdx +++ b/docs/src/content/next/rest-api/token.mdx @@ -1,16 +1,11 @@ # Token API The token API allows creating custom access tokens for the Golem Cloud REST API to be used by tools and services. ## Get token by id + Path|Method|Protected ---|---|--- `/v1/tokens/{token_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -23,16 +18,13 @@ Path|Method|Protected ``` ## Delete a token + Path|Method|Protected ---|---|--- `/v1/tokens/{token_id}`|DELETE|Yes Deletes a previously created token given by its identifier. - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/tool-releases.mdx b/docs/src/content/next/rest-api/tool-releases.mdx index 96a8da6cb4..1628036b44 100644 --- a/docs/src/content/next/rest-api/tool-releases.mdx +++ b/docs/src/content/next/rest-api/tool-releases.mdx @@ -1,16 +1,11 @@ # Tool Releases API ## List tool releases owned by an account + Path|Method|Protected ---|---|--- `/v1/accounts/{account_id}/tool-releases`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -476,16 +471,11 @@ Path|Method|Protected ``` ## Get an account-owned tool release by ID + Path|Method|Protected ---|---|--- `/v1/tool-releases/{release_id}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -953,16 +943,11 @@ Path|Method|Protected ``` ## De-publish an account-owned tool release + Path|Method|Protected ---|---|--- `/v1/tool-releases/{release_id}`|DELETE|Yes - - - - - - **Example Response JSON** ```json copy @@ -1430,16 +1415,11 @@ Path|Method|Protected ``` ## Restore a de-published account-owned tool release + Path|Method|Protected ---|---|--- `/v1/tool-releases/{release_id}/restore`|POST|Yes - - - - - - **Example Response JSON** ```json copy diff --git a/docs/src/content/next/rest-api/worker.mdx b/docs/src/content/next/rest-api/worker.mdx index fb424d6068..0c409cda64 100644 --- a/docs/src/content/next/rest-api/worker.mdx +++ b/docs/src/content/next/rest-api/worker.mdx @@ -1,6 +1,7 @@ # Worker API ## Get metadata of multiple workers + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers`|GET|Yes @@ -34,8 +35,6 @@ cursor|string|No|Count of listed values, default: 50 count|integer|No|Position where to start listing, if not provided, starts from the beginning. It is used to get the next page of results. To get next page, use the cursor returned in the response precise|boolean|No|Precision in relation to worker status, if true, calculate the most up-to-date status for each worker, default is false - - **Example Response JSON** ```json copy @@ -163,6 +162,7 @@ precise|boolean|No|Precision in relation to worker status, if true, calculate th ``` ## Launch a new worker. + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers`|POST|Yes @@ -174,8 +174,6 @@ The parameters in the request are the following: - `args` is a list of strings which appear as command line arguments for the worker - `env` is a list of key-value pairs (represented by arrays) which appear as environment variables for the worker - - **Example Request JSON** ```json copy { @@ -201,6 +199,7 @@ The parameters in the request are the following: ``` ## Get metadata of a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}`|GET|Yes @@ -221,10 +220,6 @@ Returns metadata about an existing worker: - `Failed` if the worker failed and there are no more retries scheduled for it - `Exited` if the worker explicitly exited using the exit WASI function - - - - **Example Response JSON** ```json copy @@ -344,16 +339,13 @@ Returns metadata about an existing worker: ``` ## Delete a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}`|DELETE|Yes Interrupts and deletes an existing worker. - - - - **Example Response JSON** ```json copy @@ -361,6 +353,7 @@ Interrupts and deletes an existing worker. ``` ## Complete a promise + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/complete`|POST|Yes @@ -369,8 +362,6 @@ Completes a promise with a given custom array of bytes. The promise must be previously created from within the worker, and it's identifier (a combination of a worker identifier and an oplogIdx ) must be sent out to an external caller so it can use this endpoint to mark the promise completed. The data field is sent back to the worker, and it has no predefined meaning. - - **Example Request JSON** ```json copy { @@ -388,6 +379,7 @@ true ``` ## Interrupt a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/interrupt`|POST|Yes @@ -403,8 +395,6 @@ Name|Type|Required|Description ---|---|---|--- recovery-immediately|boolean|No|if true will simulate a worker recovery. Defaults to false. - - **Example Response JSON** ```json copy @@ -412,6 +402,7 @@ recovery-immediately|boolean|No|if true will simulate a worker recovery. Default ``` ## Advanced search for workers + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/find`|POST|Yes @@ -436,8 +427,6 @@ Returns metadata about an existing component workers: - `workers` list of workers metadata - `cursor` cursor for next request, if cursor is empty/null, there are no other values - - **Example Request JSON** ```json copy { @@ -582,16 +571,11 @@ Returns metadata about an existing component workers: ``` ## Resume a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/resume`|POST|Yes - - - - - - **Example Response JSON** ```json copy @@ -599,14 +583,11 @@ Path|Method|Protected ``` ## Update a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/update`|POST|Yes - - - - **Example Request JSON** ```json copy { @@ -623,12 +604,11 @@ Path|Method|Protected ``` ## Get the oplog of a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/oplog`|GET|Yes - - **Query Parameters** Name|Type|Required|Description @@ -638,8 +618,6 @@ count|integer|Yes|- cursor|#/components/schemas/OplogCursor|No|- query|string|No|- - - **Example Response JSON** ```json copy @@ -757,16 +735,11 @@ query|string|No|- ``` ## List files in a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/files/{file_name}`|GET|Yes - - - - - - **Example Response JSON** ```json copy @@ -784,16 +757,13 @@ Path|Method|Protected ``` ## Get the wallet (active permission cards) of a worker. + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/wallet`|GET|Yes Activates the worker if it is not already active. - - - - **Example Response JSON** ```json copy @@ -828,19 +798,15 @@ Activates the worker if it is not already active. ``` ## Get contents of a file in a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/file-contents/{file_name}`|GET|Yes - - - - - - **Response Body:** `WASM Binary File` ## Activate a plugin + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/activate-plugin`|POST|Yes @@ -853,8 +819,6 @@ Name|Type|Required|Description ---|---|---|--- plugin-priority|integer|Yes|- - - **Example Response JSON** ```json copy @@ -862,6 +826,7 @@ plugin-priority|integer|Yes|- ``` ## Deactivate a plugin + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/deactivate-plugin`|POST|Yes @@ -874,8 +839,6 @@ Name|Type|Required|Description ---|---|---|--- plugin-priority|integer|Yes|- - - **Example Response JSON** ```json copy @@ -883,14 +846,13 @@ plugin-priority|integer|Yes|- ``` ## Revert a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/revert`|POST|Yes Reverts a worker by undoing either the last few invocations or the last few recorded oplog entries. - - **Example Request JSON** ```json copy { @@ -906,14 +868,13 @@ Reverts a worker by undoing either the last few invocations or the last few reco ``` ## Fork a worker + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/fork`|POST|Yes Fork a worker by creating a new worker with the oplog up to the provided index - - **Example Request JSON** ```json copy { @@ -932,16 +893,13 @@ Fork a worker by creating a new worker with the oplog up to the provided index ``` ## Cancels a pending invocation if it has not started yet + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/invocations/{idempotency_key}`|DELETE|Yes The invocation to be cancelled is identified by the idempotency key passed to the invoke API. - - - - **Example Response JSON** ```json copy @@ -951,17 +909,10 @@ The invocation to be cancelled is identified by the idempotency key passed to th ``` ## Connect to a worker using a websocket and stream events + Path|Method|Protected ---|---|--- `/v1/components/{component_id}/workers/{agent_name}/connect`|GET|No - - - - - - - - ## Worker API Errors Status Code|Description|Body ---|---|--- diff --git a/golem-registry-service/src/api/accounts.rs b/golem-registry-service/src/api/accounts.rs index 8119e67e35..b2f5f5eb35 100644 --- a/golem-registry-service/src/api/accounts.rs +++ b/golem-registry-service/src/api/accounts.rs @@ -19,7 +19,8 @@ use crate::services::token::TokenService; use golem_common::model::Empty; use golem_common::model::Page; use golem_common::model::account::{ - Account, AccountCreation, AccountId, AccountRevision, AccountSetPlan, AccountUpdate, + Account, AccountCreation, AccountEmail, AccountId, AccountRevision, AccountSetPlan, + AccountUpdate, }; use golem_common::model::auth::{Token, TokenCreation, TokenWithSecret}; use golem_common::model::plan::Plan; @@ -114,6 +115,41 @@ impl AccountsApi { Ok(Json(result)) } + /// Retrieve an account by email address. + #[oai( + path = "/by-email/:account_email", + method = "get", + operation_id = "get_account_by_email" + )] + async fn get_account_by_email( + &self, + account_email: Path, + token: GolemSecurityScheme, + ) -> ApiResult> { + let record = recorded_http_api_request!( + "get_account_by_email", + account_email = account_email.0.to_string() + ); + let auth = self.auth_service.authenticate_token(token.secret()).await?; + let response = self + .get_account_by_email_internal(account_email.0, auth) + .instrument(record.span.clone()) + .await; + record.result(response) + } + + async fn get_account_by_email_internal( + &self, + account_email: AccountEmail, + auth: AuthCtx, + ) -> ApiResult> { + Ok(Json( + self.account_service + .get_by_email(account_email.as_str(), &auth) + .await?, + )) + } + /// Get an account's plan #[oai( path = "/:account_id/plan", diff --git a/golem-registry-service/src/api/agent_secrets.rs b/golem-registry-service/src/api/agent_secrets.rs index 359f59b6e6..ca7ad80782 100644 --- a/golem-registry-service/src/api/agent_secrets.rs +++ b/golem-registry-service/src/api/agent_secrets.rs @@ -18,6 +18,7 @@ use crate::services::auth::AuthService; use golem_common::model::Page; use golem_common::model::agent_secret::{ AgentSecretDto as DomainAgentSecretDto, AgentSecretId, AgentSecretRevision, + CanonicalAgentSecretPath, }; use golem_common::model::environment::EnvironmentId; use golem_common::model::external_agent_secret::{ @@ -144,6 +145,43 @@ impl AgentSecretsApi { Ok(Json(Page { values: converted })) } + /// Get an agent secret in an environment by its path segments. + #[oai(path = "/envs/:environment_id/agent-secrets/by-path", method = "get", operation_id = "get_environment_agent_secret", tag = ApiTags::Environment)] + async fn get_environment_agent_secret( + &self, + environment_id: Path, + path: Query>, + token: GolemSecurityScheme, + ) -> ApiResult> { + let record = recorded_http_api_request!("get_environment_agent_secret", environment_id = environment_id.0.to_string(), path = ?path.0); + let auth = self.auth_service.authenticate_token(token.secret()).await?; + let response = self + .get_environment_agent_secret_internal( + environment_id.0, + CanonicalAgentSecretPath::from_path_in_unknown_casing(&path.0), + auth, + ) + .instrument(record.span.clone()) + .await; + record.result(response) + } + + async fn get_environment_agent_secret_internal( + &self, + environment_id: EnvironmentId, + path: CanonicalAgentSecretPath, + auth: AuthCtx, + ) -> ApiResult> { + let result = self + .agent_secret_service + .get_in_environment(environment_id, path, &auth) + .await?; + + let result = AgentSecretDto::try_from(DomainAgentSecretDto::from(result)) + .map_err(anyhow::Error::msg)?; + Ok(Json(result)) + } + /// Get agent secret by id. #[oai( path = "/agent-secrets/:agent_secret_id", diff --git a/golem-registry-service/src/api/domain_registrations.rs b/golem-registry-service/src/api/domain_registrations.rs index f2850e711d..baf2d48cb0 100644 --- a/golem-registry-service/src/api/domain_registrations.rs +++ b/golem-registry-service/src/api/domain_registrations.rs @@ -17,7 +17,7 @@ use crate::services::auth::AuthService; use crate::services::domain_registration::DomainRegistrationService; use golem_common::model::Page; use golem_common::model::domain_registration::{ - DomainRegistration, DomainRegistrationCreation, DomainRegistrationId, + Domain, DomainRegistration, DomainRegistrationCreation, DomainRegistrationId, }; use golem_common::model::environment::EnvironmentId; use golem_common::model::poem::NoContentResponse; @@ -135,6 +135,45 @@ impl DomainRegistrationsApi { })) } + /// Get a domain registration in an environment by domain. + #[oai( + path = "/envs/:environment_id/domain-registrations/:domain", + method = "get", + operation_id = "get_environment_domain_registration", + tag = ApiTags::Environment + )] + async fn get_environment_domain_registration( + &self, + environment_id: Path, + domain: Path, + token: GolemSecurityScheme, + ) -> ApiResult> { + let record = recorded_http_api_request!( + "get_environment_domain_registration", + environment_id = environment_id.0.to_string(), + domain = domain.0.to_string() + ); + let auth = self.auth_service.authenticate_token(token.secret()).await?; + let response = self + .get_environment_domain_registration_internal(environment_id.0, domain.0, auth) + .instrument(record.span.clone()) + .await; + record.result(response) + } + + async fn get_environment_domain_registration_internal( + &self, + environment_id: EnvironmentId, + domain: Domain, + auth: AuthCtx, + ) -> ApiResult> { + Ok(Json( + self.domain_registration_service + .get_in_environment_by_domain(environment_id, &domain, &auth) + .await?, + )) + } + /// Get domain registration by id #[oai( path = "/domain-registrations/:domain_registration_id", diff --git a/golem-registry-service/src/api/error.rs b/golem-registry-service/src/api/error.rs index 15249d11c3..6800d906e5 100644 --- a/golem-registry-service/src/api/error.rs +++ b/golem-registry-service/src/api/error.rs @@ -820,7 +820,9 @@ impl From for ApiError { PluginRegistrationError::ParentAccountNotFound(_) => { Self::not_found(api::error_code::ACCOUNT_NOT_FOUND, error) } - PluginRegistrationError::PluginRegistrationNotFound(_) => { + PluginRegistrationError::PluginRegistrationNotFound(_) + | PluginRegistrationError::PluginRegistrationByNameNotFound { .. } + | PluginRegistrationError::PluginRegistrationByEmailNotFound { .. } => { Self::not_found(api::error_code::PLUGIN_REGISTRATION_NOT_FOUND, error) } @@ -1248,7 +1250,8 @@ impl From for ApiError { AgentSecretError::AgentSecretValueDoesNotMatchType { .. } => { Self::bad_request(api::error_code::AGENT_SECRET_VALUE_TYPE_MISMATCH, error) } - AgentSecretError::AgentSecretNotFound(_) => { + AgentSecretError::AgentSecretNotFound(_) + | AgentSecretError::AgentSecretByPathNotFound { .. } => { Self::not_found(api::error_code::AGENT_SECRET_NOT_FOUND, error) } AgentSecretError::ParentEnvironmentNotFound(_) => { @@ -1280,7 +1283,8 @@ impl From for ApiError { RetryPolicyError::RetryPolicyForNameAlreadyExists { .. } => { Self::conflict(api::error_code::RETRY_POLICY_ALREADY_EXISTS, error) } - RetryPolicyError::RetryPolicyNotFound(_) => { + RetryPolicyError::RetryPolicyNotFound(_) + | RetryPolicyError::RetryPolicyByNameNotFound { .. } => { Self::not_found(api::error_code::RETRY_POLICY_NOT_FOUND, error) } RetryPolicyError::ParentEnvironmentNotFound(_) => { diff --git a/golem-registry-service/src/api/plugin_registrations.rs b/golem-registry-service/src/api/plugin_registrations.rs index 2e54cb586f..3ce59a3e93 100644 --- a/golem-registry-service/src/api/plugin_registrations.rs +++ b/golem-registry-service/src/api/plugin_registrations.rs @@ -16,7 +16,7 @@ use super::ApiResult; use crate::services::auth::AuthService; use crate::services::plugin_registration::PluginRegistrationService; use golem_common::model::Page; -use golem_common::model::account::AccountId; +use golem_common::model::account::{AccountEmail, AccountId}; use golem_common::model::plugin_registration::{ PluginRegistrationCreation, PluginRegistrationDto, PluginRegistrationId, }; @@ -137,6 +137,106 @@ impl PluginRegistrationsApi { })) } + /// Get an account plugin by name and version. + #[oai( + path = "/accounts/:account_id/plugins/:plugin_name/:plugin_version", + method = "get", + operation_id = "get_account_plugin", + tag = ApiTags::Account + )] + async fn get_account_plugin( + &self, + account_id: Path, + plugin_name: Path, + plugin_version: Path, + token: GolemSecurityScheme, + ) -> ApiResult> { + let record = recorded_http_api_request!( + "get_account_plugin", + account_id = account_id.0.to_string(), + plugin_name = plugin_name.0.clone(), + plugin_version = plugin_version.0.clone() + ); + let auth = self.auth_service.authenticate_token(token.secret()).await?; + let response = self + .get_account_plugin_internal(account_id.0, plugin_name.0, plugin_version.0, auth) + .instrument(record.span.clone()) + .await; + record.result(response) + } + + async fn get_account_plugin_internal( + &self, + account_id: AccountId, + plugin_name: String, + plugin_version: String, + auth: AuthCtx, + ) -> ApiResult> { + Ok(Json( + self.plugin_registration_service + .get_account_plugin(account_id, &plugin_name, &plugin_version, &auth) + .await? + .into(), + )) + } + + /// Get an account plugin by the owner account's email, name and version. + /// + /// Authorizes on the plugin permission alone — the same as the account-id form — resolving + /// the owner account from the email without an `AccountVerb::View` check, so the email and id + /// account scopes behave identically under granular sharing. + #[oai( + path = "/accounts/by-email/:account_email/plugins/:plugin_name/:plugin_version", + method = "get", + operation_id = "get_account_plugin_by_email", + tag = ApiTags::Account + )] + async fn get_account_plugin_by_email( + &self, + account_email: Path, + plugin_name: Path, + plugin_version: Path, + token: GolemSecurityScheme, + ) -> ApiResult> { + let record = recorded_http_api_request!( + "get_account_plugin_by_email", + account_email = account_email.0.to_string(), + plugin_name = plugin_name.0.clone(), + plugin_version = plugin_version.0.clone() + ); + let auth = self.auth_service.authenticate_token(token.secret()).await?; + let response = self + .get_account_plugin_by_email_internal( + account_email.0, + plugin_name.0, + plugin_version.0, + auth, + ) + .instrument(record.span.clone()) + .await; + record.result(response) + } + + async fn get_account_plugin_by_email_internal( + &self, + account_email: AccountEmail, + plugin_name: String, + plugin_version: String, + auth: AuthCtx, + ) -> ApiResult> { + Ok(Json( + self.plugin_registration_service + .get_account_plugin_by_email( + account_email.as_str(), + &plugin_name, + &plugin_version, + &auth, + ) + .await? + .into(), + )) + } + /// Get a plugin by id #[oai( path = "/plugins/:plugin_id", diff --git a/golem-registry-service/src/api/retry_policies.rs b/golem-registry-service/src/api/retry_policies.rs index baa5537f8c..150c3d0899 100644 --- a/golem-registry-service/src/api/retry_policies.rs +++ b/golem-registry-service/src/api/retry_policies.rs @@ -138,6 +138,40 @@ impl RetryPoliciesApi { Ok(Json(Page { values: converted })) } + /// Get a retry policy in an environment by name. + #[oai(path = "/envs/:environment_id/retry-policies/:retry_policy_name", method = "get", operation_id = "get_environment_retry_policy", tag = ApiTags::Environment)] + async fn get_environment_retry_policy( + &self, + environment_id: Path, + retry_policy_name: Path, + token: GolemSecurityScheme, + ) -> ApiResult> { + let record = recorded_http_api_request!( + "get_environment_retry_policy", + environment_id = environment_id.0.to_string(), + retry_policy_name = retry_policy_name.0.clone() + ); + let auth = self.auth_service.authenticate_token(token.secret()).await?; + let response = self + .get_environment_retry_policy_internal(environment_id.0, retry_policy_name.0, auth) + .instrument(record.span.clone()) + .await; + record.result(response) + } + + async fn get_environment_retry_policy_internal( + &self, + environment_id: EnvironmentId, + retry_policy_name: String, + auth: AuthCtx, + ) -> ApiResult> { + Ok(Json(to_dto( + self.retry_policy_service + .get_in_environment(environment_id, &retry_policy_name, &auth) + .await?, + )?)) + } + /// Get retry policy by id. #[oai( path = "/retry-policies/:retry_policy_id", diff --git a/golem-registry-service/src/api/security_schemes.rs b/golem-registry-service/src/api/security_schemes.rs index ddeed3f1b9..721dbec26e 100644 --- a/golem-registry-service/src/api/security_schemes.rs +++ b/golem-registry-service/src/api/security_schemes.rs @@ -18,8 +18,8 @@ use crate::services::security_scheme::SecuritySchemeService; use golem_common::model::Page; use golem_common::model::environment::EnvironmentId; use golem_common::model::security_scheme::{ - SecuritySchemeCreation, SecuritySchemeDto, SecuritySchemeId, SecuritySchemeRevision, - SecuritySchemeUpdate, + SecuritySchemeCreation, SecuritySchemeDto, SecuritySchemeId, SecuritySchemeName, + SecuritySchemeRevision, SecuritySchemeUpdate, }; use golem_common::recorded_http_api_request; use golem_service_base::api_tags::ApiTags; @@ -136,6 +136,50 @@ impl SecuritySchemesApi { })) } + /// Get a security scheme in an environment by name. + #[oai( + path = "/envs/:environment_id/security-schemes/:security_scheme_name", + method = "get", + operation_id = "get_environment_security_scheme", + tag = ApiTags::Environment + )] + async fn get_environment_security_scheme( + &self, + environment_id: Path, + security_scheme_name: Path, + token: GolemSecurityScheme, + ) -> ApiResult> { + let record = recorded_http_api_request!( + "get_environment_security_scheme", + environment_id = environment_id.0.to_string(), + security_scheme_name = security_scheme_name.0.to_string() + ); + let auth = self.auth_service.authenticate_token(token.secret()).await?; + let response = self + .get_environment_security_scheme_internal( + environment_id.0, + security_scheme_name.0, + auth, + ) + .instrument(record.span.clone()) + .await; + record.result(response) + } + + async fn get_environment_security_scheme_internal( + &self, + environment_id: EnvironmentId, + security_scheme_name: SecuritySchemeName, + auth: AuthCtx, + ) -> ApiResult> { + Ok(Json( + self.security_scheme_service + .get_in_environment(environment_id, &security_scheme_name, &auth) + .await? + .into(), + )) + } + /// Get security scheme #[oai( path = "/security-schemes/:security_scheme_id", diff --git a/golem-registry-service/src/repo/agent_secret.rs b/golem-registry-service/src/repo/agent_secret.rs index a68ba304a5..cb559d44e2 100644 --- a/golem-registry-service/src/repo/agent_secret.rs +++ b/golem-registry-service/src/repo/agent_secret.rs @@ -60,6 +60,12 @@ pub trait AgentSecretRepo: Send + Sync { environment_id: Uuid, ) -> Result, AgentSecretRepoError>; + async fn get_for_environment_and_path( + &self, + environment_id: Uuid, + path: Vec, + ) -> Result, AgentSecretRepoError>; + /// Gets a stored revision by identity. When `include_deleted` is true, soft-deleted /// secrets and parent environment/application/account records are still considered; /// deleted revision records themselves are never returned. @@ -142,6 +148,17 @@ impl AgentSecretRepo for LoggedAgentSecretRepo { .await } + async fn get_for_environment_and_path( + &self, + environment_id: Uuid, + path: Vec, + ) -> Result, AgentSecretRepoError> { + self.repo + .get_for_environment_and_path(environment_id, path) + .instrument(Self::span_environment_id(environment_id)) + .await + } + async fn get_revision( &self, environment_id: Uuid, @@ -410,6 +427,19 @@ impl AgentSecretRepo for DbAgentSecretRepo { Ok(results) } + async fn get_for_environment_and_path( + &self, + environment_id: Uuid, + path: Vec, + ) -> Result, AgentSecretRepoError> { + Ok(self.with_ro("get_for_environment_and_path").fetch_optional_as(sqlx::query_as(indoc! {r#" + SELECT sec.environment_id, sec.path, sec.agent_secret_data, sec.created_at AS entity_created_at, secr.agent_secret_id, secr.revision_id, secr.agent_secret_revision_data, secr.created_at, secr.created_by, secr.deleted + FROM agent_secrets sec + JOIN agent_secret_revisions secr ON secr.agent_secret_id = sec.agent_secret_id AND secr.revision_id = sec.current_revision_id + WHERE sec.environment_id = $1 AND sec.path = $2 AND sec.deleted_at IS NULL + "#}).bind(environment_id).bind(sqlx::types::Json(path))).await?) + } + async fn get_revision( &self, environment_id: Uuid, diff --git a/golem-registry-service/src/repo/plugin.rs b/golem-registry-service/src/repo/plugin.rs index afff05b603..09c205b80c 100644 --- a/golem-registry-service/src/repo/plugin.rs +++ b/golem-registry-service/src/repo/plugin.rs @@ -24,7 +24,7 @@ pub trait PluginRepo: Send + Sync { account_id: Uuid, name: &str, version: &str, - ) -> RepoResult>; + ) -> RepoResult>; async fn list_by_account(&self, account_id: Uuid) -> RepoResult>; } @@ -79,7 +79,7 @@ impl PluginRepo for LoggedPluginRepo { account_id: Uuid, name: &str, version: &str, - ) -> RepoResult> { + ) -> RepoResult> { self.repo .get_by_name_and_version(account_id, name, version) .instrument(Self::span_name_and_version(name, version)) @@ -222,7 +222,7 @@ impl PluginRepo for DbPluginRepo { account_id: Uuid, name: &str, version: &str, - ) -> RepoResult> { + ) -> RepoResult> { self.with_ro("get_by_name_and_version") .fetch_optional_as( sqlx::query_as(indoc! {r#" @@ -233,12 +233,16 @@ impl PluginRepo for DbPluginRepo { p.provided_wit_package, p.json_schema, p.validate_url, p.transform_url, p.component_id, p.component_revision_id, - p.wasm_content_hash - FROM plugins p + p.wasm_content_hash, + a.email AS account_email + FROM accounts a + JOIN plugins p + ON p.account_id = a.account_id WHERE p.account_id = $1 AND p.name = $2 AND p.version = $3 + AND a.deleted_at IS NULL AND p.deleted_at IS NULL "#}) .bind(account_id) diff --git a/golem-registry-service/src/repo/retry_policy.rs b/golem-registry-service/src/repo/retry_policy.rs index 8aa216a3bf..3c9a718b8f 100644 --- a/golem-registry-service/src/repo/retry_policy.rs +++ b/golem-registry-service/src/repo/retry_policy.rs @@ -58,6 +58,12 @@ pub trait RetryPolicyRepo: Send + Sync { &self, environment_id: Uuid, ) -> Result, RetryPolicyRepoError>; + + async fn get_for_environment_and_name( + &self, + environment_id: Uuid, + name: &str, + ) -> Result, RetryPolicyRepoError>; } pub struct LoggedRetryPolicyRepo { @@ -128,6 +134,17 @@ impl RetryPolicyRepo for LoggedRetryPolicyRepo { .instrument(Self::span_environment_id(environment_id)) .await } + + async fn get_for_environment_and_name( + &self, + environment_id: Uuid, + name: &str, + ) -> Result, RetryPolicyRepoError> { + self.repo + .get_for_environment_and_name(environment_id, name) + .instrument(Self::span_environment_id(environment_id)) + .await + } } pub struct DbRetryPolicyRepo { @@ -374,4 +391,17 @@ impl RetryPolicyRepo for DbRetryPolicyRepo { Ok(results) } + + async fn get_for_environment_and_name( + &self, + environment_id: Uuid, + name: &str, + ) -> Result, RetryPolicyRepoError> { + Ok(self.with_ro("get_for_environment_and_name").fetch_optional_as(sqlx::query_as(indoc! {r#" + SELECT rp.environment_id, rp.name, rp.created_at AS entity_created_at, rev.retry_policy_id, rev.revision_id, rev.priority, rev.predicate_json, rev.policy_json, rev.created_at, rev.created_by, rev.deleted + FROM retry_policies rp + JOIN retry_policy_revisions rev ON rev.retry_policy_id = rp.retry_policy_id AND rev.revision_id = rp.current_revision_id + WHERE rp.environment_id = $1 AND rp.name = $2 AND rp.deleted_at IS NULL + "#}).bind(environment_id).bind(name)).await?) + } } diff --git a/golem-registry-service/src/services/account/mod.rs b/golem-registry-service/src/services/account/mod.rs index cc99e01e37..5e4b1fbece 100644 --- a/golem-registry-service/src/services/account/mod.rs +++ b/golem-registry-service/src/services/account/mod.rs @@ -309,6 +309,29 @@ impl AccountService { Ok(account) } + /// Resolves an account email to its id **without** enforcing `AccountVerb::View`. + /// + /// For feeding a subsequently-authorized resource lookup (e.g. the by-email plugin + /// endpoints) so that a caller holding only a resource grant is not additionally required + /// to have account-view. The full-account endpoint ([`Self::get_by_email`]) keeps its + /// `AccountVerb::View` gate. Only ever use the returned id to build a resource + /// authorization that is then checked; never expose it or other account data unchecked. + pub async fn resolve_account_id_by_email_unchecked( + &self, + account_email: &str, + ) -> Result { + let account: Account = self + .account_repo + .get_by_email(account_email) + .await? + .ok_or(AccountError::AccountByEmailNotFound( + account_email.to_string(), + ))? + .try_into()?; + + Ok(account.id) + } + async fn create_internal( &self, id: AccountId, diff --git a/golem-registry-service/src/services/agent_secret.rs b/golem-registry-service/src/services/agent_secret.rs index d9f763f62f..dbfe3aae96 100644 --- a/golem-registry-service/src/services/agent_secret.rs +++ b/golem-registry-service/src/services/agent_secret.rs @@ -55,6 +55,11 @@ pub enum AgentSecretError { ParentEnvironmentNotFound(EnvironmentId), #[error("Agent secret {0} not found")] AgentSecretNotFound(AgentSecretId), + #[error("Agent secret for path {path} not found in environment {environment_id}")] + AgentSecretByPathNotFound { + environment_id: EnvironmentId, + path: CanonicalAgentSecretPath, + }, #[error("Concurrent update attempt")] ConcurrentModification, #[error(transparent)] @@ -344,6 +349,7 @@ impl SafeDisplay for AgentSecretError { Self::AgentSecretForPathAlreadyExists { .. } => self.to_string(), Self::ParentEnvironmentNotFound(_) => self.to_string(), Self::AgentSecretNotFound(_) => self.to_string(), + Self::AgentSecretByPathNotFound { .. } => self.to_string(), Self::ConcurrentModification => self.to_string(), Self::Unauthorized(inner) => inner.to_safe_string(), Self::InternalError(_) => "Internal error".to_string(), @@ -557,6 +563,43 @@ impl AgentSecretService { Ok(agent_secret) } + pub async fn get_in_environment( + &self, + environment_id: EnvironmentId, + path: CanonicalAgentSecretPath, + auth: &AuthCtx, + ) -> Result { + let owner = self + .environment_service + .get_owner_unchecked(environment_id) + .await + .map_err(|err| match err { + EnvironmentError::EnvironmentNotFound(_) => { + AgentSecretError::ParentEnvironmentNotFound(environment_id) + } + other => other.into(), + })?; + authorize_agent_secret_permission_for_owner( + auth, + owner, + Some(&path), + EnvironmentAgentSecretVerb::View, + ) + .map_err(|_| AgentSecretError::AgentSecretByPathNotFound { + environment_id, + path: path.clone(), + })?; + self.agent_secret_repo + .get_for_environment_and_path(environment_id.0, path.0.clone()) + .await? + .ok_or(AgentSecretError::AgentSecretByPathNotFound { + environment_id, + path, + })? + .try_into() + .map_err(Into::into) + } + pub async fn list_in_environment( &self, environment_id: EnvironmentId, diff --git a/golem-registry-service/src/services/builtin_plugin_provisioner.rs b/golem-registry-service/src/services/builtin_plugin_provisioner.rs index 207dfbc84c..834f0886dd 100644 --- a/golem-registry-service/src/services/builtin_plugin_provisioner.rs +++ b/golem-registry-service/src/services/builtin_plugin_provisioner.rs @@ -366,7 +366,8 @@ async fn register_plugin( })? .ok_or_else(|| { anyhow::anyhow!("Plugin '{plugin_name}' exists but could not be loaded") - })?; + })? + .plugin; Ok(()) } Err(other) => Err(anyhow::anyhow!( diff --git a/golem-registry-service/src/services/domain_registration/mod.rs b/golem-registry-service/src/services/domain_registration/mod.rs index 4b2c44e7ae..602e036200 100644 --- a/golem-registry-service/src/services/domain_registration/mod.rs +++ b/golem-registry-service/src/services/domain_registration/mod.rs @@ -194,6 +194,42 @@ impl DomainRegistrationService { Ok(domain_registration) } + pub async fn get_in_environment_by_domain( + &self, + environment_id: EnvironmentId, + domain: &Domain, + auth: &AuthCtx, + ) -> Result { + let owner = self + .environment_service + .get_owner_unchecked(environment_id) + .await + .map_err(|err| match err { + EnvironmentError::EnvironmentNotFound(_) => { + DomainRegistrationError::ParentEnvironmentNotFound(environment_id) + } + other => other.into(), + })?; + authorize_domain_registration_permission_for_owner( + auth, + owner, + Some(domain), + EnvironmentDomainRegistrationVerb::View, + ) + .map_err(|_| DomainRegistrationError::DomainRegistrationByDomainNotFound(domain.clone()))?; + + let domain_registration: DomainRegistration = self + .domain_registration_repo + .get_in_environment(environment_id.0, &domain.0) + .await? + .ok_or(DomainRegistrationError::DomainRegistrationByDomainNotFound( + domain.clone(), + ))? + .into(); + + Ok(domain_registration) + } + pub async fn list_in_environment( &self, environment_id: EnvironmentId, diff --git a/golem-registry-service/src/services/environment.rs b/golem-registry-service/src/services/environment.rs index 60b1be5a3f..fa921dc512 100644 --- a/golem-registry-service/src/services/environment.rs +++ b/golem-registry-service/src/services/environment.rs @@ -329,6 +329,32 @@ impl EnvironmentService { Ok(environment) } + /// Loads only the environment's ownership triple, **without** enforcing + /// `EnvironmentVerb::View`. + /// + /// This is for building the permission target of a *narrower*, resource-scoped grant + /// (by-path / by-name lookups), so that a caller holding only the resource permission is + /// authorized against the resource directly — matching the behaviour of the ID-based + /// lookups. The returned owner is always passed to a subsequent resource authorization + /// check; never return the `Environment` itself (or any non-ownership data) from here. + pub async fn get_owner_unchecked( + &self, + environment_id: EnvironmentId, + ) -> Result { + let environment: Environment = self + .environment_repo + .get_by_id(environment_id.0, false) + .await? + .ok_or(EnvironmentError::EnvironmentNotFound(environment_id))? + .try_into()?; + + Ok(EnvironmentOwnerPattern::Environment { + account: environment.owner_account_email, + application: environment.application_name, + environment: environment.name, + }) + } + pub async fn get_in_application( &self, application_id: ApplicationId, diff --git a/golem-registry-service/src/services/plugin_registration.rs b/golem-registry-service/src/services/plugin_registration.rs index 6af6c90bd0..b8d030944d 100644 --- a/golem-registry-service/src/services/plugin_registration.rs +++ b/golem-registry-service/src/services/plugin_registration.rs @@ -36,6 +36,18 @@ use std::sync::Arc; pub enum PluginRegistrationError { #[error("Registered plugin not found for id {0}")] PluginRegistrationNotFound(PluginRegistrationId), + #[error("Registered plugin {name}/{version} not found in account {account_id}")] + PluginRegistrationByNameNotFound { + account_id: AccountId, + name: String, + version: String, + }, + #[error("Registered plugin {name}/{version} not found in account {account_email}")] + PluginRegistrationByEmailNotFound { + account_email: String, + name: String, + version: String, + }, #[error("Target component for oplog processor does not exist")] OplogProcessorComponentDoesNotExist, #[error("Plugin with this name and version already exists")] @@ -52,6 +64,8 @@ impl SafeDisplay for PluginRegistrationError { fn to_safe_string(&self) -> String { match self { Self::PluginRegistrationNotFound(_) => self.to_string(), + Self::PluginRegistrationByNameNotFound { .. } => self.to_string(), + Self::PluginRegistrationByEmailNotFound { .. } => self.to_string(), Self::OplogProcessorComponentDoesNotExist => self.to_string(), Self::PluginNameAndVersionAlreadyExists => self.to_string(), Self::ParentAccountNotFound(_) => self.to_string(), @@ -201,6 +215,82 @@ impl PluginRegistrationService { Ok(plugin) } + pub async fn get_account_plugin( + &self, + account_id: AccountId, + name: &str, + version: &str, + auth: &AuthCtx, + ) -> Result { + let not_found = || PluginRegistrationError::PluginRegistrationByNameNotFound { + account_id, + name: name.to_string(), + version: version.to_string(), + }; + let record = self + .plugin_repo + .get_by_name_and_version(account_id.0, name, version) + .await? + .ok_or_else(¬_found)?; + let account_email = record.account_email(); + let plugin: PluginRegistration = record.plugin.try_into()?; + + authorize_account_plugin_permission( + auth, + &account_email, + AccountPluginVerb::View, + AccountPluginResourcePattern::Name(AccountPluginName(name.to_string())), + ) + .map_err(|_| not_found())?; + + Ok(plugin) + } + + /// Like [`Self::get_account_plugin`], but keyed by the owner account's email. + /// + /// Resolves the email to an account id **without** requiring `AccountVerb::View` (the + /// plugin grant alone decides access), so the `--account ` CLI form behaves like + /// `--account-id`. Every miss — unknown email, missing plugin, or denied permission — + /// maps to the same not-found, so the resolved account id is never leaked. + pub async fn get_account_plugin_by_email( + &self, + account_email: &str, + name: &str, + version: &str, + auth: &AuthCtx, + ) -> Result { + let not_found = || PluginRegistrationError::PluginRegistrationByEmailNotFound { + account_email: account_email.to_string(), + name: name.to_string(), + version: version.to_string(), + }; + let account_id = self + .account_service + .resolve_account_id_by_email_unchecked(account_email) + .await + .map_err(|err| match err { + AccountError::AccountByEmailNotFound(_) => not_found(), + other => other.into(), + })?; + let record = self + .plugin_repo + .get_by_name_and_version(account_id.0, name, version) + .await? + .ok_or_else(¬_found)?; + let owner_email = record.account_email(); + let plugin: PluginRegistration = record.plugin.try_into()?; + + authorize_account_plugin_permission( + auth, + &owner_email, + AccountPluginVerb::View, + AccountPluginResourcePattern::Name(AccountPluginName(name.to_string())), + ) + .map_err(|_| not_found())?; + + Ok(plugin) + } + async fn get_plugin_record( &self, plugin_id: PluginRegistrationId, diff --git a/golem-registry-service/src/services/retry_policy.rs b/golem-registry-service/src/services/retry_policy.rs index 76cbeb310d..08764efbf0 100644 --- a/golem-registry-service/src/services/retry_policy.rs +++ b/golem-registry-service/src/services/retry_policy.rs @@ -48,6 +48,11 @@ pub enum RetryPolicyError { ParentEnvironmentNotFound(EnvironmentId), #[error("Retry policy {0} not found")] RetryPolicyNotFound(RetryPolicyId), + #[error("Retry policy {name} not found in environment {environment_id}")] + RetryPolicyByNameNotFound { + environment_id: EnvironmentId, + name: String, + }, #[error("Concurrent update attempt")] ConcurrentModification, #[error(transparent)] @@ -64,6 +69,7 @@ impl SafeDisplay for RetryPolicyError { Self::RetryPolicyForNameAlreadyExists { .. } => self.to_string(), Self::ParentEnvironmentNotFound(_) => self.to_string(), Self::RetryPolicyNotFound(_) => self.to_string(), + Self::RetryPolicyByNameNotFound { .. } => self.to_string(), Self::ConcurrentModification => self.to_string(), Self::Unauthorized(inner) => inner.to_safe_string(), Self::InternalError(_) => "Internal error".to_string(), @@ -242,6 +248,43 @@ impl RetryPolicyService { Ok(retry_policy) } + pub async fn get_in_environment( + &self, + environment_id: EnvironmentId, + name: &str, + auth: &AuthCtx, + ) -> Result { + let owner = self + .environment_service + .get_owner_unchecked(environment_id) + .await + .map_err(|err| match err { + EnvironmentError::EnvironmentNotFound(_) => { + RetryPolicyError::ParentEnvironmentNotFound(environment_id) + } + other => other.into(), + })?; + authorize_retry_policy_permission_for_owner( + auth, + owner, + Some(name), + EnvironmentRetryPolicyVerb::View, + ) + .map_err(|_| RetryPolicyError::RetryPolicyByNameNotFound { + environment_id, + name: name.to_string(), + })?; + self.retry_policy_repo + .get_for_environment_and_name(environment_id.0, name) + .await? + .ok_or_else(|| RetryPolicyError::RetryPolicyByNameNotFound { + environment_id, + name: name.to_string(), + })? + .try_into() + .map_err(Into::into) + } + pub async fn list_in_environment( &self, environment_id: EnvironmentId, diff --git a/golem-registry-service/src/services/security_scheme.rs b/golem-registry-service/src/services/security_scheme.rs index a040b160b0..b4646f7998 100644 --- a/golem-registry-service/src/services/security_scheme.rs +++ b/golem-registry-service/src/services/security_scheme.rs @@ -405,6 +405,42 @@ impl SecuritySchemeService { Ok(result) } + pub async fn get_in_environment( + &self, + environment_id: EnvironmentId, + name: &SecuritySchemeName, + auth: &AuthCtx, + ) -> Result { + let owner = self + .environment_service + .get_owner_unchecked(environment_id) + .await + .map_err(|err| match err { + EnvironmentError::EnvironmentNotFound(_) => { + SecuritySchemeError::ParentEnvironmentNotFound(environment_id) + } + other => other.into(), + })?; + authorize_security_scheme_permission_for_owner( + auth, + owner, + Some(name), + EnvironmentSecuritySchemeVerb::View, + ) + .map_err(|_| SecuritySchemeError::SecuritySchemeForNameNotFound(name.clone()))?; + + let result = self + .security_scheme_repo + .get_for_environment_and_name(environment_id.0, &name.0) + .await? + .ok_or(SecuritySchemeError::SecuritySchemeForNameNotFound( + name.clone(), + ))? + .try_into()?; + + Ok(result) + } + async fn get_with_environment( &self, security_scheme_id: SecuritySchemeId, diff --git a/golem-registry-service/tests/repo/common.rs b/golem-registry-service/tests/repo/common.rs index ea4f6eefea..04323f8081 100644 --- a/golem-registry-service/tests/repo/common.rs +++ b/golem-registry-service/tests/repo/common.rs @@ -1749,6 +1749,133 @@ pub async fn test_agent_secret_get_revision_include_deleted(deps: &Deps) { check!(get_agent_secret_initial_revision(deps, &secret, true).await); } +pub async fn test_retry_policy_and_agent_secret_natural_key_lookups(deps: &Deps) { + use golem_common::model::retry_policy::{RetryPolicyId, RetryPolicyRevision}; + use golem_registry_service::repo::model::retry_policy::{ + RetryPolicyCreationRecord, RetryPolicyRepoError, + }; + + let owner = deps.create_account().await; + let app = deps.create_application(owner.revision.account_id).await; + let env = deps.create_env(app.revision.application_id).await; + let environment_id = EnvironmentId(env.revision.environment_id); + let actor = AccountId(owner.revision.account_id); + + let retry_id = RetryPolicyId::new(); + let retry = RetryPolicyCreationRecord::new( + retry_id, + environment_id, + "retry.with.dots".to_string(), + 10, + "true".to_string(), + "{}".to_string(), + actor, + ); + let _ = deps.retry_policy_repo.create(retry.clone()).await.unwrap(); + let found = deps + .retry_policy_repo + .get_for_environment_and_name(environment_id.0, &retry.name) + .await + .unwrap() + .unwrap(); + check!(found.revision.retry_policy_id == retry_id.0); + check!( + deps.retry_policy_repo + .get_for_environment_and_name(environment_id.0, "missing") + .await + .unwrap() + .is_none() + ); + check!(matches!( + deps.retry_policy_repo.create(retry.clone()).await, + Err(RetryPolicyRepoError::NameViolatesUniqueness) + )); + let mut deleted_retry = retry.revision; + deleted_retry.revision_id = RetryPolicyRevision::INITIAL.next().unwrap().into(); + deleted_retry.audit = DeletableRevisionAuditFields::deletion(actor.0); + let _ = deps.retry_policy_repo.delete(deleted_retry).await.unwrap(); + check!( + deps.retry_policy_repo + .get_for_environment_and_name(environment_id.0, &retry.name) + .await + .unwrap() + .is_none() + ); + + for path in [ + vec!["single".to_string()], + vec!["segment.with.dots".to_string()], + vec![ + "multi".to_string(), + "segment".to_string(), + "path".to_string(), + ], + ] { + let secret_id = AgentSecretId::new(); + let creation = AgentSecretCreationRecord::new( + secret_id, + environment_id, + CanonicalAgentSecretPath(path.clone()), + SchemaGraph::empty(), + None, + actor, + ); + let _ = deps + .agent_secret_repo + .create(creation.clone()) + .await + .unwrap(); + let found = deps + .agent_secret_repo + .get_for_environment_and_path(environment_id.0, path.clone()) + .await + .unwrap() + .unwrap(); + check!(found.revision.agent_secret_id == secret_id.0); + check!(matches!( + deps.agent_secret_repo.create(creation).await, + Err(golem_registry_service::repo::model::agent_secrets::AgentSecretRepoError::SecretViolatesUniqueness) + )); + } + + let deleted_path = vec!["deleted".to_string(), "secret".to_string()]; + let deleted_id = AgentSecretId::new(); + let _ = deps + .agent_secret_repo + .create(AgentSecretCreationRecord::new( + deleted_id, + environment_id, + CanonicalAgentSecretPath(deleted_path.clone()), + SchemaGraph::empty(), + None, + actor, + )) + .await + .unwrap(); + let _ = deps + .agent_secret_repo + .delete( + AgentSecretRevisionRecord::delete(deleted_id, AgentSecretRevision::INITIAL, actor) + .unwrap(), + ) + .await + .unwrap(); + check!( + deps.agent_secret_repo + .get_for_environment_and_path(environment_id.0, deleted_path) + .await + .unwrap() + .is_none() + ); + check!( + deps.agent_secret_repo + .get_for_environment_and_path(environment_id.0, vec!["missing".to_string()]) + .await + .unwrap() + .is_none() + ); +} + pub async fn test_environment_create_concurrently(deps: &Deps) { let user = deps.create_account().await; let app = deps.create_application(user.revision.account_id).await; diff --git a/golem-registry-service/tests/repo/mod.rs b/golem-registry-service/tests/repo/mod.rs index 9c1b011f0b..1c1ee75a8b 100644 --- a/golem-registry-service/tests/repo/mod.rs +++ b/golem-registry-service/tests/repo/mod.rs @@ -48,6 +48,7 @@ use golem_registry_service::repo::plugin::PluginRepo; use golem_registry_service::repo::registry_change::{ ChangeEventId, DbRegistryChangeRepo, NewRegistryChangeEvent, RegistryChangeRepo, }; +use golem_registry_service::repo::retry_policy::RetryPolicyRepo; use golem_registry_service::repo::tool_release::ToolReleaseRepo; use golem_registry_service::services::account::AccountService; use golem_registry_service::services::account_usage::AccountUsageService; @@ -75,6 +76,7 @@ pub struct Deps { pub account_usage_repo: std::sync::Arc, pub account_resource_override_repo: std::sync::Arc, pub agent_secret_repo: Box, + pub retry_policy_repo: Box, pub application_repo: Box, pub environment_repo: Box, pub environment_tool_grant_repo: Box, diff --git a/golem-registry-service/tests/repo/postgres.rs b/golem-registry-service/tests/repo/postgres.rs index b4d969668e..3f0387bb97 100644 --- a/golem-registry-service/tests/repo/postgres.rs +++ b/golem-registry-service/tests/repo/postgres.rs @@ -31,6 +31,7 @@ use golem_registry_service::repo::plugin::DbPluginRepo; use golem_registry_service::repo::registry_change::{ DbRegistryChangeRepo, NewRegistryChangeEvent, RegistryChangeEvent, RegistryChangeRepo, }; +use golem_registry_service::repo::retry_policy::DbRetryPolicyRepo; use golem_registry_service::repo::tool_release::DbToolReleaseRepo; use golem_registry_service::services::registry_change_notifier::{ PostgresRegistryChangeNotifier, RegistryChangeNotifier, @@ -218,6 +219,7 @@ async fn make_deps(pool: PostgresPool) -> Deps { pool.clone(), )), agent_secret_repo: Box::new(DbAgentSecretRepo::logged(pool.clone())), + retry_policy_repo: Box::new(DbRetryPolicyRepo::logged(pool.clone())), application_repo: Box::new(DbApplicationRepo::logged(pool.clone())), environment_repo: Box::new(DbEnvironmentRepo::logged(pool.clone())), environment_tool_grant_repo: Box::new(DbEnvironmentToolGrantRepo::logged(pool.clone())), @@ -438,6 +440,13 @@ async fn test_agent_secret_get_revision_include_deleted( crate::repo::common::test_agent_secret_get_revision_include_deleted(deps).await; } +#[test] +async fn test_retry_policy_and_agent_secret_natural_key_lookups( + #[dimension(postgres_variant)] deps: &Deps, +) { + crate::repo::common::test_retry_policy_and_agent_secret_natural_key_lookups(deps).await; +} + #[test] async fn test_component_stage(#[dimension(postgres_variant)] deps: &Deps) { crate::repo::common::test_component_stage(deps).await; diff --git a/golem-registry-service/tests/repo/sqlite.rs b/golem-registry-service/tests/repo/sqlite.rs index 1c98f7e96f..6c5d869771 100644 --- a/golem-registry-service/tests/repo/sqlite.rs +++ b/golem-registry-service/tests/repo/sqlite.rs @@ -30,6 +30,7 @@ use golem_registry_service::repo::model::new_repo_uuid; use golem_registry_service::repo::plan::DbPlanRepo; use golem_registry_service::repo::plugin::DbPluginRepo; use golem_registry_service::repo::registry_change::DbRegistryChangeRepo; +use golem_registry_service::repo::retry_policy::DbRetryPolicyRepo; use golem_registry_service::repo::tool_release::DbToolReleaseRepo; use golem_service_base::db; use golem_service_base::db::sqlite::SqlitePool; @@ -92,6 +93,7 @@ async fn deps(db: &SqliteDb) -> Deps { db.pool.clone(), )), agent_secret_repo: Box::new(DbAgentSecretRepo::logged(db.pool.clone())), + retry_policy_repo: Box::new(DbRetryPolicyRepo::logged(db.pool.clone())), application_repo: Box::new(DbApplicationRepo::logged(db.pool.clone())), environment_repo: Box::new(DbEnvironmentRepo::logged(db.pool.clone())), environment_tool_grant_repo: Box::new(DbEnvironmentToolGrantRepo::logged(db.pool.clone())), @@ -232,6 +234,11 @@ async fn test_agent_secret_get_revision_include_deleted(deps: &Deps) { crate::repo::common::test_agent_secret_get_revision_include_deleted(deps).await; } +#[test] +async fn test_retry_policy_and_agent_secret_natural_key_lookups(deps: &Deps) { + crate::repo::common::test_retry_policy_and_agent_secret_natural_key_lookups(deps).await; +} + #[test] async fn test_component_stage(deps: &Deps) { crate::repo::common::test_component_stage(deps).await; diff --git a/golem-skills/skills/common/golem-manage-plugins/SKILL.md b/golem-skills/skills/common/golem-manage-plugins/SKILL.md index 8840a87bed..4e513491a8 100644 --- a/golem-skills/skills/common/golem-manage-plugins/SKILL.md +++ b/golem-skills/skills/common/golem-manage-plugins/SKILL.md @@ -150,56 +150,38 @@ environments: ### Listing Available Plugins ```shell -golem plugin list # List all registered plugins +golem plugin list +golem plugin list --account owner@example.com +golem plugin list --account-id 2f6b30d9-bac2-4c67-9d4f-12ea89ba2211 ``` -### Installing a Plugin on a Component (imperative) +With no account option, the list includes plugins owned by the authenticated account and plugins granted to the selected environment. An explicit account lists plugins owned by that account. `--account` and `--account-id` conflict. -```shell -golem component plugin install \ - --component-name my-app:service \ - --plugin-name golem-otlp-exporter \ - --plugin-version "1.1.5" \ - --priority 0 \ - --param endpoint=http://localhost:4318 \ - --param signals=traces,logs -``` - -### Viewing Installed Plugins +### Inspecting and unregistering registry plugins ```shell -golem component plugin get \ - --component-name my-app:service +golem plugin get my-plugin 1.0.0 +golem plugin get --id 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890 +golem plugin unregister my-plugin 1.0.0 +golem plugin unregister --id 8fd5e4a2-9cab-4f8e-9d3a-1c2e4f567890 ``` -### Updating a Plugin +The name and version form requires both positional values and accepts `--account` or `--account-id`. The `--id` form conflicts with the positional identity and account scope. A positional UUID is a name, not an ID; use `--id` explicitly. -```shell -golem component plugin update \ - --component-name my-app:service \ - --plugin-to-update 0 \ - --priority 1 \ - --param endpoint=https://new-endpoint:4318 -``` - -### Uninstalling a Plugin +Register a plugin from a JSON or YAML manifest, optionally for an explicit account: ```shell -golem component plugin uninstall \ - --component-name my-app:service \ - --plugin-to-update 0 +golem plugin register ./my-plugin.yaml +golem plugin register ./my-plugin.yaml --account owner@example.com ``` -## Declarative vs Imperative - -- **Declarative (golem.yaml)**: Preferred for repeatable setups. Plugins are installed/updated on `golem deploy`. Configuration lives in version control. -- **Imperative (CLI)**: Useful for quick one-off installations, debugging, or environments where the manifest is not available. +The manifest requires `name`, `version`, `description`, `icon`, `homepage`, and `specs`. The registry currently accepts `OplogProcessor` specs, which identify the oplog-processor component with `componentId` and `componentRevision`. -When using `golem deploy`, the manifest is the source of truth — any plugins defined in `golem.yaml` are reconciled with the deployed state. +Plugin installation is declarative. The retired `component plugin` and `project plugin` workflows are not available. Define installations in `golem.yaml`; `golem deploy` reconciles the manifest with deployed state. ## Plugin Priority -When multiple plugins are installed, `priority` determines their execution order. Plugins with **higher priority values are applied first**. Priority is set explicitly via the CLI's `--priority` flag; in `golem.yaml`, the order in the `plugins` list determines priority (first entry = highest priority). +When multiple plugins are installed, the order in the manifest's `plugins` list determines priority (first entry = highest priority). ## Documentation diff --git a/integration-tests/tests/api/account.rs b/integration-tests/tests/api/account.rs index 7a3dbe7629..ea25f6e855 100644 --- a/integration-tests/tests/api/account.rs +++ b/integration-tests/tests/api/account.rs @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use golem_client::api::{RegistryServiceClient, RegistryServiceCreateAccountError}; +use golem_client::api::{ + RegistryServiceClient, RegistryServiceCreateAccountError, RegistryServiceGetAccountByEmailError, +}; use golem_client::model::AccountUpdate; use golem_common::model::account::{AccountCreation, AccountEmail, AccountRevision}; use golem_test_framework::config::{EnvBasedTestDependencies, TestDependencies}; @@ -35,6 +37,23 @@ async fn get_account(deps: &EnvBasedTestDependencies) -> anyhow::Result<()> { assert_eq!(account.email, user.account_email); assert_eq!(account.revision, AccountRevision::INITIAL); assert_eq!(account.roles, Vec::new()); + + let account_by_email = client + .get_account_by_email(user.account_email.as_str()) + .await?; + assert_eq!(account_by_email, account); + + let other_user = deps.user().await?; + let other_client = deps.registry_service().client(&other_user.token).await; + let result = other_client + .get_account_by_email(user.account_email.as_str()) + .await; + assert_matches!( + result, + Err(golem_client::Error::Item( + RegistryServiceGetAccountByEmailError::Error404(_) + )) + ); } // get account plan diff --git a/integration-tests/tests/api/agent_secret.rs b/integration-tests/tests/api/agent_secret.rs index a74f9e0c43..f87df97dd5 100644 --- a/integration-tests/tests/api/agent_secret.rs +++ b/integration-tests/tests/api/agent_secret.rs @@ -14,13 +14,17 @@ use golem_client::api::{ RegistryServiceClient, RegistryServiceCreateAgentSecretError, - RegistryServiceDeleteAgentSecretError, RegistryServiceUpdateAgentSecretError, + RegistryServiceDeleteAgentSecretError, RegistryServiceGetEnvironmentAgentSecretError, + RegistryServiceUpdateAgentSecretError, }; use golem_client::model::{AgentSecretCreation, AgentSecretUpdate}; use golem_common::model::agent_secret::{ AgentSecretPath, AgentSecretRevision, CanonicalAgentSecretPath, }; use golem_common::model::optional_field_update::OptionalFieldUpdate; +use golem_common::model::permission_share::{ + PermissionShareCreation, PermissionShareData, PermissionShareName, +}; use golem_common::schema::{ExternalSchemaValue, SchemaGraph, SchemaType, SchemaValue}; use golem_test_framework::config::{EnvBasedTestDependencies, TestDependencies}; use golem_test_framework::dsl::TestDslExtended; @@ -63,6 +67,25 @@ async fn create_agent_secret_with_value(deps: &EnvBasedTestDependencies) -> anyh assert_eq!(fetched_secret, result); } + { + let fetched_secret = client + .get_environment_agent_secret(&env.id.0, &result.path.0) + .await?; + assert_eq!(fetched_secret, result); + + let other_user = deps.user().await?; + let other_client = deps.registry_service().client(&other_user.token).await; + let other_result = other_client + .get_environment_agent_secret(&env.id.0, &result.path.0) + .await; + assert_matches!( + other_result, + Err(golem_client::Error::Item( + RegistryServiceGetEnvironmentAgentSecretError::Error404(_) + )) + ); + } + { let all_environment_secrets = client.list_environment_agent_secrets(&env.id.0).await?; assert!(all_environment_secrets.values.contains(&result)); @@ -71,6 +94,71 @@ async fn create_agent_secret_with_value(deps: &EnvBasedTestDependencies) -> anyh Ok(()) } +#[test] +#[tracing::instrument] +async fn granted_secret_view_works_without_environment_view( + deps: &EnvBasedTestDependencies, +) -> anyhow::Result<()> { + let owner = deps.user().await?; + let owner_client = deps.registry_service().client(&owner.token).await; + let (app, env) = owner.app_and_env().await?; + + let creation = AgentSecretCreation { + path: AgentSecretPath(vec!["foo".to_string(), "bar".to_string()]), + secret_type: SchemaGraph::anonymous(SchemaType::bool()), + secret_value: Some(external(SchemaValue::Bool(true))), + }; + let secret = owner_client + .create_agent_secret(&env.id.0, &creation) + .await?; + + let grantee = deps.user().await?; + let grantee_client = deps.registry_service().client(&grantee.token).await; + + // Before any grant the by-path lookup is not visible to the grantee. + let before = grantee_client + .get_environment_agent_secret(&env.id.0, &secret.path.0) + .await; + assert_matches!( + before, + Err(golem_client::Error::Item( + RegistryServiceGetEnvironmentAgentSecretError::Error404(_) + )) + ); + + // Grant view on this ONE secret only — no environment-view permission. + owner_client + .create_permission_share( + &owner.account_id.0, + &PermissionShareCreation { + target_account_email: grantee.account_email.clone(), + name: PermissionShareName("secret-view".to_string()), + data: PermissionShareData { + lower_positive: vec![format!( + "environment.agent-secret({}/{}/{}) @ {} : view : {}", + owner.account_email.as_str(), + app.name.0, + env.name.0, + grantee.account_email.as_str(), + secret.path.0.join("."), + )], + lower_negative: Vec::new(), + upper_positive: Vec::new(), + upper_negative: Vec::new(), + }, + }, + ) + .await?; + + // With only the resource grant the by-path lookup now succeeds, matching the by-id lookup. + let fetched = grantee_client + .get_environment_agent_secret(&env.id.0, &secret.path.0) + .await?; + assert_eq!(fetched, secret); + + Ok(()) +} + #[test] #[tracing::instrument] async fn secret_path_is_canonicalized_when_reading( @@ -110,6 +198,13 @@ async fn secret_path_is_canonicalized_when_reading( assert_eq!(fetched_secret, result); } + { + let fetched_secret = client + .get_environment_agent_secret(&env.id.0, &creation.path.0) + .await?; + assert_eq!(fetched_secret, result); + } + { let all_environment_secrets = client.list_environment_agent_secrets(&env.id.0).await?; assert!(all_environment_secrets.values.contains(&result)); diff --git a/integration-tests/tests/api/domain_registration.rs b/integration-tests/tests/api/domain_registration.rs index 1478186683..da068bd421 100644 --- a/integration-tests/tests/api/domain_registration.rs +++ b/integration-tests/tests/api/domain_registration.rs @@ -15,9 +15,13 @@ use golem_client::api::{ RegistryServiceClient, RegistryServiceCreateDomainRegistrationError, RegistryServiceGetDomainRegistrationError, + RegistryServiceGetEnvironmentDomainRegistrationError, RegistryServiceListEnvironmentDomainRegistrationsError, }; use golem_common::model::domain_registration::{Domain, DomainRegistrationCreation}; +use golem_common::model::permission_share::{ + PermissionShareCreation, PermissionShareData, PermissionShareName, +}; use golem_test_framework::config::{EnvBasedTestDependencies, TestDependencies}; use golem_test_framework::dsl::TestDslExtended; use pretty_assertions::assert_eq; @@ -53,6 +57,13 @@ async fn register_and_fetch_domain(deps: &EnvBasedTestDependencies) -> anyhow::R assert_eq!(fetched_domain_registration, domain_registration); } + { + let fetched_domain_registration = client + .get_environment_domain_registration(&env.id.0, &domain.0) + .await?; + assert_eq!(fetched_domain_registration, domain_registration); + } + { let result = client .list_environment_domain_registrations(&env.id.0) @@ -63,6 +74,74 @@ async fn register_and_fetch_domain(deps: &EnvBasedTestDependencies) -> anyhow::R Ok(()) } +#[test] +#[tracing::instrument] +async fn granted_domain_view_works_without_environment_view( + deps: &EnvBasedTestDependencies, +) -> anyhow::Result<()> { + let owner = deps.user().await?; + let owner_client = deps.registry_service().client(&owner.token).await; + let (app, env) = owner.app_and_env().await?; + + // Domain registration is globally unique, so this must not collide with any other test's + // domain (see register_and_fetch_domain and the other_users_* tests). + let domain = Domain("test6.golem.cloud".to_string()); + let domain_registration = owner_client + .create_domain_registration( + &env.id.0, + &DomainRegistrationCreation { + domain: domain.clone(), + }, + ) + .await?; + + let grantee = deps.user().await?; + let grantee_client = deps.registry_service().client(&grantee.token).await; + + // Before any grant the by-domain lookup is not visible to the grantee. + let before = grantee_client + .get_environment_domain_registration(&env.id.0, &domain.0) + .await; + assert!(matches!( + before, + Err(golem_client::Error::Item( + RegistryServiceGetEnvironmentDomainRegistrationError::Error404(_) + )) + )); + + // Grant view on this ONE domain registration only — no environment-view permission. + owner_client + .create_permission_share( + &owner.account_id.0, + &PermissionShareCreation { + target_account_email: grantee.account_email.clone(), + name: PermissionShareName("domain-view".to_string()), + data: PermissionShareData { + lower_positive: vec![format!( + "environment.domain-registration({}/{}/{}) @ {} : view : {}", + owner.account_email.as_str(), + app.name.0, + env.name.0, + grantee.account_email.as_str(), + domain.0, + )], + lower_negative: Vec::new(), + upper_positive: Vec::new(), + upper_negative: Vec::new(), + }, + }, + ) + .await?; + + // With only the resource grant the by-domain lookup now succeeds, matching the by-id lookup. + let fetched = grantee_client + .get_environment_domain_registration(&env.id.0, &domain.0) + .await?; + assert_eq!(fetched, domain_registration); + + Ok(()) +} + #[test] #[tracing::instrument] async fn delete_domain(deps: &EnvBasedTestDependencies) -> anyhow::Result<()> { @@ -131,6 +210,18 @@ async fn other_users_cannot_see_domain(deps: &EnvBasedTestDependencies) -> anyho )); } + { + let result = client_2 + .get_environment_domain_registration(&env.id.0, &domain.domain.0) + .await; + assert!(matches!( + result, + Err(golem_client::Error::Item( + RegistryServiceGetEnvironmentDomainRegistrationError::Error404(_) + )) + )); + } + { let result = client_2 .list_environment_domain_registrations(&env.id.0) diff --git a/integration-tests/tests/api/plugin_registration.rs b/integration-tests/tests/api/plugin_registration.rs index b83f820c5a..d3e1f9504b 100644 --- a/integration-tests/tests/api/plugin_registration.rs +++ b/integration-tests/tests/api/plugin_registration.rs @@ -13,7 +13,9 @@ // limitations under the License. use golem_client::api::{ - RegistryServiceClient, RegistryServiceCreatePluginError, RegistryServiceGetPluginByIdError, + RegistryServiceClient, RegistryServiceCreatePluginError, + RegistryServiceGetAccountPluginByEmailError, RegistryServiceGetAccountPluginError, + RegistryServiceGetPluginByIdError, }; use golem_common::model::base64::Base64; use golem_common::model::permission_share::{ @@ -65,6 +67,13 @@ async fn can_create_and_fetch_plugins(deps: &EnvBasedTestDependencies) -> anyhow assert_eq!(fetched_plugin, plugin); } + { + let fetched_plugin = client + .get_account_plugin(&user.account_id.0, &plugin.name, &plugin.version) + .await?; + assert_eq!(fetched_plugin, plugin); + } + // check other user cannot fetch plugin { let user_2 = deps.user().await?; @@ -76,6 +85,65 @@ async fn can_create_and_fetch_plugins(deps: &EnvBasedTestDependencies) -> anyhow RegistryServiceGetPluginByIdError::Error404(_) )) )); + + let result = client_2 + .get_account_plugin(&user.account_id.0, &plugin.name, &plugin.version) + .await; + assert!(matches!( + result, + Err(golem_client::Error::Item( + RegistryServiceGetAccountPluginError::Error404(_) + )) + )); + + // The by-email scope form (as used by the CLI `--account `) is likewise not + // visible before the grant. + let result = client_2 + .get_account_plugin_by_email(user.account_email.as_str(), &plugin.name, &plugin.version) + .await; + assert!(matches!( + result, + Err(golem_client::Error::Item( + RegistryServiceGetAccountPluginByEmailError::Error404(_) + )) + )); + + client + .create_permission_share( + &user.account_id.0, + &PermissionShareCreation { + target_account_email: user_2.account_email.clone(), + name: PermissionShareName("plugin-view".to_string()), + data: PermissionShareData { + lower_positive: vec![format!( + "account.plugin({}) @ {} : view : {}", + user.account_email.as_str(), + user_2.account_email.as_str(), + plugin.name, + )], + lower_negative: Vec::new(), + upper_positive: Vec::new(), + upper_negative: Vec::new(), + }, + }, + ) + .await?; + + let fetched_plugin = client_2.get_plugin_by_id(&plugin.id.0).await?; + assert_eq!(fetched_plugin, plugin); + + let fetched_plugin = client_2 + .get_account_plugin(&user.account_id.0, &plugin.name, &plugin.version) + .await?; + assert_eq!(fetched_plugin, plugin); + + // With only the plugin grant (no account-view), the by-email scope form now behaves + // like the account-id form — the CLI `--account ` path no longer needs + // `AccountVerb::View`. + let fetched_plugin = client_2 + .get_account_plugin_by_email(user.account_email.as_str(), &plugin.name, &plugin.version) + .await?; + assert_eq!(fetched_plugin, plugin); } // delete plugin diff --git a/integration-tests/tests/api/retry_policies.rs b/integration-tests/tests/api/retry_policies.rs index 7a8a045cc3..b139cb633d 100644 --- a/integration-tests/tests/api/retry_policies.rs +++ b/integration-tests/tests/api/retry_policies.rs @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use golem_client::api::RegistryServiceClient; +use golem_client::api::{RegistryServiceClient, RegistryServiceGetEnvironmentRetryPolicyError}; use golem_common::base_model::retry_policy::{ ApiCountBoxPolicy, ApiPeriodicPolicy, ApiPredicate, ApiPredicateTrue, ApiPredicateValue, ApiPropertyComparison, ApiRetryPolicy, ApiTextValue, }; use golem_common::model::agent::ParsedAgentId; use golem_common::model::component::ComponentDto; +use golem_common::model::permission_share::{ + PermissionShareCreation, PermissionShareData, PermissionShareName, +}; use golem_common::model::retry_policy::{ RetryPolicyCreation, RetryPolicyRevision, RetryPolicyUpdate, }; @@ -117,6 +120,25 @@ async fn create_and_get_retry_policy(deps: &EnvBasedTestDependencies) -> anyhow: assert_eq!(fetched, created); } + { + let fetched = client + .get_environment_retry_policy(&env.id.0, &created.name) + .await?; + assert_eq!(fetched, created); + + let other_user = deps.user().await?; + let other_client = deps.registry_service().client(&other_user.token).await; + let other_result = other_client + .get_environment_retry_policy(&env.id.0, &created.name) + .await; + assert!(matches!( + other_result, + Err(golem_client::Error::Item( + RegistryServiceGetEnvironmentRetryPolicyError::Error404(_) + )) + )); + } + { let all = client.list_environment_retry_policies(&env.id.0).await?; assert!(all.values.contains(&created)); @@ -125,6 +147,72 @@ async fn create_and_get_retry_policy(deps: &EnvBasedTestDependencies) -> anyhow: Ok(()) } +#[test] +#[tracing::instrument] +async fn granted_retry_policy_view_works_without_environment_view( + deps: &EnvBasedTestDependencies, +) -> anyhow::Result<()> { + let owner = deps.user().await?; + let owner_client = deps.registry_service().client(&owner.token).await; + let (app, env) = owner.app_and_env().await?; + + let creation = RetryPolicyCreation { + name: "test-policy".to_string(), + priority: 10, + predicate: simple_predicate(), + policy: simple_policy(), + }; + let created = owner_client + .create_retry_policy(&env.id.0, &creation) + .await?; + + let grantee = deps.user().await?; + let grantee_client = deps.registry_service().client(&grantee.token).await; + + // Before any grant the by-name lookup is not visible to the grantee. + let before = grantee_client + .get_environment_retry_policy(&env.id.0, &created.name) + .await; + assert!(matches!( + before, + Err(golem_client::Error::Item( + RegistryServiceGetEnvironmentRetryPolicyError::Error404(_) + )) + )); + + // Grant view on this ONE retry policy only — no environment-view permission. + owner_client + .create_permission_share( + &owner.account_id.0, + &PermissionShareCreation { + target_account_email: grantee.account_email.clone(), + name: PermissionShareName("retry-policy-view".to_string()), + data: PermissionShareData { + lower_positive: vec![format!( + "environment.retry-policy({}/{}/{}) @ {} : view : {}", + owner.account_email.as_str(), + app.name.0, + env.name.0, + grantee.account_email.as_str(), + created.name, + )], + lower_negative: Vec::new(), + upper_positive: Vec::new(), + upper_negative: Vec::new(), + }, + }, + ) + .await?; + + // With only the resource grant the by-name lookup now succeeds, matching the by-id lookup. + let fetched = grantee_client + .get_environment_retry_policy(&env.id.0, &created.name) + .await?; + assert_eq!(fetched, created); + + Ok(()) +} + #[test] #[tracing::instrument] async fn update_retry_policy(deps: &EnvBasedTestDependencies) -> anyhow::Result<()> { diff --git a/integration-tests/tests/api/security_schemes.rs b/integration-tests/tests/api/security_schemes.rs index c9e985d05a..6a2d10cb55 100644 --- a/integration-tests/tests/api/security_schemes.rs +++ b/integration-tests/tests/api/security_schemes.rs @@ -14,9 +14,13 @@ use golem_client::api::{ RegistryServiceClient, RegistryServiceCreateSecuritySchemeError, - RegistryServiceGetSecuritySchemeError, RegistryServiceListEnvironmentSecuritySchemesError, + RegistryServiceGetEnvironmentSecuritySchemeError, RegistryServiceGetSecuritySchemeError, + RegistryServiceListEnvironmentSecuritySchemesError, }; use golem_common::model::Empty; +use golem_common::model::permission_share::{ + PermissionShareCreation, PermissionShareData, PermissionShareName, +}; use golem_common::model::security_scheme::{ Provider, SecuritySchemeCreation, SecuritySchemeName, SecuritySchemeUpdate, }; @@ -55,6 +59,13 @@ async fn create_and_fetch_security_scheme(deps: &EnvBasedTestDependencies) -> an assert_eq!(fetched_security_scheme, security_scheme); } + { + let fetched_security_scheme = client + .get_environment_security_scheme(&env.id.0, &security_scheme.name.0) + .await?; + assert_eq!(fetched_security_scheme, security_scheme); + } + { let result = client.list_environment_security_schemes(&env.id.0).await?; assert_eq!(result.values, vec![security_scheme]); @@ -63,6 +74,74 @@ async fn create_and_fetch_security_scheme(deps: &EnvBasedTestDependencies) -> an Ok(()) } +#[test] +#[tracing::instrument] +async fn granted_security_scheme_view_works_without_environment_view( + deps: &EnvBasedTestDependencies, +) -> anyhow::Result<()> { + let owner = deps.user().await?; + let owner_client = deps.registry_service().client(&owner.token).await; + let (app, env) = owner.app_and_env().await?; + + let security_scheme_creation = SecuritySchemeCreation { + name: SecuritySchemeName("test-scheme".to_string()), + provider_type: Provider::Google(Empty {}), + client_id: "client_id".to_string(), + client_secret: "client_secret".to_string(), + redirect_url: "http://localhost:9006/auth/callback".to_string(), + scopes: vec!["user".to_string(), "admin".to_string()], + }; + let security_scheme = owner_client + .create_security_scheme(&env.id.0, &security_scheme_creation) + .await?; + + let grantee = deps.user().await?; + let grantee_client = deps.registry_service().client(&grantee.token).await; + + // Before any grant the by-name lookup is not visible to the grantee. + let before = grantee_client + .get_environment_security_scheme(&env.id.0, &security_scheme.name.0) + .await; + assert!(matches!( + before, + Err(golem_client::Error::Item( + RegistryServiceGetEnvironmentSecuritySchemeError::Error404(_) + )) + )); + + // Grant view on this ONE security scheme only — no environment-view permission. + owner_client + .create_permission_share( + &owner.account_id.0, + &PermissionShareCreation { + target_account_email: grantee.account_email.clone(), + name: PermissionShareName("security-scheme-view".to_string()), + data: PermissionShareData { + lower_positive: vec![format!( + "environment.security-scheme({}/{}/{}) @ {} : view : {}", + owner.account_email.as_str(), + app.name.0, + env.name.0, + grantee.account_email.as_str(), + security_scheme.name.0, + )], + lower_negative: Vec::new(), + upper_positive: Vec::new(), + upper_negative: Vec::new(), + }, + }, + ) + .await?; + + // With only the resource grant the by-name lookup now succeeds, matching the by-id lookup. + let fetched = grantee_client + .get_environment_security_scheme(&env.id.0, &security_scheme.name.0) + .await?; + assert_eq!(fetched, security_scheme); + + Ok(()) +} + #[test] #[tracing::instrument] async fn delete_security_scheme(deps: &EnvBasedTestDependencies) -> anyhow::Result<()> { @@ -174,6 +253,18 @@ async fn other_users_cannot_see_security_scheme( )); } + { + let result = client_2 + .get_environment_security_scheme(&env.id.0, &security_scheme.name.0) + .await; + assert!(matches!( + result, + Err(golem_client::Error::Item( + RegistryServiceGetEnvironmentSecuritySchemeError::Error404(_) + )) + )); + } + { let result = client_2.list_environment_security_schemes(&env.id.0).await; assert!(matches!( diff --git a/openapi/golem-registry-service.yaml b/openapi/golem-registry-service.yaml index 7c4e8562e4..72e37c63b7 100644 --- a/openapi/golem-registry-service.yaml +++ b/openapi/golem-registry-service.yaml @@ -351,6 +351,73 @@ paths: - Cookie: [] - Token: [] operationId: delete_account + /v1/accounts/by-email/{account_email}: + get: + tags: + - RegistryService + - Account + summary: Retrieve an account by email address. + parameters: + - name: account_email + schema: + type: string + in: path + required: true + deprecated: false + explode: true + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/Account' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + operationId: get_account_by_email /v1/accounts/{account_id}/plan: get: tags: @@ -1571,6 +1638,84 @@ paths: - Cookie: [] - Token: [] operationId: list_environment_agent_secrets + /v1/envs/{environment_id}/agent-secrets/by-path: + get: + tags: + - RegistryService + - AgentSecrets + - Environment + summary: Get an agent secret in an environment by its path segments. + parameters: + - name: environment_id + schema: + type: string + format: uuid + in: path + required: true + deprecated: false + explode: true + - name: path + schema: + type: array + items: + type: string + in: query + required: true + deprecated: false + explode: true + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/AgentSecretDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + operationId: get_environment_agent_secret /v1/agent-secrets/{agent_secret_id}: get: tags: @@ -3370,6 +3515,82 @@ paths: - Cookie: [] - Token: [] operationId: list_environment_domain_registrations + /v1/envs/{environment_id}/domain-registrations/{domain}: + get: + tags: + - RegistryService + - ApiDomain + - Environment + summary: Get a domain registration in an environment by domain. + parameters: + - name: environment_id + schema: + type: string + format: uuid + in: path + required: true + deprecated: false + explode: true + - name: domain + schema: + type: string + in: path + required: true + deprecated: false + explode: true + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/DomainRegistration' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + operationId: get_environment_domain_registration /v1/domain-registrations/{domain_registration_id}: get: tags: @@ -7920,6 +8141,175 @@ paths: - Cookie: [] - Token: [] operationId: list_account_plugins + /v1/accounts/{account_id}/plugins/{plugin_name}/{plugin_version}: + get: + tags: + - RegistryService + - Plugin + - Account + summary: Get an account plugin by name and version. + parameters: + - name: account_id + schema: + type: string + format: uuid + in: path + required: true + deprecated: false + explode: true + - name: plugin_name + schema: + type: string + in: path + required: true + deprecated: false + explode: true + - name: plugin_version + schema: + type: string + in: path + required: true + deprecated: false + explode: true + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/PluginRegistrationDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + operationId: get_account_plugin + /v1/accounts/by-email/{account_email}/plugins/{plugin_name}/{plugin_version}: + get: + tags: + - RegistryService + - Plugin + - Account + summary: Get an account plugin by the owner account's email, name and version. + description: |- + Authorizes on the plugin permission alone — the same as the account-id form — resolving + the owner account from the email without an `AccountVerb::View` check, so the email and id + account scopes behave identically under granular sharing. + parameters: + - name: account_email + schema: + type: string + in: path + required: true + deprecated: false + explode: true + - name: plugin_name + schema: + type: string + in: path + required: true + deprecated: false + explode: true + - name: plugin_version + schema: + type: string + in: path + required: true + deprecated: false + explode: true + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/PluginRegistrationDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + operationId: get_account_plugin_by_email /v1/plugins/{plugin_id}: get: tags: @@ -8821,6 +9211,82 @@ paths: - Cookie: [] - Token: [] operationId: list_environment_retry_policies + /v1/envs/{environment_id}/retry-policies/{retry_policy_name}: + get: + tags: + - RegistryService + - RetryPolicies + - Environment + summary: Get a retry policy in an environment by name. + parameters: + - name: environment_id + schema: + type: string + format: uuid + in: path + required: true + deprecated: false + explode: true + - name: retry_policy_name + schema: + type: string + in: path + required: true + deprecated: false + explode: true + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/RetryPolicyDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + operationId: get_environment_retry_policy /v1/retry-policies/{retry_policy_id}: get: tags: @@ -9180,6 +9646,82 @@ paths: - Cookie: [] - Token: [] operationId: list_environment_security_schemes + /v1/envs/{environment_id}/security-schemes/{security_scheme_name}: + get: + tags: + - RegistryService + - ApiSecurity + - Environment + summary: Get a security scheme in an environment by name. + parameters: + - name: environment_id + schema: + type: string + format: uuid + in: path + required: true + deprecated: false + explode: true + - name: security_scheme_name + schema: + type: string + in: path + required: true + deprecated: false + explode: true + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/SecuritySchemeDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + operationId: get_environment_security_scheme /v1/security-schemes/{security_scheme_id}: get: tags: diff --git a/openapi/golem-service.yaml b/openapi/golem-service.yaml index f6eca1f631..af217418a7 100644 --- a/openapi/golem-service.yaml +++ b/openapi/golem-service.yaml @@ -2436,6 +2436,74 @@ paths: security: - Cookie: [] - Token: [] + /v1/accounts/by-email/{account_email}: + get: + tags: + - RegistryService + - Account + summary: Retrieve an account by email address. + operationId: get_account_by_email + parameters: + - in: path + name: account_email + required: true + deprecated: false + schema: + type: string + explode: true + style: simple + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/Account' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] /v1/accounts/{account_id}/plan: get: tags: @@ -3673,6 +3741,86 @@ paths: security: - Cookie: [] - Token: [] + /v1/envs/{environment_id}/agent-secrets/by-path: + get: + tags: + - RegistryService + - AgentSecrets + - Environment + summary: Get an agent secret in an environment by its path segments. + operationId: get_environment_agent_secret + parameters: + - in: path + name: environment_id + required: true + deprecated: false + schema: + type: string + format: uuid + explode: true + style: simple + - in: query + name: path + required: true + deprecated: false + schema: + type: array + items: + type: string + explode: true + style: form + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/AgentSecretDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] /v1/agent-secrets/{agent_secret_id}: get: tags: @@ -5506,6 +5654,84 @@ paths: security: - Cookie: [] - Token: [] + /v1/envs/{environment_id}/domain-registrations/{domain}: + get: + tags: + - RegistryService + - ApiDomain + - Environment + summary: Get a domain registration in an environment by domain. + operationId: get_environment_domain_registration + parameters: + - in: path + name: environment_id + required: true + deprecated: false + schema: + type: string + format: uuid + explode: true + style: simple + - in: path + name: domain + required: true + deprecated: false + schema: + type: string + explode: true + style: simple + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/DomainRegistration' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] /v1/domain-registrations/{domain_registration_id}: get: tags: @@ -10141,16 +10367,17 @@ paths: security: - Cookie: [] - Token: [] - /v1/plugins/{plugin_id}: + /v1/accounts/{account_id}/plugins/{plugin_name}/{plugin_version}: get: tags: - RegistryService - Plugin - summary: Get a plugin by id - operationId: get_plugin_by_id + - Account + summary: Get an account plugin by name and version. + operationId: get_account_plugin parameters: - in: path - name: plugin_id + name: account_id required: true deprecated: false schema: @@ -10158,6 +10385,22 @@ paths: format: uuid explode: true style: simple + - in: path + name: plugin_name + required: true + deprecated: false + schema: + type: string + explode: true + style: simple + - in: path + name: plugin_version + required: true + deprecated: false + schema: + type: string + explode: true + style: simple responses: '200': description: '' @@ -10210,33 +10453,191 @@ paths: security: - Cookie: [] - Token: [] - delete: + /v1/accounts/by-email/{account_email}/plugins/{plugin_name}/{plugin_version}: + get: tags: - RegistryService - Plugin - summary: Delete a plugin - operationId: delete_plugin + - Account + summary: Get an account plugin by the owner account's email, name and version. + description: |- + Authorizes on the plugin permission alone — the same as the account-id form — resolving + the owner account from the email without an `AccountVerb::View` check, so the email and id + account scopes behave identically under granular sharing. + operationId: get_account_plugin_by_email parameters: - in: path - name: plugin_id + name: account_email required: true deprecated: false schema: type: string - format: uuid explode: true style: simple - responses: - '200': - description: '' - content: - application/json; charset=utf-8: - schema: - $ref: '#/components/schemas/PluginRegistrationDto' - '400': - description: Invalid request, returning with a list of issues detected in the request - content: - application/json; charset=utf-8: + - in: path + name: plugin_name + required: true + deprecated: false + schema: + type: string + explode: true + style: simple + - in: path + name: plugin_version + required: true + deprecated: false + schema: + type: string + explode: true + style: simple + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/PluginRegistrationDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + /v1/plugins/{plugin_id}: + get: + tags: + - RegistryService + - Plugin + summary: Get a plugin by id + operationId: get_plugin_by_id + parameters: + - in: path + name: plugin_id + required: true + deprecated: false + schema: + type: string + format: uuid + explode: true + style: simple + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/PluginRegistrationDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] + delete: + tags: + - RegistryService + - Plugin + summary: Delete a plugin + operationId: delete_plugin + parameters: + - in: path + name: plugin_id + required: true + deprecated: false + schema: + type: string + format: uuid + explode: true + style: simple + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/PluginRegistrationDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: schema: $ref: '#/components/schemas/ErrorsBody' '401': @@ -11056,6 +11457,84 @@ paths: security: - Cookie: [] - Token: [] + /v1/envs/{environment_id}/retry-policies/{retry_policy_name}: + get: + tags: + - RegistryService + - RetryPolicies + - Environment + summary: Get a retry policy in an environment by name. + operationId: get_environment_retry_policy + parameters: + - in: path + name: environment_id + required: true + deprecated: false + schema: + type: string + format: uuid + explode: true + style: simple + - in: path + name: retry_policy_name + required: true + deprecated: false + schema: + type: string + explode: true + style: simple + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/RetryPolicyDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] /v1/retry-policies/{retry_policy_id}: get: tags: @@ -11421,6 +11900,84 @@ paths: security: - Cookie: [] - Token: [] + /v1/envs/{environment_id}/security-schemes/{security_scheme_name}: + get: + tags: + - RegistryService + - ApiSecurity + - Environment + summary: Get a security scheme in an environment by name. + operationId: get_environment_security_scheme + parameters: + - in: path + name: environment_id + required: true + deprecated: false + schema: + type: string + format: uuid + explode: true + style: simple + - in: path + name: security_scheme_name + required: true + deprecated: false + schema: + type: string + explode: true + style: simple + responses: + '200': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/SecuritySchemeDto' + '400': + description: Invalid request, returning with a list of issues detected in the request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorsBody' + '401': + description: Unauthorized request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '403': + description: Forbidden Request + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '404': + description: Entity not found + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '409': + description: '' + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '422': + description: Limits of the plan exceeded + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + '500': + description: Internal server error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/ErrorBody' + security: + - Cookie: [] + - Token: [] /v1/security-schemes/{security_scheme_id}: get: tags: