summaryrefslogtreecommitdiff
path: root/src/authorization
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-07-05 13:36:41 -0600
committermo khan <mo@mokhan.ca>2025-07-05 13:36:41 -0600
commit63c5263087c9e282ced0e549b78c7ebd4353b273 (patch)
treee408c3aed5e4edf723c72897094bdd2f49077a15 /src/authorization
parent33083559c6d43d266ca77fcd1beb94c6feb4b547 (diff)
parentc6ec4e63d797c5e6cc01a4f09e723ad781b1034e (diff)
Merge branch 'sparkle-policies' into 'main'
Add Cedar policy validation and improve authorization architecture See merge request gitlab-org/software-supply-chain-security/authorization/authzd!4
Diffstat (limited to 'src/authorization')
-rw-r--r--src/authorization/authorizer.rs2
-rw-r--r--src/authorization/cedar_authorizer.rs207
-rw-r--r--src/authorization/check_service.rs2
-rw-r--r--src/authorization/server.rs12
4 files changed, 125 insertions, 98 deletions
diff --git a/src/authorization/authorizer.rs b/src/authorization/authorizer.rs
index 14a7df27..62733585 100644
--- a/src/authorization/authorizer.rs
+++ b/src/authorization/authorizer.rs
@@ -1,5 +1,5 @@
use envoy_types::ext_authz::v3::pb::CheckRequest;
-pub trait Authorizer: std::fmt::Debug {
+pub trait Authorizer: std::fmt::Debug + std::marker::Sync + std::marker::Send + 'static {
fn authorize(&self, request: CheckRequest) -> bool;
}
diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs
index 432102ef..64287414 100644
--- a/src/authorization/cedar_authorizer.rs
+++ b/src/authorization/cedar_authorizer.rs
@@ -1,45 +1,48 @@
use super::authorizer::Authorizer;
-use cedar_policy::{
- Authorizer as CedarAuth, Context, Entities, EntityId, EntityTypeName, EntityUid, PolicySet,
- Request as CedarRequest, RestrictedExpression,
-};
-use envoy_types::ext_authz::v3::pb::CheckRequest;
-use std::collections::HashMap;
use std::fs;
use std::str::FromStr;
#[derive(Debug)]
pub struct CedarAuthorizer {
- policies: PolicySet,
- authorizer: CedarAuth,
+ authorizer: cedar_policy::Authorizer,
+ entities: cedar_policy::Entities,
+ policies: cedar_policy::PolicySet,
}
impl CedarAuthorizer {
- pub fn new(policies: cedar_policy::PolicySet) -> CedarAuthorizer {
+ pub fn new(
+ policies: cedar_policy::PolicySet,
+ entities: cedar_policy::Entities,
+ ) -> CedarAuthorizer {
CedarAuthorizer {
policies,
- authorizer: CedarAuth::new(),
+ entities,
+ authorizer: cedar_policy::Authorizer::new(),
}
}
- pub fn new_from(path: &std::path::Path) -> CedarAuthorizer {
- Self::new(Self::load_from(path).unwrap_or_else(|_| PolicySet::default()))
+ pub fn new_from(path: &std::path::Path, entities: cedar_policy::Entities) -> CedarAuthorizer {
+ Self::new(
+ Self::load_from(path).unwrap_or_else(|_| cedar_policy::PolicySet::default()),
+ entities,
+ )
}
- fn load_from(path: &std::path::Path) -> Result<PolicySet, Box<dyn std::error::Error>> {
+ fn load_from(
+ path: &std::path::Path,
+ ) -> Result<cedar_policy::PolicySet, Box<dyn std::error::Error>> {
if !path.exists() || !path.is_dir() {
- return Ok(PolicySet::default());
+ return Ok(cedar_policy::PolicySet::default());
}
- let mut policies = PolicySet::new();
-
+ let mut policies = cedar_policy::PolicySet::new();
for entry in fs::read_dir(path)? {
let file_path = entry?.path();
if let Some(extension) = file_path.extension() {
if extension == "cedar" {
let content = fs::read_to_string(&file_path)?;
- let file_policies = PolicySet::from_str(&content)?;
+ let file_policies = cedar_policy::PolicySet::from_str(&content)?;
for policy in file_policies.policies() {
policies.add(policy.clone())?;
@@ -50,16 +53,96 @@ impl CedarAuthorizer {
Ok(policies)
}
+
+ fn map_from(
+ &self,
+ http_request: envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest,
+ ) -> Result<cedar_policy::Request, Box<dyn std::error::Error>> {
+ let principal = self.principal_from(&http_request)?;
+ let permission = self.permission_from(&http_request)?;
+ let resource = self.resource_from(&http_request)?;
+ let context = self.context_from(http_request)?;
+
+ Ok(cedar_policy::Request::new(
+ principal, permission, resource, context, None,
+ )?)
+ }
+
+ fn principal_from(
+ &self,
+ _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest,
+ ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> {
+ Ok(cedar_policy::EntityUid::from_type_name_and_id(
+ cedar_policy::EntityTypeName::from_str("User")?,
+ cedar_policy::EntityId::from_str("client")?,
+ ))
+ }
+
+ fn permission_from(
+ &self,
+ _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest,
+ ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> {
+ Ok(cedar_policy::EntityUid::from_type_name_and_id(
+ cedar_policy::EntityTypeName::from_str("Action")?,
+ cedar_policy::EntityId::from_str("check")?,
+ ))
+ }
+
+ fn resource_from(
+ &self,
+ _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest,
+ ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> {
+ Ok(cedar_policy::EntityUid::from_type_name_and_id(
+ cedar_policy::EntityTypeName::from_str("Resource")?,
+ cedar_policy::EntityId::from_str("resource")?,
+ ))
+ }
+
+ fn context_from(
+ &self,
+ http_request: envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest,
+ ) -> Result<cedar_policy::Context, Box<dyn std::error::Error>> {
+ let mut items = std::collections::HashMap::new();
+
+ items.insert("bearer_token".to_string(), self.token_from(&http_request));
+ items.insert("host".to_string(), self.safe_string(&http_request.host));
+ items.insert("method".to_string(), self.safe_string(&http_request.method));
+ items.insert("path".to_string(), self.safe_string(&http_request.path));
+
+ Ok(cedar_policy::Context::from_pairs(
+ items.into_iter().collect::<Vec<_>>(),
+ )?)
+ }
+
+ fn token_from(
+ &self,
+ http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest,
+ ) -> cedar_policy::RestrictedExpression {
+ let bearer_token = &http_request
+ .headers
+ .get("authorization")
+ .and_then(|auth| auth.strip_prefix("Bearer "))
+ .unwrap_or("");
+
+ self.safe_string(bearer_token)
+ }
+
+ fn safe_string(&self, item: &str) -> cedar_policy::RestrictedExpression {
+ cedar_policy::RestrictedExpression::new_string(item.to_string())
+ }
}
impl Default for CedarAuthorizer {
fn default() -> Self {
- Self::new_from(std::path::Path::new("/etc/authzd"))
+ Self::new_from(
+ std::path::Path::new("/etc/authzd"),
+ cedar_policy::Entities::empty(),
+ )
}
}
impl Authorizer for CedarAuthorizer {
- fn authorize(&self, request: CheckRequest) -> bool {
+ fn authorize(&self, request: envoy_types::ext_authz::v3::pb::CheckRequest) -> bool {
let http_request = match request
.attributes
.as_ref()
@@ -70,37 +153,26 @@ impl Authorizer for CedarAuthorizer {
None => return false,
};
- tracing::info!(
- method = %http_request.method,
- host = %http_request.host,
- path = %http_request.path,
- scheme = %http_request.scheme,
- protocol = %http_request.protocol,
- "Processing HTTP request"
- );
-
- if http_request.host == "sparkle.staging.runway.gitlab.net"
- && http_request.method == "GET"
- && http_request.path == "/"
- {
- return true;
- }
-
- let headers = &http_request.headers;
-
- let bearer_token = headers
- .get("authorization")
- .and_then(|auth| auth.strip_prefix("Bearer "))
- .unwrap_or("");
-
- match self.create_cedar_request(bearer_token, &http_request.path.to_string()) {
+ match self.map_from(http_request.clone()) {
Ok(cedar_request) => {
- let entities = Entities::empty();
let response =
self.authorizer
- .is_authorized(&cedar_request, &self.policies, &entities);
+ .is_authorized(&cedar_request, &self.policies, &self.entities);
+
+ let decision = response.decision();
- matches!(response.decision(), cedar_policy::Decision::Allow)
+ tracing::info!(
+ method = %http_request.method,
+ host = %http_request.host,
+ path = %http_request.path,
+ scheme = %http_request.scheme,
+ protocol = %http_request.protocol,
+ decision = ?decision,
+ diagnostics = ?response.diagnostics(),
+ "Processing HTTP request"
+ );
+
+ matches!(decision, cedar_policy::Decision::Allow)
}
Err(e) => {
tracing::error!(
@@ -113,46 +185,3 @@ impl Authorizer for CedarAuthorizer {
}
}
}
-
-impl CedarAuthorizer {
- fn create_cedar_request(
- &self,
- bearer_token: &str,
- path: &str,
- ) -> Result<CedarRequest, Box<dyn std::error::Error>> {
- // Create principal entity
- let principal_id = EntityId::from_str("client")?;
- let principal_type = EntityTypeName::from_str("User")?;
- let principal = EntityUid::from_type_name_and_id(principal_type, principal_id);
-
- // Create action entity
- let action_id = EntityId::from_str("check")?;
- let action_type = EntityTypeName::from_str("Action")?;
- let action = EntityUid::from_type_name_and_id(action_type, action_id);
-
- // Create resource entity
- let resource_id = EntityId::from_str("resource")?;
- let resource_type = EntityTypeName::from_str("Resource")?;
- let resource = EntityUid::from_type_name_and_id(resource_type, resource_id);
-
- // Create context with bearer token and path
- let mut context_map = HashMap::new();
- if !bearer_token.is_empty() {
- context_map.insert(
- "bearer_token".to_string(),
- RestrictedExpression::from_str(&format!("\"{bearer_token}\""))?,
- );
- }
- if !path.is_empty() {
- context_map.insert(
- "path".to_string(),
- RestrictedExpression::from_str(&format!("\"{path}\""))?,
- );
- }
-
- let context = Context::from_pairs(context_map.into_iter().collect::<Vec<_>>())?;
-
- CedarRequest::new(principal, action, resource, context, None)
- .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
- }
-}
diff --git a/src/authorization/check_service.rs b/src/authorization/check_service.rs
index 57f7b5d5..f8c7577f 100644
--- a/src/authorization/check_service.rs
+++ b/src/authorization/check_service.rs
@@ -23,10 +23,8 @@ impl envoy_types::ext_authz::v3::pb::Authorization for CheckService {
request: Request<CheckRequest>,
) -> Result<Response<CheckResponse>, Status> {
if self.authorizer.authorize(request.into_inner()) {
- log::info!("OK");
Ok(Response::new(CheckResponse::with_status(Status::ok("OK"))))
} else {
- log::info!("Unauthorized");
Ok(Response::new(CheckResponse::with_status(
Status::unauthenticated("Unauthorized"),
)))
diff --git a/src/authorization/server.rs b/src/authorization/server.rs
index 23b7720e..759a550d 100644
--- a/src/authorization/server.rs
+++ b/src/authorization/server.rs
@@ -8,13 +8,13 @@ pub struct Server {
}
impl Server {
- pub fn new() -> Result<Server, Box<dyn std::error::Error>> {
+ pub fn new<T: super::Authorizer>(authorizer: T) -> Result<Server, Box<dyn std::error::Error>> {
let (health_reporter, health_service) = tonic_health::server::health_reporter();
std::mem::drop(
health_reporter.set_service_status("", tonic_health::ServingStatus::Serving),
);
let authorization_service =
- AuthorizationServer::new(CheckService::new(Arc::new(CedarAuthorizer::default())));
+ AuthorizationServer::new(CheckService::new(Arc::new(authorizer)));
let reflection_service = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
@@ -40,9 +40,9 @@ impl Server {
tracing::info!(
method = %req.method(),
path = %req.uri().path(),
- content_type = ?req.headers().get("content-type").and_then(|v| v.to_str().ok()),
- user_agent = ?req.headers().get("user-agent").and_then(|v| v.to_str().ok()),
- x_request_id = ?req.headers().get("x-request-id").and_then(|v| v.to_str().ok()),
+ content_type = req.headers().get("content-type").map_or("unknown", |v| v.to_str().unwrap_or("unknown")),
+ user_agent = req.headers().get("user-agent").map_or("unknown", |v| v.to_str().unwrap_or("unknown")),
+ x_request_id = req.headers().get("x-request-id").map_or("none", |v| v.to_str().unwrap_or("none")),
"gRPC request"
);
@@ -64,6 +64,6 @@ impl Server {
impl Default for Server {
fn default() -> Self {
- Self::new().unwrap()
+ Self::new(CedarAuthorizer::default()).unwrap()
}
}