(
store: &Store,
provider: Provider,
)
| 174 | } |
| 175 | |
| 176 | pub(super) async fn update_provider_record( |
| 177 | store: &Store, |
| 178 | provider: Provider, |
| 179 | ) -> Result<Provider, Status> { |
| 180 | use crate::persistence::{ObjectId, ObjectName}; |
| 181 | |
| 182 | if provider.object_name().is_empty() { |
| 183 | return Err(Status::invalid_argument("provider.name is required")); |
| 184 | } |
| 185 | |
| 186 | // Extract expected version from provider metadata |
| 187 | let expected_resource_version = provider.metadata.as_ref().map_or(0, |m| m.resource_version); |
| 188 | |
| 189 | // Resolve provider ID from name for CAS update |
| 190 | let existing = store |
| 191 | .get_message_by_name::<Provider>(provider.object_name()) |
| 192 | .await |
| 193 | .map_err(|e| Status::internal(format!("fetch provider failed: {e}")))?; |
| 194 | |
| 195 | let Some(existing) = existing else { |
| 196 | return Err(Status::not_found("provider not found")); |
| 197 | }; |
| 198 | |
| 199 | // Provider type is immutable after creation. Reject if the caller |
| 200 | // sends a non-empty type that differs from the existing one. |
| 201 | let incoming_type = provider.r#type.trim(); |
| 202 | if !incoming_type.is_empty() && !incoming_type.eq_ignore_ascii_case(existing.r#type.trim()) { |
| 203 | return Err(Status::invalid_argument( |
| 204 | "provider type cannot be changed; delete and recreate the provider", |
| 205 | )); |
| 206 | } |
| 207 | |
| 208 | let current_version = existing.metadata.as_ref().map_or(0, |m| m.resource_version); |
| 209 | |
| 210 | let cas_version = if expected_resource_version == 0 { |
| 211 | current_version |
| 212 | } else { |
| 213 | expected_resource_version |
| 214 | }; |
| 215 | |
| 216 | // Apply merge to create candidate |
| 217 | let mut candidate = existing.clone(); |
| 218 | candidate.credentials = merge_map(candidate.credentials, provider.credentials); |
| 219 | candidate.config = merge_map(candidate.config, provider.config); |
| 220 | candidate.credential_expires_at_ms = merge_i64_map( |
| 221 | candidate.credential_expires_at_ms, |
| 222 | provider.credential_expires_at_ms, |
| 223 | ); |
| 224 | |
| 225 | // Validate BEFORE writing to prevent persisting invalid state. |
| 226 | // Validate only the mutable fields (credentials/config) plus metadata and |
| 227 | // attached-sandbox invariants. The immutable name/type are carried forward |
| 228 | // from `existing` and re-running full `validate_provider_fields` here would |
| 229 | // strand legacy records whose stored type predates current limits. See |
| 230 | // #1347. |
| 231 | super::validation::validate_object_metadata(candidate.metadata.as_ref(), "provider")?; |
| 232 | validate_provider_mutable_fields(&candidate)?; |
| 233 | validate_provider_update_against_attached_sandboxes(store, &candidate).await?; |
no test coverage detected