use super::relay::{RelayError, RelayServiceConfig};
use super::rendezvous::{Quotas as RendezvousQuotas, ServiceConfig as RendezvousServiceConfig};
const MIB: u64 = 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AtpServeCommand {
RendezvousServe,
RelayServe,
}
impl AtpServeCommand {
#[must_use]
pub const fn words(self) -> &'static [&'static str] {
match self {
Self::RendezvousServe => &["atp", "rendezvous", "serve"],
Self::RelayServe => &["atp", "relay", "serve"],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AtpDeploymentMode {
Personal,
Team,
Ci,
PublicGateway,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtpServiceIdentity {
service_id: String,
operator_label: String,
}
impl AtpServiceIdentity {
pub fn new(
service_id: impl Into<String>,
operator_label: impl Into<String>,
) -> Result<Self, AtpOpsConfigError> {
let service_id = service_id.into();
let operator_label = operator_label.into();
if service_id.trim().is_empty() {
return Err(AtpOpsConfigError::EmptyServiceId);
}
if operator_label.trim().is_empty() {
return Err(AtpOpsConfigError::EmptyOperatorLabel);
}
Ok(Self {
service_id,
operator_label,
})
}
#[must_use]
pub fn service_id(&self) -> &str {
&self.service_id
}
#[must_use]
pub fn operator_label(&self) -> &str {
&self.operator_label
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AtpTlsPolicy {
DisabledLoopbackOnly,
Required {
client_auth_required: bool,
},
}
impl AtpTlsPolicy {
#[must_use]
pub const fn requires_tls(self) -> bool {
matches!(self, Self::Required { .. })
}
#[must_use]
pub const fn requires_client_auth(self) -> bool {
matches!(
self,
Self::Required {
client_auth_required: true,
}
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtpRateLimitPolicy {
pub max_reservations_per_minute: u32,
pub max_packets_per_second: u32,
pub max_mailbox_bytes_per_minute: u64,
}
impl AtpRateLimitPolicy {
pub const fn validate(self) -> Result<Self, AtpOpsConfigError> {
if self.max_reservations_per_minute == 0
|| self.max_packets_per_second == 0
|| self.max_mailbox_bytes_per_minute == 0
{
return Err(AtpOpsConfigError::InvalidQuota);
}
Ok(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtpExpiryPolicy {
pub candidate_ttl_micros: u64,
pub relay_reservation_ttl_micros: u64,
pub mailbox_ttl_secs: u64,
}
impl AtpExpiryPolicy {
pub const fn validate(self) -> Result<Self, AtpOpsConfigError> {
if self.candidate_ttl_micros == 0
|| self.relay_reservation_ttl_micros == 0
|| self.mailbox_ttl_secs == 0
{
return Err(AtpOpsConfigError::InvalidExpiry);
}
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtpAccessPolicy {
allowed_peers: Vec<String>,
allowed_groups: Vec<String>,
public_registration: bool,
}
impl AtpAccessPolicy {
#[must_use]
pub const fn private() -> Self {
Self {
allowed_peers: Vec::new(),
allowed_groups: Vec::new(),
public_registration: false,
}
}
#[must_use]
pub const fn public_registration() -> Self {
Self {
allowed_peers: Vec::new(),
allowed_groups: Vec::new(),
public_registration: true,
}
}
pub fn with_allowed_peer(
mut self,
peer_id: impl Into<String>,
) -> Result<Self, AtpOpsConfigError> {
let peer_id = peer_id.into();
if peer_id.trim().is_empty() {
return Err(AtpOpsConfigError::EmptyAccessEntry);
}
self.allowed_peers.push(peer_id);
Ok(self)
}
pub fn with_allowed_group(
mut self,
group_id: impl Into<String>,
) -> Result<Self, AtpOpsConfigError> {
let group_id = group_id.into();
if group_id.trim().is_empty() {
return Err(AtpOpsConfigError::EmptyAccessEntry);
}
self.allowed_groups.push(group_id);
Ok(self)
}
#[must_use]
pub fn allowed_peers(&self) -> &[String] {
&self.allowed_peers
}
#[must_use]
pub fn allowed_groups(&self) -> &[String] {
&self.allowed_groups
}
#[must_use]
pub const fn public_registration_enabled(&self) -> bool {
self.public_registration
}
}
impl Default for AtpAccessPolicy {
fn default() -> Self {
Self::private()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtpMailboxStoragePolicy {
storage_label: String,
max_bytes: u64,
max_records: u64,
encrypted_only: bool,
}
impl AtpMailboxStoragePolicy {
pub fn new(
storage_label: impl Into<String>,
max_bytes: u64,
max_records: u64,
encrypted_only: bool,
) -> Result<Self, AtpOpsConfigError> {
let storage_label = storage_label.into();
if storage_label.trim().is_empty() {
return Err(AtpOpsConfigError::EmptyMailboxStorageLabel);
}
if max_bytes == 0 || max_records == 0 {
return Err(AtpOpsConfigError::InvalidQuota);
}
Ok(Self {
storage_label,
max_bytes,
max_records,
encrypted_only,
})
}
#[must_use]
pub fn storage_label(&self) -> &str {
&self.storage_label
}
#[must_use]
pub const fn max_bytes(&self) -> u64 {
self.max_bytes
}
#[must_use]
pub const fn max_records(&self) -> u64 {
self.max_records
}
#[must_use]
pub const fn encrypted_only(&self) -> bool {
self.encrypted_only
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtpOperatorLogPolicy {
pub log_peer_ids: bool,
pub structured_events: bool,
pub retain_state_on_restart: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtpFederationPolicy {
trusted_services: Vec<String>,
trust_assumptions: Vec<String>,
}
impl AtpFederationPolicy {
#[must_use]
pub const fn disabled() -> Self {
Self {
trusted_services: Vec::new(),
trust_assumptions: Vec::new(),
}
}
pub fn opt_in(
trusted_services: Vec<String>,
trust_assumptions: Vec<String>,
) -> Result<Self, AtpOpsConfigError> {
if trusted_services.is_empty() {
return Err(AtpOpsConfigError::FederationRequiresTrustedService);
}
if trust_assumptions.is_empty() {
return Err(AtpOpsConfigError::FederationRequiresTrustAssumption);
}
if trusted_services
.iter()
.chain(trust_assumptions.iter())
.any(|entry| entry.trim().is_empty())
{
return Err(AtpOpsConfigError::EmptyFederationEntry);
}
Ok(Self {
trusted_services,
trust_assumptions,
})
}
#[must_use]
pub fn enabled(&self) -> bool {
!self.trusted_services.is_empty()
}
#[must_use]
pub fn trusted_services(&self) -> &[String] {
&self.trusted_services
}
#[must_use]
pub fn trust_assumptions(&self) -> &[String] {
&self.trust_assumptions
}
}
impl Default for AtpFederationPolicy {
fn default() -> Self {
Self::disabled()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelayContentVisibility {
OpaqueEncryptedOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtpRendezvousServeConfig {
command: AtpServeCommand,
mode: AtpDeploymentMode,
identity: AtpServiceIdentity,
tls: AtpTlsPolicy,
quotas: RendezvousQuotas,
expiry: AtpExpiryPolicy,
rate_limits: AtpRateLimitPolicy,
access: AtpAccessPolicy,
federation: AtpFederationPolicy,
logs: AtpOperatorLogPolicy,
}
impl AtpRendezvousServeConfig {
pub fn for_mode(
mode: AtpDeploymentMode,
service_id: impl Into<String>,
operator_label: impl Into<String>,
) -> Result<Self, AtpOpsConfigError> {
let config = Self {
command: AtpServeCommand::RendezvousServe,
mode,
identity: AtpServiceIdentity::new(service_id, operator_label)?,
tls: default_tls_policy(mode),
quotas: rendezvous_quotas(mode),
expiry: expiry_policy(mode).validate()?,
rate_limits: rate_limits(mode).validate()?,
access: default_access_policy(mode),
federation: AtpFederationPolicy::disabled(),
logs: log_policy(mode),
};
config.validate()?;
Ok(config)
}
#[must_use]
pub const fn command(&self) -> AtpServeCommand {
self.command
}
#[must_use]
pub const fn mode(&self) -> AtpDeploymentMode {
self.mode
}
#[must_use]
pub const fn identity(&self) -> &AtpServiceIdentity {
&self.identity
}
#[must_use]
pub const fn tls(&self) -> AtpTlsPolicy {
self.tls
}
#[must_use]
pub const fn quotas(&self) -> RendezvousQuotas {
self.quotas
}
#[must_use]
pub const fn expiry(&self) -> AtpExpiryPolicy {
self.expiry
}
#[must_use]
pub const fn rate_limits(&self) -> AtpRateLimitPolicy {
self.rate_limits
}
#[must_use]
pub const fn access(&self) -> &AtpAccessPolicy {
&self.access
}
#[must_use]
pub const fn federation(&self) -> &AtpFederationPolicy {
&self.federation
}
#[must_use]
pub const fn logs(&self) -> AtpOperatorLogPolicy {
self.logs
}
pub fn with_tls_policy(mut self, tls: AtpTlsPolicy) -> Result<Self, AtpOpsConfigError> {
self.tls = tls;
self.validate()?;
Ok(self)
}
#[must_use]
pub fn with_access_policy(mut self, access: AtpAccessPolicy) -> Self {
self.access = access;
self
}
pub fn with_federation_policy(
mut self,
federation: AtpFederationPolicy,
) -> Result<Self, AtpOpsConfigError> {
self.federation = federation;
self.validate()?;
Ok(self)
}
pub fn rendezvous_service_config(
&self,
) -> Result<RendezvousServiceConfig, super::rendezvous::Error> {
RendezvousServiceConfig::new(self.identity.service_id.clone(), self.quotas).map(|config| {
config
.with_log_peer_ids(self.logs.log_peer_ids)
.with_retain_state_on_restart(self.logs.retain_state_on_restart)
})
}
fn validate(&self) -> Result<(), AtpOpsConfigError> {
validate_public_tls(self.mode, self.tls)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtpRelayServeConfig {
command: AtpServeCommand,
mode: AtpDeploymentMode,
identity: AtpServiceIdentity,
tls: AtpTlsPolicy,
max_active_reservations: usize,
expiry: AtpExpiryPolicy,
rate_limits: AtpRateLimitPolicy,
access: AtpAccessPolicy,
mailbox_storage: AtpMailboxStoragePolicy,
federation: AtpFederationPolicy,
logs: AtpOperatorLogPolicy,
}
impl AtpRelayServeConfig {
pub fn for_mode(
mode: AtpDeploymentMode,
relay_id: impl Into<String>,
operator_label: impl Into<String>,
) -> Result<Self, AtpOpsConfigError> {
let config = Self {
command: AtpServeCommand::RelayServe,
mode,
identity: AtpServiceIdentity::new(relay_id, operator_label)?,
tls: default_tls_policy(mode),
max_active_reservations: relay_reservation_limit(mode),
expiry: expiry_policy(mode).validate()?,
rate_limits: rate_limits(mode).validate()?,
access: default_access_policy(mode),
mailbox_storage: mailbox_storage_policy(mode)?,
federation: AtpFederationPolicy::disabled(),
logs: log_policy(mode),
};
config.validate()?;
Ok(config)
}
#[must_use]
pub const fn command(&self) -> AtpServeCommand {
self.command
}
#[must_use]
pub const fn mode(&self) -> AtpDeploymentMode {
self.mode
}
#[must_use]
pub const fn identity(&self) -> &AtpServiceIdentity {
&self.identity
}
#[must_use]
pub const fn tls(&self) -> AtpTlsPolicy {
self.tls
}
#[must_use]
pub const fn max_active_reservations(&self) -> usize {
self.max_active_reservations
}
#[must_use]
pub const fn expiry(&self) -> AtpExpiryPolicy {
self.expiry
}
#[must_use]
pub const fn rate_limits(&self) -> AtpRateLimitPolicy {
self.rate_limits
}
#[must_use]
pub const fn access(&self) -> &AtpAccessPolicy {
&self.access
}
#[must_use]
pub const fn mailbox_storage(&self) -> &AtpMailboxStoragePolicy {
&self.mailbox_storage
}
#[must_use]
pub const fn federation(&self) -> &AtpFederationPolicy {
&self.federation
}
#[must_use]
pub const fn logs(&self) -> AtpOperatorLogPolicy {
self.logs
}
#[must_use]
pub const fn content_visibility(&self) -> RelayContentVisibility {
RelayContentVisibility::OpaqueEncryptedOnly
}
pub fn with_tls_policy(mut self, tls: AtpTlsPolicy) -> Result<Self, AtpOpsConfigError> {
self.tls = tls;
self.validate()?;
Ok(self)
}
#[must_use]
pub fn with_access_policy(mut self, access: AtpAccessPolicy) -> Self {
self.access = access;
self
}
pub fn with_federation_policy(
mut self,
federation: AtpFederationPolicy,
) -> Result<Self, AtpOpsConfigError> {
self.federation = federation;
self.validate()?;
Ok(self)
}
pub fn relay_service_config(&self) -> Result<RelayServiceConfig, RelayError> {
RelayServiceConfig::new(
self.identity.service_id.clone(),
self.max_active_reservations,
)
.map(|config| {
config
.with_log_peer_ids(self.logs.log_peer_ids)
.with_retain_state_on_restart(self.logs.retain_state_on_restart)
})
}
fn validate(&self) -> Result<(), AtpOpsConfigError> {
validate_public_tls(self.mode, self.tls)?;
if self.max_active_reservations == 0 {
return Err(AtpOpsConfigError::InvalidQuota);
}
if !self.mailbox_storage.encrypted_only {
return Err(AtpOpsConfigError::MailboxMustBeEncrypted);
}
Ok(())
}
}
fn validate_public_tls(
mode: AtpDeploymentMode,
tls: AtpTlsPolicy,
) -> Result<(), AtpOpsConfigError> {
if mode == AtpDeploymentMode::PublicGateway && !tls.requires_tls() {
return Err(AtpOpsConfigError::PublicGatewayRequiresTls);
}
Ok(())
}
const fn default_tls_policy(mode: AtpDeploymentMode) -> AtpTlsPolicy {
match mode {
AtpDeploymentMode::Personal | AtpDeploymentMode::Ci => AtpTlsPolicy::DisabledLoopbackOnly,
AtpDeploymentMode::Team => AtpTlsPolicy::Required {
client_auth_required: true,
},
AtpDeploymentMode::PublicGateway => AtpTlsPolicy::Required {
client_auth_required: false,
},
}
}
const fn rendezvous_quotas(mode: AtpDeploymentMode) -> RendezvousQuotas {
match mode {
AtpDeploymentMode::Personal => RendezvousQuotas {
max_candidates_per_peer: 8,
max_total_candidates: 32,
max_observations_per_peer: 4,
max_total_observations: 32,
max_attempts_per_peer: 8,
},
AtpDeploymentMode::Team => RendezvousQuotas {
max_candidates_per_peer: 16,
max_total_candidates: 512,
max_observations_per_peer: 8,
max_total_observations: 1024,
max_attempts_per_peer: 16,
},
AtpDeploymentMode::Ci => RendezvousQuotas {
max_candidates_per_peer: 4,
max_total_candidates: 16,
max_observations_per_peer: 2,
max_total_observations: 16,
max_attempts_per_peer: 4,
},
AtpDeploymentMode::PublicGateway => RendezvousQuotas {
max_candidates_per_peer: 8,
max_total_candidates: 4096,
max_observations_per_peer: 4,
max_total_observations: 8192,
max_attempts_per_peer: 8,
},
}
}
const fn relay_reservation_limit(mode: AtpDeploymentMode) -> usize {
match mode {
AtpDeploymentMode::Personal => 64,
AtpDeploymentMode::Team => 2048,
AtpDeploymentMode::Ci => 32,
AtpDeploymentMode::PublicGateway => 8192,
}
}
const fn expiry_policy(mode: AtpDeploymentMode) -> AtpExpiryPolicy {
match mode {
AtpDeploymentMode::Personal => AtpExpiryPolicy {
candidate_ttl_micros: 60_000_000,
relay_reservation_ttl_micros: 300_000_000,
mailbox_ttl_secs: 86_400,
},
AtpDeploymentMode::Team => AtpExpiryPolicy {
candidate_ttl_micros: 60_000_000,
relay_reservation_ttl_micros: 600_000_000,
mailbox_ttl_secs: 604_800,
},
AtpDeploymentMode::Ci => AtpExpiryPolicy {
candidate_ttl_micros: 10_000_000,
relay_reservation_ttl_micros: 60_000_000,
mailbox_ttl_secs: 3_600,
},
AtpDeploymentMode::PublicGateway => AtpExpiryPolicy {
candidate_ttl_micros: 30_000_000,
relay_reservation_ttl_micros: 120_000_000,
mailbox_ttl_secs: 86_400,
},
}
}
const fn rate_limits(mode: AtpDeploymentMode) -> AtpRateLimitPolicy {
match mode {
AtpDeploymentMode::Personal => AtpRateLimitPolicy {
max_reservations_per_minute: 60,
max_packets_per_second: 2_000,
max_mailbox_bytes_per_minute: 64 * MIB,
},
AtpDeploymentMode::Team => AtpRateLimitPolicy {
max_reservations_per_minute: 2_000,
max_packets_per_second: 200_000,
max_mailbox_bytes_per_minute: 4 * 1024 * MIB,
},
AtpDeploymentMode::Ci => AtpRateLimitPolicy {
max_reservations_per_minute: 30,
max_packets_per_second: 1_000,
max_mailbox_bytes_per_minute: 32 * MIB,
},
AtpDeploymentMode::PublicGateway => AtpRateLimitPolicy {
max_reservations_per_minute: 1_000,
max_packets_per_second: 100_000,
max_mailbox_bytes_per_minute: 1024 * MIB,
},
}
}
fn default_access_policy(mode: AtpDeploymentMode) -> AtpAccessPolicy {
if mode == AtpDeploymentMode::PublicGateway {
AtpAccessPolicy::public_registration()
} else {
AtpAccessPolicy::private()
}
}
fn mailbox_storage_policy(
mode: AtpDeploymentMode,
) -> Result<AtpMailboxStoragePolicy, AtpOpsConfigError> {
match mode {
AtpDeploymentMode::Personal => {
AtpMailboxStoragePolicy::new("personal-mailbox", 512 * MIB, 16_384, true)
}
AtpDeploymentMode::Team => {
AtpMailboxStoragePolicy::new("team-mailbox", 64 * 1024 * MIB, 1_000_000, true)
}
AtpDeploymentMode::Ci => AtpMailboxStoragePolicy::new("ci-mailbox", 128 * MIB, 4096, true),
AtpDeploymentMode::PublicGateway => {
AtpMailboxStoragePolicy::new("public-gateway-mailbox", 16 * 1024 * MIB, 250_000, true)
}
}
}
const fn log_policy(mode: AtpDeploymentMode) -> AtpOperatorLogPolicy {
match mode {
AtpDeploymentMode::Personal => AtpOperatorLogPolicy {
log_peer_ids: false,
structured_events: true,
retain_state_on_restart: true,
},
AtpDeploymentMode::Team => AtpOperatorLogPolicy {
log_peer_ids: false,
structured_events: true,
retain_state_on_restart: true,
},
AtpDeploymentMode::Ci => AtpOperatorLogPolicy {
log_peer_ids: true,
structured_events: true,
retain_state_on_restart: false,
},
AtpDeploymentMode::PublicGateway => AtpOperatorLogPolicy {
log_peer_ids: false,
structured_events: true,
retain_state_on_restart: true,
},
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AtpOpsConfigError {
#[error("ATP service id is empty")]
EmptyServiceId,
#[error("ATP operator label is empty")]
EmptyOperatorLabel,
#[error("ATP access entry is empty")]
EmptyAccessEntry,
#[error("ATP mailbox storage label is empty")]
EmptyMailboxStorageLabel,
#[error("ATP service quota is invalid")]
InvalidQuota,
#[error("ATP service expiry is invalid")]
InvalidExpiry,
#[error("public ATP gateways require TLS")]
PublicGatewayRequiresTls,
#[error("ATP federation requires at least one trusted service")]
FederationRequiresTrustedService,
#[error("ATP federation requires explicit trust assumptions")]
FederationRequiresTrustAssumption,
#[error("ATP federation entry is empty")]
EmptyFederationEntry,
#[error("ATP relay mailbox storage must be encrypted-only")]
MailboxMustBeEncrypted,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serve_modes_map_to_cli_commands_and_state_machine_configs() {
let rendezvous =
AtpRendezvousServeConfig::for_mode(AtpDeploymentMode::Team, "rv-team", "team-ops")
.expect("team rendezvous config");
assert_eq!(
rendezvous.command().words(),
&["atp", "rendezvous", "serve"]
);
assert!(rendezvous.tls().requires_client_auth());
assert_eq!(rendezvous.quotas().max_total_candidates, 512);
let state_config = rendezvous
.rendezvous_service_config()
.expect("rendezvous service config");
assert_eq!(state_config.service_id(), "rv-team");
assert_eq!(state_config.default_quotas().max_total_candidates, 512);
assert!(!state_config.log_peer_ids());
assert!(state_config.retain_state_on_restart());
let relay = AtpRelayServeConfig::for_mode(AtpDeploymentMode::Ci, "relay-ci", "ci-ops")
.expect("ci relay config");
assert_eq!(relay.command().words(), &["atp", "relay", "serve"]);
assert_eq!(relay.max_active_reservations(), 32);
assert!(!relay.logs().retain_state_on_restart);
let relay_state = relay.relay_service_config().expect("relay state config");
assert_eq!(relay_state.relay_id(), "relay-ci");
assert_eq!(relay_state.max_active_reservations(), 32);
assert!(relay_state.log_peer_ids());
assert!(!relay_state.retain_state_on_restart());
}
#[test]
fn config_covers_access_logs_mailbox_quotas_expiry_and_rate_limits() {
let access = AtpAccessPolicy::private()
.with_allowed_peer("peer:alice")
.expect("allowed peer")
.with_allowed_group("team:infra")
.expect("allowed group");
let relay =
AtpRelayServeConfig::for_mode(AtpDeploymentMode::Team, "relay-team", "team-ops")
.expect("relay config")
.with_access_policy(access);
assert_eq!(relay.access().allowed_peers(), &["peer:alice".to_owned()]);
assert_eq!(relay.access().allowed_groups(), &["team:infra".to_owned()]);
assert!(!relay.access().public_registration_enabled());
assert_eq!(relay.mailbox_storage().storage_label(), "team-mailbox");
assert!(relay.mailbox_storage().encrypted_only());
assert!(relay.mailbox_storage().max_bytes() >= 64 * 1024 * MIB);
assert_eq!(relay.expiry().mailbox_ttl_secs, 604_800);
assert!(relay.rate_limits().max_packets_per_second >= 200_000);
assert!(relay.logs().structured_events);
}
#[test]
fn federation_is_disabled_by_default_and_opt_in_requires_trust() {
let rendezvous =
AtpRendezvousServeConfig::for_mode(AtpDeploymentMode::Personal, "rv-personal", "me")
.expect("personal rendezvous");
assert!(!rendezvous.federation().enabled());
assert_eq!(
AtpFederationPolicy::opt_in(Vec::new(), vec!["team roots are pinned".to_owned()])
.expect_err("trusted service required"),
AtpOpsConfigError::FederationRequiresTrustedService
);
assert_eq!(
AtpFederationPolicy::opt_in(vec!["rv-team".to_owned()], Vec::new())
.expect_err("trust assumption required"),
AtpOpsConfigError::FederationRequiresTrustAssumption
);
let federated = rendezvous
.with_federation_policy(
AtpFederationPolicy::opt_in(
vec!["rv-team".to_owned()],
vec!["operators pin each service identity out of band".to_owned()],
)
.expect("federation policy"),
)
.expect("federated rendezvous config");
assert!(federated.federation().enabled());
assert_eq!(
federated.federation().trusted_services(),
&["rv-team".to_owned()]
);
}
#[test]
fn public_gateway_requires_tls_and_relay_visibility_stays_opaque() {
let relay = AtpRelayServeConfig::for_mode(
AtpDeploymentMode::PublicGateway,
"relay-public",
"public-ops",
)
.expect("public relay config");
assert!(relay.tls().requires_tls());
assert!(!relay.tls().requires_client_auth());
assert!(relay.access().public_registration_enabled());
assert_eq!(
relay.content_visibility(),
RelayContentVisibility::OpaqueEncryptedOnly
);
assert_eq!(
relay
.clone()
.with_tls_policy(AtpTlsPolicy::DisabledLoopbackOnly)
.expect_err("public gateway must require TLS"),
AtpOpsConfigError::PublicGatewayRequiresTls
);
}
}