use std::collections::BTreeMap;
use std::fmt;
use reqwest::{Method, StatusCode};
use secrecy::{ExposeSecret, SecretString};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value as JsonValue;
use crate::{
Authenticated, Client, Error, Result,
path::{validate_endpoint_path, validate_mount_path},
response::{
Empty, ListEntries, ResponseEnvelope, deserialize_bounded_string_map_or_default,
deserialize_bounded_string_vec,
},
validation::validate_duration_parameter,
};
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>,
}
impl ListEntries for IdentityEntityList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[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, Debug, Default, Serialize)]
pub struct IdentityEntityLookupRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias_mount_accessor: Option<String>,
}
impl IdentityEntityLookupRequest {
pub fn by_id(id: impl Into<String>) -> Self {
Self {
id: Some(id.into()),
..Self::default()
}
}
pub fn by_name(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
..Self::default()
}
}
pub fn by_alias(
alias_name: impl Into<String>,
alias_mount_accessor: impl Into<String>,
) -> Self {
Self {
alias_name: Some(alias_name.into()),
alias_mount_accessor: Some(alias_mount_accessor.into()),
..Self::default()
}
}
fn validate(&self) -> Result<()> {
let identifiers = [
self.id.as_ref(),
self.name.as_ref(),
self.alias_id.as_ref(),
self.alias_name.as_ref(),
]
.into_iter()
.flatten()
.count();
if identifiers == 0 {
return Err(Error::InvalidParameter(
"identity entity lookup requires an id, name, alias_id, or alias_name".into(),
));
}
if [
self.id.as_ref(),
self.name.as_ref(),
self.alias_id.as_ref(),
self.alias_name.as_ref(),
self.alias_mount_accessor.as_ref(),
]
.into_iter()
.flatten()
.any(|value| value.trim().is_empty())
{
return Err(Error::InvalidParameter(
"identity entity lookup fields must not be empty".into(),
));
}
if self.alias_name.is_some() && self.alias_mount_accessor.is_none() {
return Err(Error::InvalidParameter(
"identity alias lookup requires alias_mount_accessor".into(),
));
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityEntityMergeRequest {
pub to_entity_id: String,
pub from_entity_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub force: Option<bool>,
}
impl IdentityEntityMergeRequest {
pub fn new(
to_entity_id: impl Into<String>,
from_entity_ids: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
to_entity_id: to_entity_id.into(),
from_entity_ids: from_entity_ids.into_iter().map(Into::into).collect(),
force: None,
}
}
#[must_use]
pub fn force(mut self) -> Self {
self.force = Some(true);
self
}
fn validate(&self) -> Result<()> {
if self.to_entity_id.trim().is_empty() {
return Err(Error::InvalidParameter(
"identity merge target entity ID must not be empty".into(),
));
}
if self.from_entity_ids.is_empty() {
return Err(Error::InvalidParameter(
"identity merge requires at least one source entity ID".into(),
));
}
validate_string_count(
self.from_entity_ids.len(),
"identity merge source entity IDs",
)
}
}
#[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, Serialize)]
pub struct IdentityGroupLookupRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias_mount_accessor: Option<String>,
}
impl IdentityGroupLookupRequest {
pub fn by_id(id: impl Into<String>) -> Self {
Self {
id: Some(id.into()),
..Self::default()
}
}
pub fn by_name(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
..Self::default()
}
}
pub fn by_alias(
alias_name: impl Into<String>,
alias_mount_accessor: impl Into<String>,
) -> Self {
Self {
alias_name: Some(alias_name.into()),
alias_mount_accessor: Some(alias_mount_accessor.into()),
..Self::default()
}
}
fn validate(&self) -> Result<()> {
let identifiers = [
self.id.as_ref(),
self.name.as_ref(),
self.alias_id.as_ref(),
self.alias_name.as_ref(),
]
.into_iter()
.flatten()
.count();
if identifiers == 0 {
return Err(Error::InvalidParameter(
"identity group lookup requires an id, name, alias_id, or alias_name".into(),
));
}
if [
self.id.as_ref(),
self.name.as_ref(),
self.alias_id.as_ref(),
self.alias_name.as_ref(),
self.alias_mount_accessor.as_ref(),
]
.into_iter()
.flatten()
.any(|value| value.trim().is_empty())
{
return Err(Error::InvalidParameter(
"identity group lookup fields must not be empty".into(),
));
}
if self.alias_name.is_some() && self.alias_mount_accessor.is_none() {
return Err(Error::InvalidParameter(
"identity group alias lookup requires alias_mount_accessor".into(),
));
}
Ok(())
}
}
impl ListEntries for IdentityGroupList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[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 ListEntries for IdentityAliasList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityOidcConfigRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
}
impl IdentityOidcConfigRequest {
pub fn new(issuer: impl Into<String>) -> Self {
Self {
issuer: Some(issuer.into()),
}
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcConfig {
#[serde(default)]
pub issuer: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityOidcKeyRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub rotation_period: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_ttl: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_client_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub algorithm: Option<String>,
}
impl IdentityOidcKeyRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_rotation_period(mut self, rotation_period: impl Into<String>) -> Self {
self.rotation_period = Some(rotation_period.into());
self
}
#[must_use]
pub fn with_verification_ttl(mut self, verification_ttl: impl Into<String>) -> Self {
self.verification_ttl = Some(verification_ttl.into());
self
}
#[must_use]
pub fn with_allowed_client_id(mut self, client_id: impl Into<String>) -> Self {
self.allowed_client_ids.push(client_id.into());
self
}
#[must_use]
pub fn with_algorithm(mut self, algorithm: impl Into<String>) -> Self {
self.algorithm = Some(algorithm.into());
self
}
fn validate(&self) -> Result<()> {
if let Some(rotation_period) = &self.rotation_period {
validate_duration_parameter(rotation_period, "identity OIDC key rotation_period")?;
}
if let Some(verification_ttl) = &self.verification_ttl {
validate_duration_parameter(verification_ttl, "identity OIDC key verification_ttl")?;
}
validate_string_count(
self.allowed_client_ids.len(),
"identity OIDC allowed client IDs",
)
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityOidcKeyRotateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_ttl: Option<String>,
}
impl IdentityOidcKeyRotateRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_verification_ttl(mut self, verification_ttl: impl Into<String>) -> Self {
self.verification_ttl = Some(verification_ttl.into());
self
}
fn validate(&self) -> Result<()> {
if let Some(verification_ttl) = &self.verification_ttl {
validate_duration_parameter(
verification_ttl,
"identity OIDC key rotation verification_ttl",
)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcKeyInfo {
#[serde(default)]
pub algorithm: Option<String>,
#[serde(default)]
pub rotation_period: Option<u64>,
#[serde(default)]
pub verification_ttl: Option<u64>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub allowed_client_ids: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityOidcKeyList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for IdentityOidcKeyList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityOidcRoleRequest {
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
}
impl IdentityOidcRoleRequest {
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
..Self::default()
}
}
#[must_use]
pub fn with_template(mut self, template: impl Into<String>) -> Self {
self.template = Some(template.into());
self
}
#[must_use]
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
#[must_use]
pub fn with_ttl(mut self, ttl: impl Into<String>) -> Self {
self.ttl = Some(ttl.into());
self
}
fn validate(&self) -> Result<()> {
if self.key.trim().is_empty() {
return Err(Error::InvalidParameter(
"identity OIDC role key must not be empty".into(),
));
}
if let Some(ttl) = &self.ttl {
validate_duration_parameter(ttl, "identity OIDC role ttl")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcRoleInfo {
#[serde(default)]
pub client_id: Option<String>,
#[serde(default)]
pub key: Option<String>,
#[serde(default)]
pub template: Option<String>,
#[serde(default)]
pub ttl: Option<u64>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityOidcRoleList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for IdentityOidcRoleList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Deserialize)]
pub struct IdentityOidcToken {
#[serde(default)]
pub client_id: Option<String>,
pub token: SecretString,
#[serde(default)]
pub ttl: Option<u64>,
}
impl fmt::Debug for IdentityOidcToken {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityOidcToken")
.field("client_id", &self.client_id)
.field("token", &"<redacted>")
.field("ttl", &self.ttl)
.finish()
}
}
#[derive(Clone)]
pub struct IdentityOidcIntrospectRequest {
pub token: SecretString,
pub client_id: Option<String>,
}
impl IdentityOidcIntrospectRequest {
pub fn new(token: SecretString) -> Self {
Self {
token,
client_id: None,
}
}
#[must_use]
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
}
impl fmt::Debug for IdentityOidcIntrospectRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityOidcIntrospectRequest")
.field("token", &"<redacted>")
.field("client_id", &self.client_id)
.finish()
}
}
#[derive(Clone, Debug, Default)]
pub struct IdentityOidcIntrospection {
pub active: bool,
pub extra: BTreeMap<String, JsonValue>,
}
impl<'de> Deserialize<'de> for IdentityOidcIntrospection {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let mut extra = deserialize_bounded_json_map(deserializer)?;
let active = extra
.remove("active")
.map(serde_json::from_value::<bool>)
.transpose()
.map_err(serde::de::Error::custom)?
.unwrap_or(false);
Ok(Self { active, extra })
}
}
#[derive(Clone, Debug, Default)]
pub struct IdentityOidcDiscovery {
pub issuer: Option<String>,
pub authorization_endpoint: Option<String>,
pub token_endpoint: Option<String>,
pub jwks_uri: Option<String>,
pub response_types_supported: Option<Vec<String>>,
pub subject_types_supported: Option<Vec<String>>,
pub id_token_signing_alg_values_supported: Option<Vec<String>>,
pub scopes_supported: Option<Vec<String>>,
pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
pub claims_supported: Option<Vec<String>>,
pub extra: BTreeMap<String, JsonValue>,
}
impl<'de> Deserialize<'de> for IdentityOidcDiscovery {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let mut extra = deserialize_bounded_json_map(deserializer)?;
Ok(Self {
issuer: take_optional_string::<D::Error>(&mut extra, "issuer")?,
authorization_endpoint: take_optional_string::<D::Error>(
&mut extra,
"authorization_endpoint",
)?,
token_endpoint: take_optional_string::<D::Error>(&mut extra, "token_endpoint")?,
jwks_uri: take_optional_string::<D::Error>(&mut extra, "jwks_uri")?,
response_types_supported: take_optional_string_vec::<D::Error>(
&mut extra,
"response_types_supported",
)?,
subject_types_supported: take_optional_string_vec::<D::Error>(
&mut extra,
"subject_types_supported",
)?,
id_token_signing_alg_values_supported: take_optional_string_vec::<D::Error>(
&mut extra,
"id_token_signing_alg_values_supported",
)?,
scopes_supported: take_optional_string_vec::<D::Error>(&mut extra, "scopes_supported")?,
token_endpoint_auth_methods_supported: take_optional_string_vec::<D::Error>(
&mut extra,
"token_endpoint_auth_methods_supported",
)?,
claims_supported: take_optional_string_vec::<D::Error>(&mut extra, "claims_supported")?,
extra,
})
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcJwks {
#[serde(default, deserialize_with = "deserialize_bounded_json_vec")]
pub keys: Vec<JsonValue>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityOidcProviderRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_client_ids: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub scopes_supported: Vec<String>,
}
impl IdentityOidcProviderRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
#[must_use]
pub fn with_allowed_client_id(mut self, client_id: impl Into<String>) -> Self {
self.allowed_client_ids.push(client_id.into());
self
}
#[must_use]
pub fn with_scope_supported(mut self, scope: impl Into<String>) -> Self {
self.scopes_supported.push(scope.into());
self
}
fn validate(&self) -> Result<()> {
validate_string_count(
self.allowed_client_ids.len(),
"identity OIDC provider allowed client IDs",
)?;
validate_string_count(
self.scopes_supported.len(),
"identity OIDC provider supported scopes",
)
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcProviderInfo {
#[serde(default)]
pub issuer: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub allowed_client_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub scopes_supported: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcProviderList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_bounded_oidc_provider_info_map"
)]
pub key_info: BTreeMap<String, IdentityOidcProviderInfo>,
}
impl ListEntries for IdentityOidcProviderList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityOidcScopeRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl IdentityOidcScopeRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_template(mut self, template: impl Into<String>) -> Self {
self.template = Some(template.into());
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcScopeInfo {
#[serde(default)]
pub template: Option<String>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityOidcScopeList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for IdentityOidcScopeList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IdentityOidcClientType {
Confidential,
Public,
}
impl IdentityOidcClientType {
fn as_str(self) -> &'static str {
match self {
Self::Confidential => "confidential",
Self::Public => "public",
}
}
}
impl Serialize for IdentityOidcClientType {
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 IdentityOidcClientType {
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() {
"confidential" => Ok(Self::Confidential),
"public" => Ok(Self::Public),
_ => Err(serde::de::Error::custom(
"unsupported identity OIDC client type",
)),
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityOidcClientRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub redirect_uris: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub assignments: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_type: Option<IdentityOidcClientType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_token_ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_token_ttl: Option<String>,
}
impl IdentityOidcClientRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
#[must_use]
pub fn with_redirect_uri(mut self, redirect_uri: impl Into<String>) -> Self {
self.redirect_uris.push(redirect_uri.into());
self
}
#[must_use]
pub fn with_assignment(mut self, assignment: impl Into<String>) -> Self {
self.assignments.push(assignment.into());
self
}
#[must_use]
pub fn with_client_type(mut self, client_type: IdentityOidcClientType) -> Self {
self.client_type = Some(client_type);
self
}
#[must_use]
pub fn with_id_token_ttl(mut self, ttl: impl Into<String>) -> Self {
self.id_token_ttl = Some(ttl.into());
self
}
#[must_use]
pub fn with_access_token_ttl(mut self, ttl: impl Into<String>) -> Self {
self.access_token_ttl = Some(ttl.into());
self
}
fn validate(&self) -> Result<()> {
validate_string_count(
self.redirect_uris.len(),
"identity OIDC client redirect URIs",
)?;
validate_string_count(self.assignments.len(), "identity OIDC client assignments")?;
if let Some(ttl) = &self.id_token_ttl {
validate_duration_parameter(ttl, "identity OIDC client id_token_ttl")?;
}
if let Some(ttl) = &self.access_token_ttl {
validate_duration_parameter(ttl, "identity OIDC client access_token_ttl")?;
}
Ok(())
}
}
#[derive(Clone, Default, Deserialize)]
pub struct IdentityOidcClientInfo {
#[serde(default)]
pub access_token_ttl: Option<u64>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub assignments: Vec<String>,
#[serde(default)]
pub client_id: Option<String>,
#[serde(default)]
pub client_secret: Option<SecretString>,
#[serde(default)]
pub client_type: Option<IdentityOidcClientType>,
#[serde(default)]
pub id_token_ttl: Option<u64>,
#[serde(default)]
pub key: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub redirect_uris: Vec<String>,
}
impl fmt::Debug for IdentityOidcClientInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityOidcClientInfo")
.field("access_token_ttl", &self.access_token_ttl)
.field("assignments", &self.assignments)
.field("client_id", &self.client_id)
.field(
"client_secret",
&self.client_secret.as_ref().map(|_| "<redacted>"),
)
.field("client_type", &self.client_type)
.field("id_token_ttl", &self.id_token_ttl)
.field("key", &self.key)
.field("redirect_uris", &self.redirect_uris)
.finish()
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcClientList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_oidc_client_info_map")]
pub key_info: BTreeMap<String, IdentityOidcClientInfo>,
}
impl ListEntries for IdentityOidcClientList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityOidcAssignmentRequest {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub entity_ids: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub group_ids: Vec<String>,
}
impl IdentityOidcAssignmentRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_entity_id(mut self, entity_id: impl Into<String>) -> Self {
self.entity_ids.push(entity_id.into());
self
}
#[must_use]
pub fn with_group_id(mut self, group_id: impl Into<String>) -> Self {
self.group_ids.push(group_id.into());
self
}
fn validate(&self) -> Result<()> {
validate_string_count(self.entity_ids.len(), "identity OIDC assignment entity IDs")?;
validate_string_count(self.group_ids.len(), "identity OIDC assignment group IDs")
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityOidcAssignmentInfo {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub entity_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub group_ids: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityOidcAssignmentList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for IdentityOidcAssignmentList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone)]
pub struct IdentityMfaDuoMethodRequest {
pub method_name: String,
pub username_format: Option<String>,
pub secret_key: SecretString,
pub integration_key: SecretString,
pub api_hostname: String,
pub push_info: Option<String>,
pub use_passcode: Option<bool>,
}
impl IdentityMfaDuoMethodRequest {
pub fn new(
method_name: impl Into<String>,
secret_key: SecretString,
integration_key: SecretString,
api_hostname: impl Into<String>,
) -> Self {
Self {
method_name: method_name.into(),
username_format: None,
secret_key,
integration_key,
api_hostname: api_hostname.into(),
push_info: None,
use_passcode: None,
}
}
#[must_use]
pub fn with_username_format(mut self, username_format: impl Into<String>) -> Self {
self.username_format = Some(username_format.into());
self
}
#[must_use]
pub fn with_push_info(mut self, push_info: impl Into<String>) -> Self {
self.push_info = Some(push_info.into());
self
}
#[must_use]
pub fn with_use_passcode(mut self, use_passcode: bool) -> Self {
self.use_passcode = Some(use_passcode);
self
}
fn validate(&self) -> Result<()> {
validate_required(&self.method_name, "identity MFA Duo method_name")?;
validate_required(&self.api_hostname, "identity MFA Duo api_hostname")
}
}
impl fmt::Debug for IdentityMfaDuoMethodRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityMfaDuoMethodRequest")
.field("method_name", &self.method_name)
.field("username_format", &self.username_format)
.field("secret_key", &"<redacted>")
.field("integration_key", &"<redacted>")
.field("api_hostname", &self.api_hostname)
.field("push_info", &self.push_info)
.field("use_passcode", &self.use_passcode)
.finish()
}
}
impl Serialize for IdentityMfaDuoMethodRequest {
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("method_name", &self.method_name)?;
serialize_optional_entry(&mut map, "username_format", self.username_format.as_deref())?;
map.serialize_entry("secret_key", self.secret_key.expose_secret())?;
map.serialize_entry("integration_key", self.integration_key.expose_secret())?;
map.serialize_entry("api_hostname", &self.api_hostname)?;
serialize_optional_entry(&mut map, "push_info", self.push_info.as_deref())?;
if let Some(use_passcode) = self.use_passcode {
map.serialize_entry("use_passcode", &use_passcode)?;
}
map.end()
}
}
#[derive(Clone, Deserialize)]
pub struct IdentityMfaDuoMethodInfo {
#[serde(default)]
pub id: Option<String>,
#[serde(default, alias = "name")]
pub method_name: Option<String>,
#[serde(default)]
pub username_format: Option<String>,
#[serde(default)]
pub secret_key: Option<SecretString>,
#[serde(default)]
pub integration_key: Option<SecretString>,
#[serde(default)]
pub api_hostname: Option<String>,
#[serde(default, alias = "pushinfo")]
pub push_info: Option<String>,
#[serde(default)]
pub use_passcode: Option<bool>,
#[serde(default, rename = "type")]
pub method_type: Option<String>,
}
impl fmt::Debug for IdentityMfaDuoMethodInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityMfaDuoMethodInfo")
.field("id", &self.id)
.field("method_name", &self.method_name)
.field("username_format", &self.username_format)
.field(
"secret_key",
&self.secret_key.as_ref().map(|_| "<redacted>"),
)
.field(
"integration_key",
&self.integration_key.as_ref().map(|_| "<redacted>"),
)
.field("api_hostname", &self.api_hostname)
.field("push_info", &self.push_info)
.field("use_passcode", &self.use_passcode)
.field("method_type", &self.method_type)
.finish()
}
}
#[derive(Clone)]
pub struct IdentityMfaOktaMethodRequest {
pub method_name: String,
pub username_format: Option<String>,
pub org_name: String,
pub api_token: SecretString,
pub base_url: Option<String>,
pub primary_email: Option<bool>,
}
impl IdentityMfaOktaMethodRequest {
pub fn new(
method_name: impl Into<String>,
org_name: impl Into<String>,
api_token: SecretString,
) -> Self {
Self {
method_name: method_name.into(),
username_format: None,
org_name: org_name.into(),
api_token,
base_url: None,
primary_email: None,
}
}
#[must_use]
pub fn with_username_format(mut self, username_format: impl Into<String>) -> Self {
self.username_format = Some(username_format.into());
self
}
#[must_use]
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
#[must_use]
pub fn with_primary_email(mut self, primary_email: bool) -> Self {
self.primary_email = Some(primary_email);
self
}
fn validate(&self) -> Result<()> {
validate_required(&self.method_name, "identity MFA Okta method_name")?;
validate_required(&self.org_name, "identity MFA Okta org_name")
}
}
impl fmt::Debug for IdentityMfaOktaMethodRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityMfaOktaMethodRequest")
.field("method_name", &self.method_name)
.field("username_format", &self.username_format)
.field("org_name", &self.org_name)
.field("api_token", &"<redacted>")
.field("base_url", &self.base_url)
.field("primary_email", &self.primary_email)
.finish()
}
}
impl Serialize for IdentityMfaOktaMethodRequest {
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("method_name", &self.method_name)?;
serialize_optional_entry(&mut map, "username_format", self.username_format.as_deref())?;
map.serialize_entry("org_name", &self.org_name)?;
map.serialize_entry("api_token", self.api_token.expose_secret())?;
serialize_optional_entry(&mut map, "base_url", self.base_url.as_deref())?;
if let Some(primary_email) = self.primary_email {
map.serialize_entry("primary_email", &primary_email)?;
}
map.end()
}
}
#[derive(Clone, Deserialize)]
pub struct IdentityMfaOktaMethodInfo {
#[serde(default)]
pub id: Option<String>,
#[serde(default, alias = "name")]
pub method_name: Option<String>,
#[serde(default)]
pub username_format: Option<String>,
#[serde(default)]
pub org_name: Option<String>,
#[serde(default)]
pub api_token: Option<SecretString>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub primary_email: Option<bool>,
#[serde(default, rename = "type")]
pub method_type: Option<String>,
}
impl fmt::Debug for IdentityMfaOktaMethodInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityMfaOktaMethodInfo")
.field("id", &self.id)
.field("method_name", &self.method_name)
.field("username_format", &self.username_format)
.field("org_name", &self.org_name)
.field("api_token", &self.api_token.as_ref().map(|_| "<redacted>"))
.field("base_url", &self.base_url)
.field("primary_email", &self.primary_email)
.field("method_type", &self.method_type)
.finish()
}
}
#[derive(Clone)]
pub struct IdentityMfaPingIdMethodRequest {
pub method_name: String,
pub username_format: Option<String>,
pub settings_file_base64: SecretString,
}
impl IdentityMfaPingIdMethodRequest {
pub fn new(method_name: impl Into<String>, settings_file_base64: SecretString) -> Self {
Self {
method_name: method_name.into(),
username_format: None,
settings_file_base64,
}
}
#[must_use]
pub fn with_username_format(mut self, username_format: impl Into<String>) -> Self {
self.username_format = Some(username_format.into());
self
}
fn validate(&self) -> Result<()> {
validate_required(&self.method_name, "identity MFA PingID method_name")
}
}
impl fmt::Debug for IdentityMfaPingIdMethodRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityMfaPingIdMethodRequest")
.field("method_name", &self.method_name)
.field("username_format", &self.username_format)
.field("settings_file_base64", &"<redacted>")
.finish()
}
}
impl Serialize for IdentityMfaPingIdMethodRequest {
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("method_name", &self.method_name)?;
serialize_optional_entry(&mut map, "username_format", self.username_format.as_deref())?;
map.serialize_entry(
"settings_file_base64",
self.settings_file_base64.expose_secret(),
)?;
map.end()
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityMfaPingIdMethodInfo {
#[serde(default)]
pub id: Option<String>,
#[serde(default, alias = "name")]
pub method_name: Option<String>,
#[serde(default)]
pub username_format: Option<String>,
#[serde(default)]
pub idp_url: Option<String>,
#[serde(default)]
pub admin_url: Option<String>,
#[serde(default)]
pub authenticator_url: Option<String>,
#[serde(default)]
pub org_alias: Option<String>,
#[serde(default)]
pub use_signature: Option<bool>,
#[serde(default, rename = "type")]
pub method_type: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityMfaTotpMethodRequest {
pub method_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub period: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub qr_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub algorithm: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub digits: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skew: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_validation_attempts: Option<u64>,
}
impl IdentityMfaTotpMethodRequest {
pub fn new(method_name: impl Into<String>) -> Self {
Self {
method_name: method_name.into(),
..Self::default()
}
}
#[must_use]
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
fn validate(&self) -> Result<()> {
validate_required(&self.method_name, "identity MFA TOTP method_name")
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityMfaTotpMethodInfo {
#[serde(default)]
pub id: Option<String>,
#[serde(default, alias = "name")]
pub method_name: Option<String>,
#[serde(default)]
pub issuer: Option<String>,
#[serde(default)]
pub period: Option<u64>,
#[serde(default)]
pub key_size: Option<u64>,
#[serde(default)]
pub qr_size: Option<u64>,
#[serde(default)]
pub algorithm: Option<String>,
#[serde(default)]
pub digits: Option<u64>,
#[serde(default)]
pub skew: Option<u64>,
#[serde(default)]
pub max_validation_attempts: Option<u64>,
#[serde(default, rename = "type")]
pub method_type: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityMfaMethodList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for IdentityMfaMethodList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Serialize)]
pub struct IdentityMfaTotpGenerateRequest {
pub method_id: String,
}
impl IdentityMfaTotpGenerateRequest {
pub fn new(method_id: impl Into<String>) -> Self {
Self {
method_id: method_id.into(),
}
}
fn validate(&self) -> Result<()> {
validate_required(&self.method_id, "identity MFA TOTP method_id")
}
}
#[derive(Clone, Debug, Serialize)]
pub struct IdentityMfaTotpAdminRequest {
pub method_id: String,
pub entity_id: String,
}
impl IdentityMfaTotpAdminRequest {
pub fn new(method_id: impl Into<String>, entity_id: impl Into<String>) -> Self {
Self {
method_id: method_id.into(),
entity_id: entity_id.into(),
}
}
fn validate(&self) -> Result<()> {
validate_required(&self.method_id, "identity MFA TOTP method_id")?;
validate_required(&self.entity_id, "identity MFA TOTP entity_id")
}
}
#[derive(Clone, Deserialize)]
pub struct IdentityMfaTotpSecret {
pub barcode: SecretString,
pub url: SecretString,
}
impl fmt::Debug for IdentityMfaTotpSecret {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IdentityMfaTotpSecret")
.field("barcode", &"<redacted>")
.field("url", &"<redacted>")
.finish()
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct IdentityMfaLoginEnforcementRequest {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub mfa_method_ids: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub auth_method_accessors: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub auth_method_types: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub identity_group_ids: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub identity_entity_ids: Vec<String>,
}
impl IdentityMfaLoginEnforcementRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_mfa_method_id(mut self, method_id: impl Into<String>) -> Self {
self.mfa_method_ids.push(method_id.into());
self
}
#[must_use]
pub fn with_auth_method_accessor(mut self, accessor: impl Into<String>) -> Self {
self.auth_method_accessors.push(accessor.into());
self
}
#[must_use]
pub fn with_auth_method_type(mut self, method_type: impl Into<String>) -> Self {
self.auth_method_types.push(method_type.into());
self
}
#[must_use]
pub fn with_identity_group_id(mut self, group_id: impl Into<String>) -> Self {
self.identity_group_ids.push(group_id.into());
self
}
#[must_use]
pub fn with_identity_entity_id(mut self, entity_id: impl Into<String>) -> Self {
self.identity_entity_ids.push(entity_id.into());
self
}
fn validate(&self) -> Result<()> {
if self.mfa_method_ids.is_empty() {
return Err(Error::InvalidParameter(
"identity MFA login enforcement requires at least one MFA method ID".into(),
));
}
if self.auth_method_accessors.is_empty()
&& self.auth_method_types.is_empty()
&& self.identity_group_ids.is_empty()
&& self.identity_entity_ids.is_empty()
{
return Err(Error::InvalidParameter(
"identity MFA login enforcement requires at least one auth or identity condition"
.into(),
));
}
validate_string_count(
self.mfa_method_ids.len(),
"identity MFA login enforcement method IDs",
)?;
validate_string_count(
self.auth_method_accessors.len(),
"identity MFA login enforcement auth method accessors",
)?;
validate_string_count(
self.auth_method_types.len(),
"identity MFA login enforcement auth method types",
)?;
validate_string_count(
self.identity_group_ids.len(),
"identity MFA login enforcement group IDs",
)?;
validate_string_count(
self.identity_entity_ids.len(),
"identity MFA login enforcement entity IDs",
)
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct IdentityMfaLoginEnforcementInfo {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub namespace_id: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub mfa_method_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub auth_method_accessors: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub auth_method_types: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub identity_group_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub identity_entity_ids: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct IdentityMfaLoginEnforcementList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for IdentityMfaLoginEnforcementList {
fn entries(&self) -> &[String] {
&self.keys
}
}
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 lookup_entity(
&self,
request: &IdentityEntityLookupRequest,
) -> Result<IdentityEntityInfo> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityEntityInfo> = self
.client
.request_json(
Method::POST,
&self.path(&["lookup", "entity"])?,
Some(request),
)
.await?;
Ok(envelope.data)
}
pub async fn merge_entities(&self, request: &IdentityEntityMergeRequest) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.path(&["entity", "merge"])?,
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 lookup_group(
&self,
request: &IdentityGroupLookupRequest,
) -> Result<IdentityGroupInfo> {
request.validate()?;
let envelope: ResponseEnvelope<IdentityGroupInfo> = self
.client
.request_json(
Method::POST,
&self.path(&["lookup", "group"])?,
Some(request),
)
.await?;
Ok(envelope.data)
}
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
}
pub async fn write_oidc_config(&self, request: &IdentityOidcConfigRequest) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "config"])?,
Some(request),
)
.await
}
pub async fn read_oidc_config(&self) -> Result<IdentityOidcConfig> {
let envelope: ResponseEnvelope<IdentityOidcConfig> = self
.client
.request_json(
Method::GET,
&self.path(&["oidc", "config"])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn write_oidc_key(
&self,
name: &str,
request: &IdentityOidcKeyRequest,
) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "key", name])?,
Some(request),
)
.await
}
pub async fn read_oidc_key(&self, name: &str) -> Result<IdentityOidcKeyInfo> {
let envelope: ResponseEnvelope<IdentityOidcKeyInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["oidc", "key", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn delete_oidc_key(&self, name: &str) -> Result<Empty> {
self.delete_at(&["oidc", "key", name]).await
}
pub async fn list_oidc_keys(&self) -> Result<IdentityOidcKeyList> {
self.list_at(&["oidc", "key"]).await
}
pub async fn rotate_oidc_key(
&self,
name: &str,
request: &IdentityOidcKeyRotateRequest,
) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "key", name, "rotate"])?,
Some(request),
)
.await
}
pub async fn write_oidc_role(
&self,
name: &str,
request: &IdentityOidcRoleRequest,
) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "role", name])?,
Some(request),
)
.await
}
pub async fn read_oidc_role(&self, name: &str) -> Result<IdentityOidcRoleInfo> {
let envelope: ResponseEnvelope<IdentityOidcRoleInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["oidc", "role", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn delete_oidc_role(&self, name: &str) -> Result<Empty> {
self.delete_at(&["oidc", "role", name]).await
}
pub async fn list_oidc_roles(&self) -> Result<IdentityOidcRoleList> {
self.list_at(&["oidc", "role"]).await
}
pub async fn generate_oidc_token(&self, name: &str) -> Result<IdentityOidcToken> {
let envelope: ResponseEnvelope<IdentityOidcToken> = self
.client
.request_json(
Method::GET,
&self.path(&["oidc", "token", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn introspect_oidc_token(
&self,
request: &IdentityOidcIntrospectRequest,
) -> Result<IdentityOidcIntrospection> {
let payload = IdentityOidcIntrospectPayload {
token: request.token.expose_secret(),
client_id: request.client_id.as_deref(),
};
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "introspect"])?,
Some(&payload),
)
.await
}
pub async fn read_oidc_discovery(&self) -> Result<IdentityOidcDiscovery> {
self.client
.request_json(
Method::GET,
&self.path(&["oidc", ".well-known", "openid-configuration"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn read_oidc_jwks(&self) -> Result<IdentityOidcJwks> {
self.client
.request_json(
Method::GET,
&self.path(&["oidc", ".well-known", "keys"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn write_oidc_provider(
&self,
name: &str,
request: &IdentityOidcProviderRequest,
) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "provider", name])?,
Some(request),
)
.await
}
pub async fn read_oidc_provider(&self, name: &str) -> Result<IdentityOidcProviderInfo> {
let envelope: ResponseEnvelope<IdentityOidcProviderInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["oidc", "provider", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn list_oidc_providers(&self) -> Result<IdentityOidcProviderList> {
self.list_oidc_providers_with_client_id(None).await
}
pub async fn list_oidc_providers_for_client_id(
&self,
client_id: &str,
) -> Result<IdentityOidcProviderList> {
if client_id.trim().is_empty() {
return Err(Error::InvalidParameter(
"identity OIDC provider client_id must not be empty".into(),
));
}
self.list_oidc_providers_with_client_id(Some(client_id))
.await
}
pub async fn delete_oidc_provider(&self, name: &str) -> Result<Empty> {
self.delete_at(&["oidc", "provider", name]).await
}
pub async fn write_oidc_scope(
&self,
name: &str,
request: &IdentityOidcScopeRequest,
) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "scope", name])?,
Some(request),
)
.await
}
pub async fn read_oidc_scope(&self, name: &str) -> Result<IdentityOidcScopeInfo> {
let envelope: ResponseEnvelope<IdentityOidcScopeInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["oidc", "scope", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn list_oidc_scopes(&self) -> Result<IdentityOidcScopeList> {
self.list_at(&["oidc", "scope"]).await
}
pub async fn delete_oidc_scope(&self, name: &str) -> Result<Empty> {
self.delete_at(&["oidc", "scope", name]).await
}
pub async fn write_oidc_client(
&self,
name: &str,
request: &IdentityOidcClientRequest,
) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "client", name])?,
Some(request),
)
.await
}
pub async fn read_oidc_client(&self, name: &str) -> Result<IdentityOidcClientInfo> {
let envelope: ResponseEnvelope<IdentityOidcClientInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["oidc", "client", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn list_oidc_clients(&self) -> Result<IdentityOidcClientList> {
self.list_at(&["oidc", "client"]).await
}
pub async fn delete_oidc_client(&self, name: &str) -> Result<Empty> {
self.delete_at(&["oidc", "client", name]).await
}
pub async fn write_oidc_assignment(
&self,
name: &str,
request: &IdentityOidcAssignmentRequest,
) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.path(&["oidc", "assignment", name])?,
Some(request),
)
.await
}
pub async fn read_oidc_assignment(&self, name: &str) -> Result<IdentityOidcAssignmentInfo> {
let envelope: ResponseEnvelope<IdentityOidcAssignmentInfo> = self
.client
.request_json(
Method::GET,
&self.path(&["oidc", "assignment", name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn list_oidc_assignments(&self) -> Result<IdentityOidcAssignmentList> {
self.list_at(&["oidc", "assignment"]).await
}
pub async fn delete_oidc_assignment(&self, name: &str) -> Result<Empty> {
self.delete_at(&["oidc", "assignment", name]).await
}
pub async fn read_oidc_provider_discovery(&self, name: &str) -> Result<IdentityOidcDiscovery> {
self.client
.request_json(
Method::GET,
&self.path(&[
"oidc",
"provider",
name,
".well-known",
"openid-configuration",
])?,
Option::<&Empty>::None,
)
.await
}
pub async fn read_oidc_provider_jwks(&self, name: &str) -> Result<IdentityOidcJwks> {
self.client
.request_json(
Method::GET,
&self.path(&["oidc", "provider", name, ".well-known", "keys"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn create_mfa_duo_method(
&self,
request: &IdentityMfaDuoMethodRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "duo"], request).await
}
pub async fn write_mfa_duo_method(
&self,
method_id: &str,
request: &IdentityMfaDuoMethodRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "duo", method_id], request)
.await
}
pub async fn read_mfa_duo_method(&self, id: &str) -> Result<IdentityMfaDuoMethodInfo> {
self.read_at(&["mfa", "method", "duo", id]).await
}
pub async fn delete_mfa_duo_method(&self, id: &str) -> Result<Empty> {
self.delete_at(&["mfa", "method", "duo", id]).await
}
pub async fn list_mfa_duo_methods(&self) -> Result<IdentityMfaMethodList> {
self.list_at(&["mfa", "method", "duo"]).await
}
pub async fn create_mfa_okta_method(
&self,
request: &IdentityMfaOktaMethodRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "okta"], request).await
}
pub async fn write_mfa_okta_method(
&self,
method_id: &str,
request: &IdentityMfaOktaMethodRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "okta", method_id], request)
.await
}
pub async fn read_mfa_okta_method(&self, id: &str) -> Result<IdentityMfaOktaMethodInfo> {
self.read_at(&["mfa", "method", "okta", id]).await
}
pub async fn delete_mfa_okta_method(&self, id: &str) -> Result<Empty> {
self.delete_at(&["mfa", "method", "okta", id]).await
}
pub async fn list_mfa_okta_methods(&self) -> Result<IdentityMfaMethodList> {
self.list_at(&["mfa", "method", "okta"]).await
}
pub async fn create_mfa_pingid_method(
&self,
request: &IdentityMfaPingIdMethodRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "pingid"], request).await
}
pub async fn write_mfa_pingid_method(
&self,
method_id: &str,
request: &IdentityMfaPingIdMethodRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "pingid", method_id], request)
.await
}
pub async fn read_mfa_pingid_method(&self, id: &str) -> Result<IdentityMfaPingIdMethodInfo> {
self.read_at(&["mfa", "method", "pingid", id]).await
}
pub async fn delete_mfa_pingid_method(&self, id: &str) -> Result<Empty> {
self.delete_at(&["mfa", "method", "pingid", id]).await
}
pub async fn list_mfa_pingid_methods(&self) -> Result<IdentityMfaMethodList> {
self.list_at(&["mfa", "method", "pingid"]).await
}
pub async fn create_mfa_totp_method(
&self,
request: &IdentityMfaTotpMethodRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "totp"], request).await
}
pub async fn write_mfa_totp_method(
&self,
method_id: &str,
request: &IdentityMfaTotpMethodRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "totp", method_id], request)
.await
}
pub async fn read_mfa_totp_method(&self, id: &str) -> Result<IdentityMfaTotpMethodInfo> {
self.read_at(&["mfa", "method", "totp", id]).await
}
pub async fn delete_mfa_totp_method(&self, id: &str) -> Result<Empty> {
self.delete_at(&["mfa", "method", "totp", id]).await
}
pub async fn list_mfa_totp_methods(&self) -> Result<IdentityMfaMethodList> {
self.list_at(&["mfa", "method", "totp"]).await
}
pub async fn generate_mfa_totp_secret(
&self,
request: &IdentityMfaTotpGenerateRequest,
) -> Result<IdentityMfaTotpSecret> {
request.validate()?;
self.post_data(&["mfa", "method", "totp", "generate"], request)
.await
}
pub async fn admin_generate_mfa_totp_secret(
&self,
request: &IdentityMfaTotpAdminRequest,
) -> Result<IdentityMfaTotpSecret> {
request.validate()?;
self.post_data(&["mfa", "method", "totp", "admin-generate"], request)
.await
}
pub async fn admin_destroy_mfa_totp_secret(
&self,
request: &IdentityMfaTotpAdminRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "method", "totp", "admin-destroy"], request)
.await
}
pub async fn write_mfa_login_enforcement(
&self,
name: &str,
request: &IdentityMfaLoginEnforcementRequest,
) -> Result<Empty> {
request.validate()?;
self.post_empty(&["mfa", "login-enforcement", name], request)
.await
}
pub async fn read_mfa_login_enforcement(
&self,
name: &str,
) -> Result<IdentityMfaLoginEnforcementInfo> {
self.read_at(&["mfa", "login-enforcement", name]).await
}
pub async fn delete_mfa_login_enforcement(&self, name: &str) -> Result<Empty> {
self.delete_at(&["mfa", "login-enforcement", name]).await
}
pub async fn list_mfa_login_enforcements(&self) -> Result<IdentityMfaLoginEnforcementList> {
self.list_at(&["mfa", "login-enforcement"]).await
}
async fn read_at<T>(&self, tail: &[&str]) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let envelope: ResponseEnvelope<T> = self
.client
.request_json(Method::GET, &self.path(tail)?, Option::<&Empty>::None)
.await?;
Ok(envelope.data)
}
async fn post_empty<T>(&self, tail: &[&str], request: &T) -> Result<Empty>
where
T: Serialize + ?Sized,
{
self.client
.request_json(Method::POST, &self.path(tail)?, Some(request))
.await
}
async fn post_data<T, U>(&self, tail: &[&str], request: &T) -> Result<U>
where
T: Serialize + ?Sized,
U: serde::de::DeserializeOwned,
{
let envelope: ResponseEnvelope<U> = self
.client
.request_json(Method::POST, &self.path(tail)?, Some(request))
.await?;
Ok(envelope.data)
}
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 list_oidc_providers_with_client_id(
&self,
client_id: Option<&str>,
) -> Result<IdentityOidcProviderList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
let mut query = Vec::new();
if let Some(client_id) = client_id {
query.push(("client_id", client_id.to_owned()));
}
let envelope: ResponseEnvelope<IdentityOidcProviderList> = self
.client
.request_json_query_accepting(
method,
&self.path(&["oidc", "provider"])?,
&query,
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"
)))
}
fn validate_required(value: &str, field: &'static str) -> Result<()> {
if value.trim().is_empty() {
return Err(Error::InvalidParameter(format!(
"{field} must not be empty"
)));
}
Ok(())
}
fn serialize_optional_entry<S>(
map: &mut S,
key: &'static str,
value: Option<&str>,
) -> core::result::Result<(), S::Error>
where
S: SerializeMap,
{
if let Some(value) = value {
map.serialize_entry(key, value)?;
}
Ok(())
}
fn take_optional_string<E>(
map: &mut BTreeMap<String, JsonValue>,
key: &'static str,
) -> core::result::Result<Option<String>, E>
where
E: serde::de::Error,
{
match map.remove(key) {
None | Some(JsonValue::Null) => Ok(None),
Some(value) => serde_json::from_value::<String>(value)
.map(Some)
.map_err(E::custom),
}
}
fn take_optional_string_vec<E>(
map: &mut BTreeMap<String, JsonValue>,
key: &'static str,
) -> core::result::Result<Option<Vec<String>>, E>
where
E: serde::de::Error,
{
let Some(value) = map.remove(key) else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
let JsonValue::Array(values) = value else {
return Err(E::custom(format!("expected array for field {key}")));
};
if values.len() > IDENTITY_LIST_LIMIT {
return Err(E::custom(
"identity OIDC discovery string list exceeds item limit",
));
}
values
.into_iter()
.map(serde_json::from_value::<String>)
.collect::<core::result::Result<Vec<_>, _>>()
.map(Some)
.map_err(E::custom)
}
#[derive(Serialize)]
struct IdentityOidcIntrospectPayload<'a> {
token: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
client_id: Option<&'a str>,
}
fn deserialize_bounded_json_vec<'de, D>(
deserializer: D,
) -> core::result::Result<Vec<JsonValue>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = Vec<JsonValue>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a bounded JSON array")
}
fn visit_none<E>(self) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Vec::new())
}
fn visit_unit<E>(self) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Vec::new())
}
fn visit_some<D>(self, deserializer: D) -> core::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_seq(self)
}
fn visit_seq<A>(self, mut seq: A) -> core::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut values = Vec::new();
while values.len() < IDENTITY_LIST_LIMIT {
let Some(value) = seq.next_element::<JsonValue>()? else {
return Ok(values);
};
values.push(value);
}
if seq.next_element::<serde::de::IgnoredAny>()?.is_some() {
return Err(serde::de::Error::custom(
"identity OIDC JWKS key list exceeds item limit",
));
}
Ok(values)
}
}
deserializer.deserialize_option(Visitor)
}
fn deserialize_bounded_json_map<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, JsonValue>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = BTreeMap<String, JsonValue>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a bounded JSON object")
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut values = BTreeMap::new();
while values.len() < IDENTITY_LIST_LIMIT {
let Some((key, value)) = map.next_entry::<String, JsonValue>()? else {
return Ok(values);
};
values.insert(key, value);
}
if map
.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
.is_some()
{
return Err(serde::de::Error::custom(
"identity OIDC JSON object exceeds item limit",
));
}
Ok(values)
}
}
deserializer.deserialize_map(Visitor)
}
fn deserialize_bounded_oidc_provider_info_map<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, IdentityOidcProviderInfo>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = BTreeMap<String, IdentityOidcProviderInfo>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a bounded Identity OIDC provider info map")
}
fn visit_none<E>(self) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(BTreeMap::new())
}
fn visit_unit<E>(self) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(BTreeMap::new())
}
fn visit_some<D>(self, deserializer: D) -> core::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(self)
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut values = BTreeMap::new();
while values.len() < IDENTITY_LIST_LIMIT {
let Some((key, value)) = map.next_entry::<String, IdentityOidcProviderInfo>()?
else {
return Ok(values);
};
values.insert(key, value);
}
if map
.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
.is_some()
{
return Err(serde::de::Error::custom(
"identity OIDC provider info map exceeds item limit",
));
}
Ok(values)
}
}
deserializer.deserialize_option(Visitor)
}
fn deserialize_bounded_oidc_client_info_map<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, IdentityOidcClientInfo>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = BTreeMap<String, IdentityOidcClientInfo>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a bounded Identity OIDC client info map")
}
fn visit_none<E>(self) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(BTreeMap::new())
}
fn visit_unit<E>(self) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(BTreeMap::new())
}
fn visit_some<D>(self, deserializer: D) -> core::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(self)
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut values = BTreeMap::new();
while values.len() < IDENTITY_LIST_LIMIT {
let Some((key, value)) = map.next_entry::<String, IdentityOidcClientInfo>()? else {
return Ok(values);
};
values.insert(key, value);
}
if map
.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
.is_some()
{
return Err(serde::de::Error::custom(
"identity OIDC client info map exceeds item limit",
));
}
Ok(values)
}
}
deserializer.deserialize_option(Visitor)
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
#![allow(deprecated)]
use secrecy::SecretString;
use crate::{Client, OpenBaoConfig};
use super::{
IdentityAliasList, IdentityEntityBatchDeleteRequest, IdentityEntityList,
IdentityEntityRequest, IdentityGroupList, IdentityGroupRequest, IdentityMfaDuoMethodInfo,
IdentityMfaDuoMethodRequest, IdentityMfaLoginEnforcementList,
IdentityMfaLoginEnforcementRequest, IdentityMfaMethodList, IdentityMfaOktaMethodInfo,
IdentityMfaOktaMethodRequest, IdentityMfaPingIdMethodRequest, IdentityMfaTotpSecret,
IdentityOidcAssignmentList, IdentityOidcClientInfo, IdentityOidcClientList,
IdentityOidcDiscovery, IdentityOidcIntrospectRequest, IdentityOidcIntrospection,
IdentityOidcJwks, IdentityOidcKeyList, IdentityOidcProviderList, IdentityOidcRoleList,
IdentityOidcScopeList, IdentityOidcToken,
};
#[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());
}
#[test]
fn identity_oidc_secret_debug_is_redacted() {
let token = IdentityOidcToken {
client_id: Some("client-id".to_owned()),
token: SecretString::from("signed-id-token"),
ttl: Some(3600),
};
let debug = format!("{token:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("signed-id-token"));
let request = IdentityOidcIntrospectRequest::new(SecretString::from("signed-id-token"))
.with_client_id("client-id");
let debug = format!("{request:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("signed-id-token"));
let client = IdentityOidcClientInfo {
client_secret: Some(SecretString::from("client-secret")),
..IdentityOidcClientInfo::default()
};
let debug = format!("{client:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("client-secret"));
}
#[test]
fn identity_mfa_secret_debug_is_redacted_and_validated() {
let duo = IdentityMfaDuoMethodRequest::new(
"duo-main",
SecretString::from("fixture-a"),
SecretString::from("fixture-b"),
"api.example.com",
);
let debug = format!("{duo:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("fixture-a"));
assert!(!debug.contains("fixture-b"));
let okta = IdentityMfaOktaMethodRequest::new(
"okta-main",
"dev-org",
SecretString::from("fixture-c"),
);
let debug = format!("{okta:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("fixture-c"));
let ping =
IdentityMfaPingIdMethodRequest::new("ping-main", SecretString::from("fixture-d"));
let debug = format!("{ping:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("fixture-d"));
let duo_info = serde_json::from_value::<IdentityMfaDuoMethodInfo>(serde_json::json!({
"secret_key": "fixture-a",
"integration_key": "fixture-b"
}))
.unwrap_or_else(|error| panic!("{error}"));
let debug = format!("{duo_info:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("fixture-a"));
assert!(!debug.contains("fixture-b"));
let okta_info = serde_json::from_value::<IdentityMfaOktaMethodInfo>(serde_json::json!({
"api_token": "fixture-c"
}))
.unwrap_or_else(|error| panic!("{error}"));
let debug = format!("{okta_info:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("fixture-c"));
let totp_secret = serde_json::from_value::<IdentityMfaTotpSecret>(serde_json::json!({
"barcode": "barcode-data",
"url": "otpauth://totp/example?secret=value"
}))
.unwrap_or_else(|error| panic!("{error}"));
let debug = format!("{totp_secret:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("barcode-data"));
assert!(!debug.contains("value"));
assert!(
IdentityMfaLoginEnforcementRequest::new()
.with_mfa_method_id("totp-id")
.validate()
.is_err()
);
assert!(
IdentityMfaLoginEnforcementRequest::new()
.with_mfa_method_id("totp-id")
.with_auth_method_accessor("auth-userpass")
.validate()
.is_ok()
);
}
#[test]
fn identity_oidc_lists_are_bounded() {
let mut keys = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.push(format!("identity-oidc-{index}"));
}
let value = serde_json::json!({ "keys": keys });
assert!(serde_json::from_value::<IdentityOidcKeyList>(value.clone()).is_err());
assert!(serde_json::from_value::<IdentityOidcRoleList>(value.clone()).is_err());
assert!(
serde_json::from_value::<IdentityOidcProviderList>(serde_json::json!({
"keys": value["keys"].clone(),
"key_info": {}
}))
.is_err()
);
assert!(serde_json::from_value::<IdentityOidcScopeList>(value.clone()).is_err());
assert!(serde_json::from_value::<IdentityOidcClientList>(value.clone()).is_err());
assert!(serde_json::from_value::<IdentityOidcAssignmentList>(value).is_err());
let mut jwks = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
jwks.push(serde_json::json!({ "kid": format!("key-{index}") }));
}
assert!(
serde_json::from_value::<IdentityOidcJwks>(serde_json::json!({
"keys": jwks
}))
.is_err()
);
let mut key_info = serde_json::Map::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
key_info.insert(format!("client-{index}"), serde_json::json!({}));
}
assert!(
serde_json::from_value::<IdentityOidcClientList>(serde_json::json!({
"keys": [],
"key_info": key_info
}))
.is_err()
);
}
#[test]
fn identity_oidc_extra_maps_are_bounded() {
let mut extra = serde_json::Map::new();
extra.insert("active".to_owned(), serde_json::json!(true));
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
extra.insert(format!("claim-{index}"), serde_json::json!("value"));
}
assert!(serde_json::from_value::<IdentityOidcIntrospection>(extra.clone().into()).is_err());
assert!(serde_json::from_value::<IdentityOidcDiscovery>(extra.into()).is_err());
}
#[test]
fn identity_oidc_discovery_string_lists_are_bounded() {
let values = (0..=crate::response::MAX_RESPONSE_STRINGS)
.map(|index| serde_json::json!(format!("claim-{index}")))
.collect::<Vec<_>>();
assert!(
serde_json::from_value::<IdentityOidcDiscovery>(serde_json::json!({
"claims_supported": values
}))
.is_err()
);
}
#[test]
fn identity_oidc_jwks_exact_limit_is_accepted() {
let mut keys = Vec::new();
for index in 0..crate::response::MAX_RESPONSE_STRINGS {
keys.push(serde_json::json!({ "kid": format!("key-{index}") }));
}
let jwks = serde_json::from_value::<IdentityOidcJwks>(serde_json::json!({
"keys": keys
}))
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(jwks.keys.len(), crate::response::MAX_RESPONSE_STRINGS);
}
#[test]
fn identity_mfa_lists_are_bounded() {
let mut keys = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.push(format!("identity-mfa-{index}"));
}
let value = serde_json::json!({ "keys": keys });
assert!(serde_json::from_value::<IdentityMfaMethodList>(value.clone()).is_err());
assert!(serde_json::from_value::<IdentityMfaLoginEnforcementList>(value).is_err());
}
}