Skip to content

Commit b0ca895

Browse files
committed
fix(credentials): harden credential update handling
Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
1 parent c113e69 commit b0ca895

3 files changed

Lines changed: 315 additions & 66 deletions

File tree

crates/openshell-driver-vault/src/lib.rs

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,18 @@ impl VaultCredentialDriver {
131131
&self,
132132
request: StoreCredentialRequest,
133133
) -> Result<CredentialHandle, Status> {
134-
let token = self.auth_token().await?;
135134
let logical_path = if let Some(existing_handle) = request.existing_handle.as_ref() {
136135
Self::logical_path_from_handle(existing_handle)?
137136
} else {
138137
managed_secret_path(&request.provider_name, &request.credential_key)
139138
};
140139
validate_secret_path(&logical_path).map_err(Status::invalid_argument)?;
140+
validate_managed_secret_path(
141+
&request.provider_name,
142+
&request.credential_key,
143+
&logical_path,
144+
)?;
145+
let token = self.auth_token().await?;
141146
let reference = VaultSecretReference {
142147
api_path: api_path_for_reference(
143148
&self.settings.mount,
@@ -157,9 +162,14 @@ impl VaultCredentialDriver {
157162
}
158163

159164
pub async fn delete_credential(&self, request: DeleteCredentialRequest) -> Result<(), Status> {
160-
let token = self.auth_token().await?;
161165
let handle = Self::handle_from_request("delete", request.handle)?;
162166
let logical_path = Self::logical_path_from_handle(&handle)?;
167+
validate_managed_secret_path(
168+
&request.provider_name,
169+
&request.credential_key,
170+
&logical_path,
171+
)?;
172+
let token = self.auth_token().await?;
163173
let api_path = delete_api_path_for_reference(
164174
&self.settings.mount,
165175
self.settings.kv_version,
@@ -172,11 +182,16 @@ impl VaultCredentialDriver {
172182
&self,
173183
requests: Vec<ResolveCredentialRequest>,
174184
) -> Result<Vec<ResolvedCredential>, Status> {
175-
let token = self.auth_token().await?;
176185
let mut responses = Vec::with_capacity(requests.len());
186+
let mut resolved_requests = Vec::with_capacity(requests.len());
177187
for request in requests {
178188
let handle = Self::handle_from_request(&request.request_id, request.handle)?;
179189
let logical_path = Self::logical_path_from_handle(&handle)?;
190+
validate_managed_secret_path(
191+
&request.provider_name,
192+
&request.credential_key,
193+
&logical_path,
194+
)?;
180195
let reference = VaultSecretReference {
181196
api_path: api_path_for_reference(
182197
&self.settings.mount,
@@ -186,9 +201,14 @@ impl VaultCredentialDriver {
186201
key: STORED_VALUE_KEY.to_string(),
187202
kv_version: self.settings.kv_version,
188203
};
204+
resolved_requests.push((request.request_id, reference));
205+
}
206+
207+
let token = self.auth_token().await?;
208+
for (request_id, reference) in resolved_requests {
189209
let value = self.resolve_secret_value(&reference, &token).await?;
190210
responses.push(ResolvedCredential {
191-
request_id: request.request_id,
211+
request_id,
192212
value,
193213
expires_at_ms: 0,
194214
});
@@ -690,6 +710,20 @@ fn managed_secret_path(provider_name: &str, credential_key: &str) -> String {
690710
format!("openshell/provider-credentials/{}", &hex[..40])
691711
}
692712

713+
fn validate_managed_secret_path(
714+
provider_name: &str,
715+
credential_key: &str,
716+
logical_path: &str,
717+
) -> Result<(), Status> {
718+
let expected = managed_secret_path(provider_name, credential_key);
719+
if logical_path == expected {
720+
return Ok(());
721+
}
722+
Err(Status::invalid_argument(format!(
723+
"vault credential handle path does not match the managed path for provider credential '{credential_key}'"
724+
)))
725+
}
726+
693727
async fn read_secret_file(path: &Path, description: &str) -> Result<String, Status> {
694728
let contents = tokio::fs::read_to_string(path).await.map_err(|err| {
695729
Status::unauthenticated(format!(
@@ -920,6 +954,19 @@ mod tests {
920954
assert!(err.message().contains("path segments"));
921955
}
922956

957+
#[test]
958+
fn handle_rejects_unexpected_managed_path() {
959+
let err = validate_managed_secret_path(
960+
"nvidia-prod",
961+
"NVIDIA_API_KEY",
962+
"openshell/provider-credentials/other",
963+
)
964+
.unwrap_err();
965+
966+
assert_eq!(err.code(), Code::InvalidArgument);
967+
assert!(err.message().contains("managed path"));
968+
}
969+
923970
#[tokio::test]
924971
async fn store_and_resolve_token_file_kv2_secret() {
925972
let mock_server = MockServer::start().await;
@@ -985,10 +1032,9 @@ mod tests {
9851032
#[tokio::test]
9861033
async fn store_with_existing_handle_reuses_logical_path() {
9871034
let mock_server = MockServer::start().await;
1035+
let logical_path = managed_secret_path("nvidia-prod", "NVIDIA_API_KEY");
9881036
Mock::given(method("POST"))
989-
.and(path(
990-
"/v1/secret/data/openshell/provider-credentials/existing",
991-
))
1037+
.and(path(format!("/v1/secret/data/{logical_path}")))
9921038
.and(header("x-vault-token", "dev-token"))
9931039
.and(body_string_contains("updated-secret"))
9941040
.respond_with(ResponseTemplate::new(200))
@@ -1010,21 +1056,20 @@ mod tests {
10101056
provider_name: "nvidia-prod".to_string(),
10111057
credential_key: "NVIDIA_API_KEY".to_string(),
10121058
value: "updated-secret".to_string(),
1013-
existing_handle: Some(handle("v1:openshell/provider-credentials/existing")),
1059+
existing_handle: Some(handle(&format!("v1:{logical_path}"))),
10141060
})
10151061
.await
10161062
.unwrap();
10171063

1018-
assert_eq!(stored.handle, "v1:openshell/provider-credentials/existing");
1064+
assert_eq!(stored.handle, format!("v1:{logical_path}"));
10191065
}
10201066

10211067
#[tokio::test]
10221068
async fn delete_token_file_kv2_secret() {
10231069
let mock_server = MockServer::start().await;
1070+
let logical_path = managed_secret_path("nvidia-prod", "NVIDIA_API_KEY");
10241071
Mock::given(method("DELETE"))
1025-
.and(path(
1026-
"/v1/secret/metadata/openshell/provider-credentials/existing",
1027-
))
1072+
.and(path(format!("/v1/secret/metadata/{logical_path}")))
10281073
.and(header("x-vault-token", "dev-token"))
10291074
.respond_with(ResponseTemplate::new(204))
10301075
.mount(&mock_server)
@@ -1044,7 +1089,7 @@ mod tests {
10441089
.delete_credential(DeleteCredentialRequest {
10451090
provider_name: "nvidia-prod".to_string(),
10461091
credential_key: "NVIDIA_API_KEY".to_string(),
1047-
handle: Some(handle("v1:openshell/provider-credentials/existing")),
1092+
handle: Some(handle(&format!("v1:{logical_path}"))),
10481093
})
10491094
.await
10501095
.unwrap();
@@ -1105,10 +1150,9 @@ mod tests {
11051150
#[tokio::test]
11061151
async fn resolve_maps_missing_key() {
11071152
let mock_server = MockServer::start().await;
1153+
let logical_path = managed_secret_path("nvidia-prod", "NVIDIA_API_KEY");
11081154
Mock::given(method("GET"))
1109-
.and(path(
1110-
"/v1/secret/data/openshell/provider-credentials/missing-key",
1111-
))
1155+
.and(path(format!("/v1/secret/data/{logical_path}")))
11121156
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
11131157
"data": {
11141158
"data": {}
@@ -1132,7 +1176,7 @@ mod tests {
11321176
request_id: "credential-0".to_string(),
11331177
provider_name: "nvidia-prod".to_string(),
11341178
credential_key: "NVIDIA_API_KEY".to_string(),
1135-
handle: Some(handle("v1:openshell/provider-credentials/missing-key")),
1179+
handle: Some(handle(&format!("v1:{logical_path}"))),
11361180
}])
11371181
.await
11381182
.unwrap_err();
@@ -1144,10 +1188,9 @@ mod tests {
11441188
#[tokio::test]
11451189
async fn resolve_maps_permission_denied() {
11461190
let mock_server = MockServer::start().await;
1191+
let logical_path = managed_secret_path("nvidia-prod", "NVIDIA_API_KEY");
11471192
Mock::given(method("GET"))
1148-
.and(path(
1149-
"/v1/secret/data/openshell/provider-credentials/denied",
1150-
))
1193+
.and(path(format!("/v1/secret/data/{logical_path}")))
11511194
.respond_with(ResponseTemplate::new(403))
11521195
.mount(&mock_server)
11531196
.await;
@@ -1167,7 +1210,7 @@ mod tests {
11671210
request_id: "credential-0".to_string(),
11681211
provider_name: "nvidia-prod".to_string(),
11691212
credential_key: "NVIDIA_API_KEY".to_string(),
1170-
handle: Some(handle("v1:openshell/provider-credentials/denied")),
1213+
handle: Some(handle(&format!("v1:{logical_path}"))),
11711214
}])
11721215
.await
11731216
.unwrap_err();

crates/openshell-server/src/credentials.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ impl CredentialRuntime {
193193
self.drivers.contains_key(&driver_name)
194194
}
195195

196+
pub fn storage_owns_handle(&self, handle: &CredentialHandle) -> bool {
197+
normalize_driver_name(&handle.driver) == self.registry.storage_owner_name()
198+
}
199+
196200
pub async fn store_provider_credentials(
197201
&self,
198202
provider_name: &str,

0 commit comments

Comments
 (0)