use std::collections::BTreeMap;
use reqwest::{Method, StatusCode};
use serde::{Deserialize, Serialize};
use crate::{
Authenticated, Client, Error, Result,
path::{validate_endpoint_path, validate_mount_path},
response::{
Empty, ResponseEnvelope, deserialize_bounded_string_map_or_default,
deserialize_bounded_string_vec,
},
};
const IDENTITY_LIST_LIMIT: usize = crate::response::MAX_RESPONSE_STRINGS;
#[derive(Debug)]
pub struct Identity<'a> {
client: &'a Client<Authenticated>,
mount: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityEntityRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_string_map_or_default"
)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub policies: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
}
impl IdentityEntityRequest {
pub fn named(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
..Self::default()
}
}
#[must_use]
pub fn with_policy(mut self, policy: impl Into<String>) -> Self {
self.policies.push(policy.into());
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
fn validate(&self) -> Result<()> {
validate_string_count(self.policies.len(), "identity entity policies")?;
validate_string_count(self.metadata.len(), "identity entity metadata")?;
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityEntityInfo {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_string_map_or_default"
)]
pub metadata: BTreeMap<String, String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub policies: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub direct_group_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub inherited_group_ids: Vec<String>,
#[serde(default)]
pub disabled: bool,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityEntityUpsert {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityEntityList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityEntityBatchDeleteRequest {
pub entity_ids: Vec<String>,
}
impl IdentityEntityBatchDeleteRequest {
pub fn new(entity_ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
entity_ids: entity_ids.into_iter().map(Into::into).collect(),
}
}
fn validate(&self) -> Result<()> {
if self.entity_ids.is_empty() {
return Err(Error::InvalidParameter(
"identity entity batch delete requires at least one entity ID".into(),
));
}
validate_string_count(self.entity_ids.len(), "identity entity IDs")?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IdentityGroupType {
Internal,
External,
}
impl IdentityGroupType {
fn as_str(self) -> &'static str {
match self {
Self::Internal => "internal",
Self::External => "external",
}
}
}
impl Serialize for IdentityGroupType {
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for IdentityGroupType {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
match value.as_str() {
"internal" => Ok(Self::Internal),
"external" => Ok(Self::External),
_ => Err(serde::de::Error::custom("unsupported identity group type")),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityGroupRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
pub group_type: Option<IdentityGroupType>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub policies: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub member_entity_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub member_group_ids: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_string_map_or_default"
)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
}
impl IdentityGroupRequest {
pub fn internal(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
group_type: Some(IdentityGroupType::Internal),
..Self::default()
}
}
pub fn external(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
group_type: Some(IdentityGroupType::External),
..Self::default()
}
}
#[must_use]
pub fn with_policy(mut self, policy: impl Into<String>) -> Self {
self.policies.push(policy.into());
self
}
#[must_use]
pub fn with_member_entity_id(mut self, entity_id: impl Into<String>) -> Self {
self.member_entity_ids.push(entity_id.into());
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
fn validate(&self) -> Result<()> {
validate_string_count(self.policies.len(), "identity group policies")?;
validate_string_count(
self.member_entity_ids.len(),
"identity group member entity IDs",
)?;
validate_string_count(
self.member_group_ids.len(),
"identity group member group IDs",
)?;
validate_string_count(self.metadata.len(), "identity group metadata")?;
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityGroupInfo {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default, rename = "type")]
pub group_type: Option<IdentityGroupType>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub policies: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub member_entity_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub member_group_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub parent_group_ids: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_string_map_or_default"
)]
pub metadata: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityGroupUpsert {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityGroupList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityEntityAliasRequest {
pub name: String,
pub canonical_id: String,
pub mount_accessor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_string_map_or_default"
)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub custom_metadata: BTreeMap<String, String>,
}
impl IdentityEntityAliasRequest {
pub fn new(
name: impl Into<String>,
canonical_id: impl Into<String>,
mount_accessor: impl Into<String>,
) -> Self {
Self {
name: name.into(),
canonical_id: canonical_id.into(),
mount_accessor: mount_accessor.into(),
id: None,
custom_metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_custom_metadata(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.custom_metadata.insert(key.into(), value.into());
self
}
fn validate(&self) -> Result<()> {
validate_string_count(self.custom_metadata.len(), "identity entity alias metadata")?;
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityGroupAliasRequest {
pub name: String,
pub canonical_id: String,
pub mount_accessor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
impl IdentityGroupAliasRequest {
pub fn new(
name: impl Into<String>,
canonical_id: impl Into<String>,
mount_accessor: impl Into<String>,
) -> Self {
Self {
name: name.into(),
canonical_id: canonical_id.into(),
mount_accessor: mount_accessor.into(),
id: None,
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityAliasInfo {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub canonical_id: Option<String>,
#[serde(default)]
pub mount_accessor: Option<String>,
#[serde(default)]
pub mount_path: Option<String>,
#[serde(default)]
pub mount_type: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_string_map_or_default"
)]
pub custom_metadata: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityAliasUpsert {
#[serde(default)]
pub id: String,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityAliasList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl Client<Authenticated> {
pub fn identity(&self) -> Result<Identity<'_>> {
self.identity_at("identity")
}
pub fn identity_at(&self, mount: impl Into<String>) -> Result<Identity<'_>> {
let mount = mount.into();
Ok(Identity {
client: self,
mount: validate_mount_path(&mount)?,
})
}
}
impl Identity<'_> {
pub async fn write_entity(
&self,
request: &IdentityEntityRequest,
) -> Result<IdentityEntityUpsert> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityEntityUpsert> = self
.client
.request_json(Method::POST, &self.path(&["entity"])?, Some(request))
.await?;
Ok(envelope.data)
}
pub async fn read_entity_by_id(&self, id: &str) -> Result<IdentityEntityInfo> {
let envelope: ResponseEnvelope<IdentityEntityInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["entity", "id", id])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn update_entity_by_id(
&self,
id: &str,
request: &IdentityEntityRequest,
) -> Result<IdentityEntityUpsert> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityEntityUpsert> = self
.client
.request_json(
Method::POST,
&self.path(&["entity", "id", id])?,
Some(request),
)
.await?;
Ok(envelope.data)
}
pub async fn delete_entity_by_id(&self, id: &str) -> Result<Empty> {
self.delete_at(&["entity", "id", id]).await
}
pub async fn batch_delete_entities(
&self,
request: &IdentityEntityBatchDeleteRequest,
) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.path(&["entity", "batch-delete"])?,
Some(request),
)
.await
}
pub async fn list_entity_ids(&self) -> Result<IdentityEntityList> {
self.list_at(&["entity", "id"]).await
}
pub async fn write_entity_by_name(
&self,
name: &str,
request: &IdentityEntityRequest,
) -> Result<IdentityEntityUpsert> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityEntityUpsert> = self
.client
.request_json(
Method::POST,
&self.path(&["entity", "name", name])?,
Some(request),
)
.await?;
Ok(envelope.data)
}
pub async fn read_entity_by_name(&self, name: &str) -> Result<IdentityEntityInfo> {
let envelope: ResponseEnvelope<IdentityEntityInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["entity", "name", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn delete_entity_by_name(&self, name: &str) -> Result<Empty> {
self.delete_at(&["entity", "name", name]).await
}
pub async fn list_entity_names(&self) -> Result<IdentityEntityList> {
self.list_at(&["entity", "name"]).await
}
pub async fn write_group(&self, request: &IdentityGroupRequest) -> Result<IdentityGroupUpsert> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityGroupUpsert> = self
.client
.request_json(Method::POST, &self.path(&["group"])?, Some(request))
.await?;
Ok(envelope.data)
}
pub async fn read_group_by_id(&self, id: &str) -> Result<IdentityGroupInfo> {
let envelope: ResponseEnvelope<IdentityGroupInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["group", "id", id])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn update_group_by_id(
&self,
id: &str,
request: &IdentityGroupRequest,
) -> Result<IdentityGroupUpsert> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityGroupUpsert> = self
.client
.request_json(
Method::POST,
&self.path(&["group", "id", id])?,
Some(request),
)
.await?;
Ok(envelope.data)
}
pub async fn delete_group_by_id(&self, id: &str) -> Result<Empty> {
self.delete_at(&["group", "id", id]).await
}
pub async fn list_group_ids(&self) -> Result<IdentityGroupList> {
self.list_at(&["group", "id"]).await
}
pub async fn write_group_by_name(
&self,
name: &str,
request: &IdentityGroupRequest,
) -> Result<IdentityGroupUpsert> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityGroupUpsert> = self
.client
.request_json(
Method::POST,
&self.path(&["group", "name", name])?,
Some(request),
)
.await?;
Ok(envelope.data)
}
pub async fn read_group_by_name(&self, name: &str) -> Result<IdentityGroupInfo> {
let envelope: ResponseEnvelope<IdentityGroupInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["group", "name", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn delete_group_by_name(&self, name: &str) -> Result<Empty> {
self.delete_at(&["group", "name", name]).await
}
pub async fn list_group_names(&self) -> Result<IdentityGroupList> {
self.list_at(&["group", "name"]).await
}
pub async fn write_entity_alias(
&self,
request: &IdentityEntityAliasRequest,
) -> Result<IdentityAliasUpsert> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityAliasUpsert> = self
.client
.request_json(Method::POST, &self.path(&["entity-alias"])?, Some(request))
.await?;
Ok(envelope.data)
}
pub async fn read_entity_alias_by_id(&self, id: &str) -> Result<IdentityAliasInfo> {
let envelope: ResponseEnvelope<IdentityAliasInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["entity-alias", "id", id])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn delete_entity_alias_by_id(&self, id: &str) -> Result<Empty> {
self.delete_at(&["entity-alias", "id", id]).await
}
pub async fn list_entity_alias_ids(&self) -> Result<IdentityAliasList> {
self.list_at(&["entity-alias", "id"]).await
}
pub async fn write_group_alias(
&self,
request: &IdentityGroupAliasRequest,
) -> Result<IdentityAliasUpsert> {
let envelope: ResponseEnvelope<IdentityAliasUpsert> = self
.client
.request_json(Method::POST, &self.path(&["group-alias"])?, Some(request))
.await?;
Ok(envelope.data)
}
pub async fn read_group_alias_by_id(&self, id: &str) -> Result<IdentityAliasInfo> {
let envelope: ResponseEnvelope<IdentityAliasInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["group-alias", "id", id])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn delete_group_alias_by_id(&self, id: &str) -> Result<Empty> {
self.delete_at(&["group-alias", "id", id]).await
}
pub async fn list_group_alias_ids(&self) -> Result<IdentityAliasList> {
self.list_at(&["group-alias", "id"]).await
}
async fn list_at<T>(&self, tail: &[&str]) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
let envelope: ResponseEnvelope<T> = self
.client
.request_json_query_accepting(
method,
&self.path(tail)?,
&[],
Option::<&Empty>::None,
&[StatusCode::OK],
)
.await?;
Ok(envelope.data)
}
async fn delete_at(&self, tail: &[&str]) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&self.path(tail)?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
fn path(&self, tail: &[&str]) -> Result<String> {
let mut segments = self.mount.clone();
for segment in tail {
segments.extend(validate_endpoint_path(segment)?);
}
Ok(segments.join("/"))
}
}
fn validate_string_count(count: usize, field: &'static str) -> Result<()> {
if count <= IDENTITY_LIST_LIMIT {
return Ok(());
}
Err(Error::InvalidParameter(format!(
"{field} exceeds maximum item count"
)))
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
#![allow(deprecated)]
use secrecy::SecretString;
use crate::{Client, OpenBaoConfig};
use super::{
IdentityAliasList, IdentityEntityBatchDeleteRequest, IdentityEntityList,
IdentityEntityRequest, IdentityGroupList, IdentityGroupRequest,
};
#[test]
fn identity_paths_are_validated() {
let config = OpenBaoConfig::new("http://127.0.0.1:8200")
.and_then(OpenBaoConfig::allow_localhost_http)
.unwrap_or_else(|error| panic!("{error}"));
let client = Client::from_config(config)
.unwrap_or_else(|error| panic!("{error}"))
.with_token(SecretString::from("token"));
let identity = client
.identity_at("identity")
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
identity
.path(&["entity", "name", "app"])
.unwrap_or_else(|error| panic!("{error}")),
"identity/entity/name/app"
);
assert!(client.identity_at("../identity").is_err());
assert!(identity.path(&["entity", "name", "../app"]).is_err());
}
#[test]
fn identity_lists_are_bounded() {
let mut keys = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.push(format!("identity-{index}"));
}
let value = serde_json::json!({ "keys": keys });
assert!(serde_json::from_value::<IdentityEntityList>(value.clone()).is_err());
assert!(serde_json::from_value::<IdentityGroupList>(value.clone()).is_err());
assert!(serde_json::from_value::<IdentityAliasList>(value).is_err());
}
#[test]
fn identity_request_counts_are_bounded() {
let mut entity = IdentityEntityRequest::named("app");
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
entity.policies.push(format!("policy-{index}"));
}
assert!(entity.validate().is_err());
let mut group = IdentityGroupRequest::internal("app");
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
group.member_entity_ids.push(format!("entity-{index}"));
}
assert!(group.validate().is_err());
let batch = IdentityEntityBatchDeleteRequest::new(Vec::<String>::new());
assert!(batch.validate().is_err());
}
}