Skip to content

Commit

Permalink
server: allow delegation in AuthorizedKeyPolicy
Browse files Browse the repository at this point in the history
This adds a delegation config flag to AuthorizedKeyPolicy which, when
enabled, causes the policy to only check authorized keys for init
entries, relying on proper log validation of permissions for all other
entries.
  • Loading branch information
lann committed Aug 24, 2023
1 parent c8b7396 commit a7c5a90
Showing 1 changed file with 111 additions and 40 deletions.
151 changes: 111 additions & 40 deletions crates/server/src/policy/record/authorization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,39 @@ use anyhow::{bail, Result};
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use warg_crypto::signing::KeyID;
use warg_protocol::{package::PackageRecord, registry::PackageId, ProtoEnvelope};
use warg_protocol::{
package::{PackageEntry, PackageRecord},
registry::PackageId,
ProtoEnvelope,
};
use wasmparser::names::KebabStr;

/// A policy that ensures a published record is authorized by
/// the key signing the record.
/// A policy that ensures a published record is signed by an authorized key.
#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthorizedKeyPolicy {
#[serde(skip)]
superuser_keys: HashSet<KeyID>,
#[serde(default, rename = "namespace")]
namespaces: HashMap<String, LogPolicy>,
#[serde(default, rename = "package")]
packages: HashMap<PackageId, LogPolicy>,
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct LogPolicy {
// Authorized key IDs
keys: HashSet<KeyID>,
// If true, permission grants are permitted.
#[serde(default)]
namespace_keys: HashMap<String, HashSet<KeyID>>,
#[serde(default)]
package_keys: HashMap<PackageId, HashSet<KeyID>>,
delegation: bool,
}

impl LogPolicy {
fn key_authorized_for_entry(&self, key: &KeyID, is_init: bool) -> bool {
(self.delegation && !is_init) || self.keys.contains(key)
}
}

impl AuthorizedKeyPolicy {
Expand All @@ -34,40 +54,62 @@ impl AuthorizedKeyPolicy {

/// Sets an authorized key for publishing to a particular namespace.
pub fn with_namespace_key(mut self, namespace: impl Into<String>, key: KeyID) -> Result<Self> {
self.namespace_or_default_mut(namespace)?.keys.insert(key);
Ok(self)
}

/// Enables delegation for a particular namespace.
pub fn with_namespace_delegation(mut self, namespace: impl Into<String>) -> Result<Self> {
self.namespace_or_default_mut(namespace)?.delegation = true;
Ok(self)
}

fn namespace_or_default_mut(&mut self, namespace: impl Into<String>) -> Result<&mut LogPolicy> {
let namespace = namespace.into();
if KebabStr::new(&namespace).is_none() {
bail!("namespace `{namespace}` is not a legal kebab-case identifier");
}

self.namespace_keys
.entry(namespace)
.or_default()
.insert(key);
Ok(self)
Ok(self.namespaces.entry(namespace).or_default())
}

/// Sets an authorized key for publishing to a particular package.
pub fn with_package_key(mut self, package_id: impl Into<String>, key: KeyID) -> Result<Self> {
let package_id = PackageId::new(package_id)?;
self.package_keys.entry(package_id).or_default().insert(key);
self.package_or_default_mut(package_id)?.keys.insert(key);
Ok(self)
}

pub fn key_authorized_for_package(&self, key: &KeyID, package: &PackageId) -> bool {
/// Enables delegation for a particular package.
pub fn with_package_delegation(mut self, package_id: impl Into<String>) -> Result<Self> {
self.package_or_default_mut(package_id)?.delegation = true;
Ok(self)
}

fn package_or_default_mut(&mut self, package_id: impl Into<String>) -> Result<&mut LogPolicy> {
let package_id = PackageId::new(package_id)?;
Ok(self.packages.entry(package_id).or_default())
}

pub fn key_authorized_for_entry(
&self,
key: &KeyID,
package: &PackageId,
is_init: bool,
) -> bool {
if self.superuser_keys.contains(key) {
return true;
}

let namespace_keys = self.namespace_keys.get(package.namespace());
if namespace_keys
.map(|keys| keys.contains(key))
.unwrap_or(false)
{
return true;
if let Some(policy) = self.namespaces.get(package.namespace()) {
if policy.key_authorized_for_entry(key, is_init) {
return true;
}
}

let package_keys = self.package_keys.get(package);
if package_keys.map(|keys| keys.contains(key)).unwrap_or(false) {
return true;
if let Some(policy) = self.packages.get(package) {
if policy.key_authorized_for_entry(key, is_init) {
return true;
}
}

false
Expand All @@ -81,10 +123,13 @@ impl RecordPolicy for AuthorizedKeyPolicy {
record: &ProtoEnvelope<PackageRecord>,
) -> RecordPolicyResult<()> {
let key = record.key_id();
if !self.key_authorized_for_package(key, id) {
return Err(RecordPolicyError::Unauthorized(format!(
"key id `{key}` is not authorized to publish to package `{id}`",
)));
for entry in &record.as_ref().entries {
let is_init = matches!(entry, PackageEntry::Init { .. });
if !self.key_authorized_for_entry(key, id, is_init) {
return Err(RecordPolicyError::Unauthorized(format!(
"key id `{key}` is not authorized to publish to package `{id}`",
)));
}
}
Ok(())
}
Expand All @@ -107,25 +152,51 @@ mod tests {
.with_package_key("my-namespace:my-package", package_key.clone())?;

let my_package: PackageId = "my-namespace:my-package".parse()?;
let my_namespace_other_package: PackageId = "my-namespace:other-package".parse()?;
let my_ns_other_package: PackageId = "my-namespace:other-package".parse()?;
let other_namespace: PackageId = "other-namespace:any-package".parse()?;

assert!(policy.key_authorized_for_package(&super_key, &my_package));
assert!(policy.key_authorized_for_package(&super_key, &my_namespace_other_package));
assert!(policy.key_authorized_for_package(&super_key, &other_namespace));
assert!(policy.key_authorized_for_entry(&super_key, &my_package, false));
assert!(policy.key_authorized_for_entry(&super_key, &my_ns_other_package, false));
assert!(policy.key_authorized_for_entry(&super_key, &other_namespace, false));

assert!(policy.key_authorized_for_package(&namespace_key, &my_package));
assert!(policy.key_authorized_for_package(&namespace_key, &my_namespace_other_package));
assert!(!policy.key_authorized_for_package(&namespace_key, &other_namespace));
assert!(policy.key_authorized_for_entry(&namespace_key, &my_package, false));
assert!(policy.key_authorized_for_entry(&namespace_key, &my_ns_other_package, false));
assert!(!policy.key_authorized_for_entry(&namespace_key, &other_namespace, false));

assert!(policy.key_authorized_for_package(&package_key, &my_package));
assert!(!policy.key_authorized_for_package(&package_key, &my_namespace_other_package));
assert!(!policy.key_authorized_for_package(&package_key, &other_namespace));
assert!(policy.key_authorized_for_entry(&package_key, &my_package, false));
assert!(!policy.key_authorized_for_entry(&package_key, &my_ns_other_package, false));
assert!(!policy.key_authorized_for_entry(&package_key, &other_namespace, false));

assert!(!policy.key_authorized_for_package(&other_key, &my_package));
assert!(!policy.key_authorized_for_package(&other_key, &my_namespace_other_package));
assert!(!policy.key_authorized_for_package(&other_key, &other_namespace));
assert!(!policy.key_authorized_for_entry(&other_key, &my_package, false));
assert!(!policy.key_authorized_for_entry(&other_key, &my_ns_other_package, false));
assert!(!policy.key_authorized_for_entry(&other_key, &other_namespace, false));

Ok(())
}

#[test]
fn test_key_authorized_for_package_init() -> Result<()> {
let authed_key = KeyID::from("authed-key".to_string());
let other_key = KeyID::from("other-key".to_string());

let policy = AuthorizedKeyPolicy::new()
.with_namespace_key("ns1", authed_key.clone())?
.with_namespace_delegation("ns1")?
.with_package_key("ns2:pkg", authed_key.clone())?
.with_package_delegation("ns2:pkg")?;

let ns1_pkg: PackageId = "ns1:pkg".parse()?;
let ns2_pkg: PackageId = "ns2:pkg".parse()?;

assert!(policy.key_authorized_for_entry(&authed_key, &ns1_pkg, true));
assert!(policy.key_authorized_for_entry(&authed_key, &ns1_pkg, false));
assert!(policy.key_authorized_for_entry(&authed_key, &ns2_pkg, true));
assert!(policy.key_authorized_for_entry(&authed_key, &ns2_pkg, false));

assert!(!policy.key_authorized_for_entry(&other_key, &ns1_pkg, true));
assert!(policy.key_authorized_for_entry(&other_key, &ns1_pkg, false));
assert!(!policy.key_authorized_for_entry(&other_key, &ns2_pkg, true));
assert!(policy.key_authorized_for_entry(&other_key, &ns2_pkg, false));
Ok(())
}
}

0 comments on commit a7c5a90

Please sign in to comment.