Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deny.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ allow = [
#"[email protected]",
#{ crate = "[email protected]", reason = "you can specify a reason it is allowed" },
]
# If true, workspace members are automatically allowed even when using deny-by-default
# This is useful for organizations that want to deny all external dependencies by default
# but allow their own workspace crates without having to explicitly list them
allow-workspace = false
# List of crates to deny
deny = [
#"[email protected]",
Expand Down
16 changes: 16 additions & 0 deletions docs/src/checks/bans/cfg.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,22 @@ allow = ["package-spec"]

Determines specific crates that are allowed. If the `allow` list has one or more entries, then any crate not in that list will be denied, so use with care. Each entry uses the same [PackageSpec](../cfg.md#package-specs) as other parts of cargo-deny's configuration.

### The `allow-workspace` field (optional)

```ini
allow-workspace = false
```

If `true`, automatically allows all workspace members even when using a deny-by-default policy (i.e., when `deny = [{ name = "*" }]` is specified). This is useful for organizations that want to implement strict dependency allowlists for external crates while automatically allowing their own workspace crates without having to explicitly list them in the `allow` configuration.

When `allow-workspace = true`:
- All workspace members are automatically treated as if they were in the `allow` list
- Workspace members take precedence over explicit `deny` entries
- External dependencies still require explicit allowlisting
- Works for both single-crate and multi-crate workspaces

**Default**: `false`

#### The `allow.reason` field (optional)

```ini
Expand Down
41 changes: 41 additions & 0 deletions examples/workspace_allow_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Workspace Allow Example

This example demonstrates the `allow-workspace` feature that enables a "deny external dependencies by default, but allow workspace crates" policy.

## Configuration

The `deny.toml` file shows how to:

1. **Deny all external dependencies by default**: `deny = [{ name = "*" }]`
2. **Automatically allow workspace members**: `allow-workspace = true`
3. **Explicitly allow blessed external dependencies**: List approved crates in the `allow` section

## Benefits

This configuration pattern is useful for organizations that want to:

- Maintain strict control over external dependencies
- Avoid having to manually list every workspace crate in the allow list
- Ensure consistent dependency policies across all Rust projects
- Reduce configuration maintenance overhead

## How it works

When `allow-workspace = true`:

- All workspace members are automatically treated as allowed
- Workspace members take precedence over explicit `deny` entries
- External dependencies still require explicit allowlisting
- Works for both single-crate and multi-crate workspaces

## Example scenarios

### ✅ Allowed
- Workspace crates (automatically allowed)
- External crates in the `allow` list (e.g., `serde`, `tokio`, `clap`)

### ❌ Blocked
- External crates not in the `allow` list
- Any dependency that would normally be denied

This approach eliminates the need for complex workarounds like dynamically modifying deny.toml files or using fragile sed/awk scripts to handle workspace crates.
16 changes: 16 additions & 0 deletions examples/workspace_allow_example/deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Example configuration demonstrating the allow-workspace feature
# This enables a "deny external dependencies by default, but allow workspace crates" policy

[bans]
# Deny all external dependencies by default
deny = [{ name = "*" }]

# Automatically allow workspace members even when using deny-by-default
allow-workspace = true

# Explicitly allow blessed external dependencies
allow = [
{ name = "serde", version = "1.0" },
{ name = "tokio", version = "1.0" },
{ name = "clap", version = "4.0" },
]
38 changes: 37 additions & 1 deletion src/bans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ pub fn check(
denied,
denied_multiple_versions,
allowed,
allow_workspace,
features,
workspace_default_features,
external_default_features,
Expand Down Expand Up @@ -380,6 +381,25 @@ pub fn check(
.collect(),
);

// Collect workspace members if allow_workspace is enabled
let workspace_members: std::collections::HashSet<Kid> = if allow_workspace {
let members: std::collections::HashSet<Kid> = ctx.krates
.workspace_members()
.filter_map(|node| {
if let krates::Node::Krate { id, .. } = node {
Some(id.clone())
} else {
None
}
})
.collect();


members
} else {
std::collections::HashSet::new()
};

let report_duplicates = |multi_detector: &mut MultiDetector<'_>, sink: &mut diag::ErrorSink| {
if multi_detector.dupes.len() != 1 {
// Filter out crates that depend on another version of themselves https://github.com/dtolnay/semver-trick
Expand Down Expand Up @@ -599,12 +619,20 @@ pub fn check(

// Check if the crate has been explicitly banned
if let Some(matches) = denied_ids.matches(krate) {
let is_workspace_member = workspace_members.contains(&krate.id);

for rm in matches {
let ban_cfg = CfgCoord {
file: file_id,
span: rm.specr.spec.name.span,
};

// If allow_workspace is enabled and this is a workspace member,
// skip the ban (workspace members take precedence over explicit bans)
if is_workspace_member && allow_workspace {
continue;
}

// The crate is banned, but it might be allowed if it's
// wrapped by one or more particular crates
let is_allowed_by_wrapper = if ban_wrappers.has_wrappers(rm.index) {
Expand Down Expand Up @@ -663,6 +691,8 @@ pub fn check(
if !allowed.0.is_empty() {
// Since only allowing specific crates is pretty draconian,
// also emit which allow filters actually passed each crate
let is_workspace_member = workspace_members.contains(&krate.id);

match allowed.matches(krate) {
Some(matches) => {
for rm in matches {
Expand All @@ -673,7 +703,13 @@ pub fn check(
}
}
None => {
pack.push(diags::NotAllowed { krate });
// If allow_workspace is enabled and this is a workspace member,
// automatically allow it without requiring explicit configuration
if is_workspace_member && allow_workspace {
// Workspace member is automatically allowed, no diagnostic needed
} else {
pack.push(diags::NotAllowed { krate });
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/bans/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ pub struct Config {
pub deny: Vec<CrateBan>,
/// If specified, means only the listed crates are allowed
pub allow: Vec<CrateAllow>,
/// If true, automatically allow all workspace members even when using deny-by-default
pub allow_workspace: bool,
/// Allows specifying features that are or are not allowed on crates
pub features: Vec<CrateFeatures>,
/// The default lint level for default features for external, non-workspace
Expand Down Expand Up @@ -407,6 +409,7 @@ impl Default for Config {
highlight: GraphHighlight::All,
deny: Vec::new(),
allow: Vec::new(),
allow_workspace: false,
features: Vec::new(),
external_default_features: None,
workspace_default_features: None,
Expand All @@ -431,6 +434,7 @@ impl<'de> Deserialize<'de> for Config {
let highlight = th.optional("highlight").unwrap_or_default();
let deny = th.optional("deny").unwrap_or_default();
let allow = th.optional("allow").unwrap_or_default();
let allow_workspace = th.optional("allow-workspace").unwrap_or_default();
let features = th.optional("features").unwrap_or_default();
let external_default_features = th.optional("external-default-features");
let workspace_default_features = th.optional("workspace-default-features");
Expand All @@ -452,6 +456,7 @@ impl<'de> Deserialize<'de> for Config {
highlight,
deny,
allow,
allow_workspace,
features,
external_default_features,
workspace_default_features,
Expand Down Expand Up @@ -769,6 +774,7 @@ impl crate::cfg::UnvalidatedConfig for Config {
denied,
denied_multiple_versions,
allowed,
allow_workspace: self.allow_workspace,
features,
external_default_features: self.external_default_features,
workspace_default_features: self.workspace_default_features,
Expand Down Expand Up @@ -942,6 +948,7 @@ pub struct ValidConfig {
pub(crate) denied: Vec<ValidKrateBan>,
pub(crate) denied_multiple_versions: Vec<PackageSpec>,
pub(crate) allowed: Vec<SpecAndReason>,
pub allow_workspace: bool,
pub(crate) features: Vec<ValidKrateFeatures>,
pub external_default_features: Option<Spanned<LintLevel>>,
pub workspace_default_features: Option<Spanned<LintLevel>>,
Expand Down
65 changes: 65 additions & 0 deletions tests/bans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,68 @@ skip = [

insta::assert_json_snapshot!(diags);
}

/// Tests the allow-workspace feature that automatically allows workspace members
/// even when using deny-by-default policy
#[test]
fn allow_workspace_members() {
let diags = gather_bans(
func_name!(),
KrateGather::new("workspace_allow"),
r#"
# Deny all external dependencies by default
deny = [{ name = "*" }]

# Allow specific external dependencies
allow = [{ name = "serde" }]

# Automatically allow workspace members
allow-workspace = true
"#,
);

insta::assert_json_snapshot!(diags);
}

/// Tests that allow-workspace=false (default) still blocks workspace members
/// when using deny-by-default policy
#[test]
fn deny_workspace_members_by_default() {
let diags = gather_bans(
func_name!(),
KrateGather::new("workspace_allow"),
r#"
# Deny all external dependencies by default
deny = [{ name = "*" }]

# Allow specific external dependencies
allow = [{ name = "serde" }]

# allow-workspace defaults to false, so workspace members should be blocked
"#,
);

insta::assert_json_snapshot!(diags);
}

/// Tests that workspace members take precedence over explicit bans
/// when allow-workspace=true
#[test]
fn workspace_members_override_explicit_bans() {
let diags = gather_bans(
func_name!(),
KrateGather::new("workspace_allow"),
r#"
# Explicitly ban workspace members
deny = [
{ name = "test-app" },
{ name = "test-lib" }
]

# But allow them via workspace setting (should take precedence)
allow-workspace = true
"#,
);

insta::assert_json_snapshot!(diags);
}
Loading