blob: e5a70b6c3157b8b1aadc7e283652215fd7b02ad3 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::{advance, invalidate_test, status, status_basis, write_secret};
use crate::{ConfigError, ConfigurationStore, Result, TransactionMode, catalog};
use maka_agent_facts::configuration::headers::*;
use maka_agent_facts::configuration::*;
use sqlx::SqliteConnection;
use std::collections::BTreeMap;
fn locator(id: String) -> CredentialLocator {
CredentialLocator::Connection {
connection_id: id,
kind: ConnectionCredentialKind::RequestHeaders,
}
}
async fn read(tx: &mut SqliteConnection, locator: &CredentialLocator) -> Result<Option<String>> {
sqlx::query_scalar("SELECT secret FROM credentials WHERE locator = ?")
.bind(serde_json::to_string(locator)?)
.fetch_optional(tx)
.await
.map_err(Into::into)
}
fn parse(secret: Option<&str>) -> Result<BTreeMap<String, String>> {
validation::parse_headers(secret.unwrap_or("{}")).map_err(ConfigError::Invalid)
}
impl ConfigurationStore {
pub async fn request_headers(&self, id: String) -> Result<RequestHeadersQueryResult> {
validation::entity_id(&id).map_err(ConfigError::Invalid)?;
self.transaction(TransactionMode::Deferred, move |tx| {
Box::pin(async move {
let Some(connection) = catalog::find(tx, &id).await? else {
return Ok(RequestHeadersQueryResult::ConnectionNotFound);
};
let locator = locator(id);
let secret = read(tx, &locator).await?;
Ok(RequestHeadersQueryResult::Found {
names: parse(secret.as_deref())?.into_keys().collect(),
basis: RequestHeadersBasis {
connection: catalog::basis(&connection),
credential: status_basis(&status(tx, &locator).await?),
},
})
})
})
.await
}
/// Complete replacement under the vault's existing transaction owner.
/// The name snapshot's connection and credential bases must still match.
/// Missing values retain that known name's value; omitted names are deleted.
pub async fn replace_request_headers(
&self,
mut input: RequestHeadersReplace,
now: u64,
) -> Result<RequestHeadersReplaceResult> {
input.normalize().map_err(ConfigError::Invalid)?;
validation::revision(now, false).map_err(ConfigError::Invalid)?;
self.transaction(TransactionMode::Immediate, move |tx| {
Box::pin(async move {
let id = &input.expected.connection.connection_id;
let Some(connection) = catalog::find(tx, id).await? else {
return Ok(RequestHeadersReplaceResult::ConnectionNotFound);
};
let actual_connection = catalog::basis(&connection);
if actual_connection != input.expected.connection {
return Ok(RequestHeadersReplaceResult::ConnectionStale {
expected: input.expected.connection,
actual: actual_connection,
});
}
let locator = locator(id.clone());
let actual_credential = status_basis(&status(tx, &locator).await?);
if actual_credential != input.expected.credential {
return Ok(RequestHeadersReplaceResult::CredentialStale {
expected: input.expected.credential,
actual: actual_credential,
});
}
let previous = read(tx, &locator).await?;
let saved = parse(previous.as_deref())?;
let by_name: BTreeMap<_, _> = saved
.iter()
.map(|(name, value)| (name.to_ascii_lowercase(), value))
.collect();
let mut headers = BTreeMap::new();
for update in input.headers {
let value =
match update.value {
Some(value) => value,
None => (*by_name.get(&update.name.to_ascii_lowercase()).ok_or_else(
|| {
ConfigError::Invalid(
"new request header requires a value".into(),
)
},
)?)
.clone(),
};
headers.insert(update.name, value);
}
// Retained values participate in the aggregate bound too.
let secret = validation::normalize_headers(&serde_json::to_string(&headers)?)
.map_err(ConfigError::Invalid)?;
let names = headers.keys().cloned().collect();
if saved == headers && !(headers.is_empty() && previous.is_some()) {
return Ok(RequestHeadersReplaceResult::Unchanged {
names,
basis: input.expected,
});
}
if headers.is_empty() {
sqlx::query("DELETE FROM credentials WHERE locator = ?")
.bind(serde_json::to_string(&locator)?)
.execute(&mut *tx)
.await?;
} else {
write_secret(tx, &locator, &secret, now).await?;
}
invalidate_test(tx, &locator).await?;
advance(tx).await?;
let connection = catalog::find(tx, id)
.await?
.ok_or_else(|| ConfigError::Invalid("connection disappeared".into()))?;
Ok(RequestHeadersReplaceResult::Committed {
names,
basis: RequestHeadersBasis {
connection: catalog::basis(&connection),
credential: status_basis(&status(tx, &locator).await?),
},
})
})
})
.await
}
}