policy: Test identity-parsing logic (#8103)

The identity-string parsing logic is currently implemented directly in
the ServerAuthorization indexer. In preparation of this being needed in
additional modules, this change extracts identity parsing and adds some
basic tests.

Signed-off-by: Oliver Gould <ver@buoyant.io>
This commit is contained in:
Oliver Gould 2022-03-21 12:08:32 -07:00 committed by GitHub
parent 0860e1c685
commit e962bf857c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 35 deletions

View File

@ -1,10 +1,10 @@
use std::fmt; use std::{convert::Infallible, fmt, str::FromStr};
/// Matches a client's mesh identity. /// Matches a client's mesh identity.
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum IdentityMatch { pub enum IdentityMatch {
/// An exact match. /// An exact match.
Name(String), Exact(String),
/// A suffix match. /// A suffix match.
Suffix(Vec<String>), Suffix(Vec<String>),
@ -12,11 +12,29 @@ pub enum IdentityMatch {
// === impl IdentityMatch === // === impl IdentityMatch ===
impl FromStr for IdentityMatch {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Infallible> {
if s == "*" {
return Ok(IdentityMatch::Suffix(vec![]));
}
if s.starts_with("*.") {
return Ok(IdentityMatch::Suffix(
s.split('.').skip(1).map(|s| s.to_string()).collect(),
));
}
Ok(IdentityMatch::Exact(s.to_string()))
}
}
impl fmt::Display for IdentityMatch { impl fmt::Display for IdentityMatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use std::fmt::Write; use std::fmt::Write;
match self { match self {
Self::Name(name) => name.fmt(f), Self::Exact(name) => name.fmt(f),
Self::Suffix(suffix) => { Self::Suffix(suffix) => {
f.write_char('*')?; f.write_char('*')?;
for part in suffix { for part in suffix {
@ -27,3 +45,43 @@ impl fmt::Display for IdentityMatch {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_star() {
assert_eq!("*".parse(), Ok(IdentityMatch::Suffix(vec![])));
assert_eq!(
"*.example.com".parse(),
Ok(IdentityMatch::Suffix(vec![
"example".to_string(),
"com".to_string()
]))
);
assert_eq!(
"*.*.example.com".parse(),
Ok(IdentityMatch::Suffix(vec![
"*".to_string(),
"example".to_string(),
"com".to_string()
]))
);
assert_eq!(
"x.example.com".parse(),
Ok(IdentityMatch::Exact("x.example.com".to_string()))
);
assert_eq!(
"**.example.com".parse(),
Ok(IdentityMatch::Exact("**.example.com".to_string()))
);
assert_eq!(
"foo.*.example.com".parse(),
Ok(IdentityMatch::Exact("foo.*.example.com".to_string()))
);
}
}

View File

@ -266,7 +266,7 @@ fn to_authz(
let identities = identities let identities = identities
.iter() .iter()
.filter_map(|i| match i { .filter_map(|i| match i {
IdentityMatch::Name(n) => Some(proto::Identity { IdentityMatch::Exact(n) => Some(proto::Identity {
name: n.to_string(), name: n.to_string(),
}), }),
_ => None, _ => None,

View File

@ -124,3 +124,12 @@ impl Index {
self.namespaces.index.get(ns) self.namespaces.index.get(ns)
} }
} }
impl ClusterInfo {
fn service_account_identity(&self, ns: &str, sa: &str) -> String {
format!(
"{}.{}.serviceaccount.identity.{}.{}",
sa, ns, self.control_plane_ns, self.identity_domain
)
}
}

View File

@ -235,39 +235,27 @@ fn mk_mtls_authn(
return Ok(ClientAuthentication::TlsUnauthenticated); return Ok(ClientAuthentication::TlsUnauthenticated);
} }
let mut identities = Vec::new(); let ids = mtls
.identities
.into_iter()
.flatten()
.map(|id| match id.parse() {
Ok(id) => id,
Err(e) => match e {},
});
for id in mtls.identities.into_iter().flatten() { let sas = mtls
if id == "*" { .service_accounts
debug!(suffix = %id, "Authenticated"); .into_iter()
identities.push(IdentityMatch::Suffix(vec![])); .flatten()
} else if id.starts_with("*.") { .filter_map(|sa| {
debug!(suffix = %id, "Authenticated"); let ns = sa.namespace.as_deref().or(metadata.namespace.as_deref())?;
let mut parts = id.split('.'); Some(IdentityMatch::Exact(
let star = parts.next(); cluster.service_account_identity(ns, &sa.name),
debug_assert_eq!(star, Some("*")); ))
identities.push(IdentityMatch::Suffix( });
parts.map(|p| p.to_string()).collect::<Vec<_>>(),
));
} else {
debug!(%id, "Authenticated");
identities.push(IdentityMatch::Name(id));
}
}
for sa in mtls.service_accounts.into_iter().flatten() {
let name = sa.name;
let ns = sa
.namespace
.unwrap_or_else(|| metadata.namespace.clone().unwrap());
debug!(ns = %ns, serviceaccount = %name, "Authenticated");
let n = format!(
"{}.{}.serviceaccount.identity.{}.{}",
name, ns, cluster.control_plane_ns, cluster.identity_domain
);
identities.push(IdentityMatch::Name(n));
}
let identities = ids.chain(sas).collect::<Vec<_>>();
if identities.is_empty() { if identities.is_empty() {
bail!("authorization authorizes no clients"); bail!("authorization authorizes no clients");
} }