Skip to content

Commit fe29318

Browse files
authored
Per route authorization (#8901)
This change updates the policy controller to admit `AuthorizationPolicy` resources that reference `HTTPRoute` parents. These policies configure proxies to augment server-level authorizations with per-route authorizations. Fixes #8890 Signed-off-by: Alex Leong <alex@buoyant.io>
1 parent 82d8592 commit fe29318

9 files changed

Lines changed: 229 additions & 39 deletions

File tree

policy-controller/core/src/http_route.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::{AuthorizationRef, ClientAuthorization};
2+
use ahash::AHashMap as HashMap;
13
use anyhow::Result;
24
pub use http::{
35
header::{HeaderName, HeaderValue},
@@ -11,6 +13,7 @@ use std::num::NonZeroU16;
1113
pub struct InboundHttpRoute {
1214
pub hostnames: Vec<HostMatch>,
1315
pub rules: Vec<InboundHttpRouteRule>,
16+
pub authorizations: HashMap<AuthorizationRef, ClientAuthorization>,
1417
}
1518

1619
#[derive(Clone, Debug, PartialEq, Eq)]

policy-controller/grpc/src/lib.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server {
177177
http_routes: srv
178178
.http_routes
179179
.iter()
180-
.map(|(name, route)| to_http_route(name, route.clone()))
180+
.map(|(name, route)| to_http_route(name, route.clone(), cluster_networks))
181181
.collect(),
182182
},
183183
)),
@@ -186,7 +186,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server {
186186
routes: srv
187187
.http_routes
188188
.iter()
189-
.map(|(name, route)| to_http_route(name, route.clone()))
189+
.map(|(name, route)| to_http_route(name, route.clone(), cluster_networks))
190190
.collect(),
191191
},
192192
)),
@@ -195,7 +195,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server {
195195
routes: srv
196196
.http_routes
197197
.iter()
198-
.map(|(name, route)| to_http_route(name, route.clone()))
198+
.map(|(name, route)| to_http_route(name, route.clone(), cluster_networks))
199199
.collect(),
200200
},
201201
)),
@@ -366,7 +366,12 @@ fn to_authz(
366366

367367
fn to_http_route(
368368
name: impl ToString,
369-
InboundHttpRoute { hostnames, rules }: InboundHttpRoute,
369+
InboundHttpRoute {
370+
hostnames,
371+
rules,
372+
authorizations,
373+
}: InboundHttpRoute,
374+
cluster_networks: &[IpNet],
370375
) -> proto::HttpRoute {
371376
let metadata = Metadata {
372377
kind: Some(metadata::Kind::Resource(api::meta::Resource {
@@ -391,11 +396,16 @@ fn to_http_route(
391396
)
392397
.collect();
393398

399+
let authorizations = authorizations
400+
.iter()
401+
.map(|(n, c)| to_authz(n, c, cluster_networks))
402+
.collect();
403+
394404
proto::HttpRoute {
395405
metadata: Some(metadata),
396406
hosts,
397407
rules,
398-
authorizations: Vec::default(), // TODO populate per-route authorizations
408+
authorizations,
399409
}
400410
}
401411

policy-controller/k8s/index/src/authorization_policy.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub(crate) struct Spec {
1313

1414
#[derive(Debug, PartialEq)]
1515
pub(crate) enum Target {
16+
HttpRoute(String),
1617
Server(String),
1718
Namespace,
1819
}
@@ -60,16 +61,17 @@ impl TryFrom<k8s::policy::AuthorizationPolicySpec> for Spec {
6061

6162
fn target(t: LocalTargetRef) -> Result<Target> {
6263
if t.targets_kind::<k8s::policy::Server>() {
63-
return Ok(Target::Server(t.name));
64-
}
65-
if t.targets_kind::<k8s::Namespace>() {
66-
return Ok(Target::Namespace);
64+
Ok(Target::Server(t.name))
65+
} else if t.targets_kind::<k8s::Namespace>() {
66+
Ok(Target::Namespace)
67+
} else if t.targets_kind::<k8s_gateway_api::HttpRoute>() {
68+
Ok(Target::HttpRoute(t.name))
69+
} else {
70+
anyhow::bail!(
71+
"unsupported authorization target type: {}",
72+
t.canonical_kind()
73+
)
6774
}
68-
69-
anyhow::bail!(
70-
"unsupported authorization target type: {}",
71-
t.canonical_kind()
72-
);
7375
}
7476

7577
fn authentication_ref(t: NamespacedTargetRef) -> Result<AuthenticationTarget> {

policy-controller/k8s/index/src/http_route.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use ahash::AHashMap as HashMap;
12
use anyhow::{bail, Error, Result};
23
use k8s_gateway_api as api;
34
use linkerd_policy_controller_core::http_route;
@@ -109,7 +110,11 @@ impl TryFrom<api::HttpRoute> for InboundRouteBinding {
109110

110111
Ok(InboundRouteBinding {
111112
parents,
112-
route: http_route::InboundHttpRoute { hostnames, rules },
113+
route: http_route::InboundHttpRoute {
114+
hostnames,
115+
rules,
116+
authorizations: HashMap::default(),
117+
},
113118
})
114119
}
115120
}

policy-controller/k8s/index/src/index.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,7 +1104,7 @@ impl PolicyIndex {
11041104
) -> InboundServer {
11051105
tracing::trace!(%name, ?server, "Creating inbound server");
11061106
let authorizations = self.client_authzs(&name, server, authentications);
1107-
let routes = self.http_routes(&name);
1107+
let routes = self.http_routes(&name, authentications);
11081108

11091109
InboundServer {
11101110
reference: ServerRef::Server(name),
@@ -1146,6 +1146,12 @@ impl PolicyIndex {
11461146
}
11471147
}
11481148
authorization_policy::Target::Namespace => {}
1149+
authorization_policy::Target::HttpRoute(_) => {
1150+
// Policies which target HttpRoutes will be attached to
1151+
// the route authorizations and should not be included in
1152+
// the server authorizations.
1153+
continue;
1154+
}
11491155
}
11501156

11511157
tracing::trace!(
@@ -1176,11 +1182,70 @@ impl PolicyIndex {
11761182
authzs
11771183
}
11781184

1179-
fn http_routes(&self, server_name: &str) -> HashMap<String, InboundHttpRoute> {
1185+
fn route_client_authzs(
1186+
&self,
1187+
route_name: &str,
1188+
authentications: &AuthenticationNsIndex,
1189+
) -> HashMap<AuthorizationRef, ClientAuthorization> {
1190+
let mut authzs = HashMap::default();
1191+
1192+
for (name, spec) in &self.authorization_policies {
1193+
// Skip the policy if it doesn't apply to the route.
1194+
match &spec.target {
1195+
authorization_policy::Target::HttpRoute(n) if n == route_name => {}
1196+
_ => {
1197+
tracing::trace!(
1198+
ns = %self.namespace,
1199+
authorizationpolicy = %name,
1200+
route = %route_name,
1201+
target = ?spec.target,
1202+
"AuthorizationPolicy does not target HttpRoute",
1203+
);
1204+
continue;
1205+
}
1206+
}
1207+
1208+
tracing::trace!(
1209+
ns = %self.namespace,
1210+
authorizationpolicy = %name,
1211+
route = %route_name,
1212+
"AuthorizationPolicy targets HttpRoute",
1213+
);
1214+
tracing::trace!(authns = ?spec.authentications);
1215+
1216+
let authz = match self.policy_client_authz(spec, authentications) {
1217+
Ok(authz) => authz,
1218+
Err(error) => {
1219+
tracing::info!(
1220+
route = %route_name,
1221+
authorizationpolicy = %name,
1222+
%error,
1223+
"Illegal AuthorizationPolicy; ignoring",
1224+
);
1225+
continue;
1226+
}
1227+
};
1228+
1229+
let reference = AuthorizationRef::AuthorizationPolicy(name.to_string());
1230+
authzs.insert(reference, authz);
1231+
}
1232+
1233+
authzs
1234+
}
1235+
1236+
fn http_routes(
1237+
&self,
1238+
server_name: &str,
1239+
authentications: &AuthenticationNsIndex,
1240+
) -> HashMap<String, InboundHttpRoute> {
11801241
self.http_routes
11811242
.iter()
11821243
.filter(|(_, route)| route.selects_server(server_name))
1183-
.map(|(name, route)| (name.clone(), route.route.clone()))
1244+
.map(|(name, route)| {
1245+
let mut route = route.route.clone();
1246+
route.authorizations = self.route_client_authzs(name, authentications);
1247+
(name.clone(), route)
1248+
})
11841249
.collect()
11851250
}
11861251

policy-controller/k8s/index/src/tests/http_routes.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::*;
44
fn route_attaches_to_server() {
55
let test = TestConfig::default();
66

7+
// Create pod.
78
let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None)));
89
pod.labels_mut()
910
.insert("app".to_string(), "app-0".to_string());
@@ -16,6 +17,7 @@ fn route_attaches_to_server() {
1617
.expect("pod-0.ns-0 should exist");
1718
assert_eq!(*rx.borrow_and_update(), test.default_server());
1819

20+
// Create server.
1921
test.index.write().apply(mk_server(
2022
"ns-0",
2123
"srv-8080",
@@ -34,6 +36,8 @@ fn route_attaches_to_server() {
3436
http_routes: HashMap::default(),
3537
},
3638
);
39+
40+
// Create route.
3741
test.index
3842
.write()
3943
.apply(mk_http_route("ns-0", "route-foo", "srv-8080"));
@@ -42,7 +46,27 @@ fn route_attaches_to_server() {
4246
rx.borrow().reference,
4347
ServerRef::Server("srv-8080".to_string())
4448
);
45-
assert!(rx.borrow().http_routes.contains_key("route-foo"));
49+
assert!(rx.borrow_and_update().http_routes.contains_key("route-foo"));
50+
51+
// Create authz policy.
52+
test.index.write().apply(mk_authorization_policy(
53+
"ns-0",
54+
"authz-foo",
55+
"route-foo",
56+
vec![NamespacedTargetRef {
57+
group: None,
58+
kind: "ServiceAccount".to_string(),
59+
namespace: Some("ns-0".to_string()),
60+
name: "foo".to_string(),
61+
}],
62+
));
63+
64+
assert!(rx.has_changed().unwrap());
65+
assert!(rx.borrow().http_routes["route-foo"]
66+
.authorizations
67+
.contains_key(&AuthorizationRef::AuthorizationPolicy(
68+
"authz-foo".to_string()
69+
)));
4670
}
4771

4872
fn mk_http_route(
@@ -84,3 +108,26 @@ fn mk_http_route(
84108
status: None,
85109
}
86110
}
111+
112+
fn mk_authorization_policy(
113+
ns: impl ToString,
114+
name: impl ToString,
115+
route: impl ToString,
116+
authns: impl IntoIterator<Item = NamespacedTargetRef>,
117+
) -> k8s::policy::AuthorizationPolicy {
118+
k8s::policy::AuthorizationPolicy {
119+
metadata: k8s::ObjectMeta {
120+
namespace: Some(ns.to_string()),
121+
name: Some(name.to_string()),
122+
..Default::default()
123+
},
124+
spec: k8s::policy::AuthorizationPolicySpec {
125+
target_ref: LocalTargetRef {
126+
group: Some("gateway.networking.k8s.io".to_string()),
127+
kind: "HttpRoute".to_string(),
128+
name: route.to_string(),
129+
},
130+
required_authentication_refs: authns.into_iter().collect(),
131+
},
132+
}
133+
}

policy-controller/src/admission.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ fn validate_policy_target(ns: &str, tgt: &LocalTargetRef) -> Result<()> {
201201
return Ok(());
202202
}
203203

204+
if tgt.targets_kind::<HttpRoute>() {
205+
return Ok(());
206+
}
207+
204208
if tgt.targets_kind::<Namespace>() {
205209
if tgt.name != ns {
206210
bail!("cannot target another namespace: {}", tgt.name);

policy-test/tests/admit_authorization_policy.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,39 @@ async fn rejects_targets_other_namespace() {
103103
.await;
104104
}
105105

106+
#[tokio::test(flavor = "current_thread")]
107+
async fn accepts_targets_route() {
108+
admission::accepts(|ns| AuthorizationPolicy {
109+
metadata: api::ObjectMeta {
110+
namespace: Some(ns),
111+
name: Some("test".to_string()),
112+
..Default::default()
113+
},
114+
spec: AuthorizationPolicySpec {
115+
target_ref: LocalTargetRef {
116+
group: Some("gateway.networking.k8s.io".to_string()),
117+
kind: "HttpRoute".to_string(),
118+
name: "route-foo".to_string(),
119+
},
120+
required_authentication_refs: vec![
121+
NamespacedTargetRef {
122+
group: Some("policy.linkerd.io".to_string()),
123+
kind: "MeshTLSAuthentication".to_string(),
124+
name: "mtls-clients".to_string(),
125+
namespace: None,
126+
},
127+
NamespacedTargetRef {
128+
group: Some("policy.linkerd.io".to_string()),
129+
kind: "NetworkAuthentication".to_string(),
130+
name: "cluster-nets".to_string(),
131+
namespace: Some("linkerd".to_string()),
132+
},
133+
],
134+
},
135+
})
136+
.await;
137+
}
138+
106139
#[tokio::test(flavor = "current_thread")]
107140
async fn accepts_valid_with_only_meshtls() {
108141
admission::accepts(|ns| AuthorizationPolicy {

0 commit comments

Comments
 (0)