Skip to content

feat: add trusted-users list to AllowOnly guard policy for user integrity elevation#2584

Merged
lpcox merged 3 commits intomainfrom
copilot/add-trusted-users-list-to-allowonly-policy
Mar 26, 2026
Merged

feat: add trusted-users list to AllowOnly guard policy for user integrity elevation#2584
lpcox merged 3 commits intomainfrom
copilot/add-trusted-users-list-to-allowonly-policy

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

External users (contractors, cross-org collaborators) couldn't be elevated to approved integrity without globally lowering min-integrity. trusted-users adds a targeted allowlist inside allow-only, analogous to trusted-bots but for regular human users.

Policy shape

{
  "allow-only": {
    "repos": "public",
    "min-integrity": "approved",
    "trusted-users": ["contractor-1", "partner-dev"]
  }
}

Precedence: blocked-users > trusted-users > approval-labels > author_association

Changes

Go (config/guard/proxy)

  • internal/config/guard_policy.goTrustedUsers []string on AllowOnlyPolicy and NormalizedGuardPolicy; added UnmarshalJSON case, MarshalJSON serialization, and normalization (whitespace trim, empty-entry rejection, case-insensitive dedup)
  • internal/guard/wasm.goBuildLabelAgentPayload gains trustedUsers []string parameter; injects into the allow-only object (not top-level, unlike trusted-bots); buildStrictLabelAgentPayload validates trusted-users entries
  • internal/cmd/proxy.go--trusted-users flag, mirrors --trusted-bots
  • internal/proxy/proxy.goTrustedUsers in Config; forwarded through initGuardPolicy
  • internal/server/guard_init.goBuildLabelAgentPayload call updated; nil for injected trusted-users because gateway mode embeds them directly in the policy JSON

Rust (WASM guard)

  • src/lib.rstrusted_users: Vec<String> on AllowOnlyPolicy; passed into PolicyContext
  • src/labels/helpers.rstrusted_users field on PolicyContext; new is_trusted_user() (case-insensitive); author_association_floor() and pr_integrity() check is_trusted_user() alongside existing bot checks
  • src/labels/tool_rules.rs — issue/PR elevation checks (get_issue, pull_request_read) extended to is_trusted_user()

Tests

  • Go: parse/normalize/serialize round-trips, deduplication, error cases, BuildLabelAgentPayload injection
  • Rust: detection, case-insensitivity, issue/PR integrity elevation, FIRST_TIMER elevation, blocked-users precedence over trusted-users

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build967590049/b330/launcher.test /tmp/go-build967590049/b330/launcher.test -test.testlogfile=/tmp/go-build967590049/b330/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/c-p s/block.go 64/pkg/tool/linu-lang=go1.25 (dns block)
    • Triggering command: /tmp/go-build2594907654/b334/launcher.test /tmp/go-build2594907654/b334/launcher.test -test.testlogfile=/tmp/go-build2594907654/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s -gua�� -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.05.rcgu/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.06.rcgu/tmp/go-build702816338/b309/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.07.rcgu.o -guard/target/de/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/de/tmp/go-build702816338/b209/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.10.rcgu.o -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.11.rcgu.o -gua�� -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.13.rcgugrep -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.14.rcgu-qE d30fe3.rlib ithub-guard/rust/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/link vendor/golang.or-o -lang=go1.25 known-linux-gnu/-importcfg (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build967590049/b315/config.test /tmp/go-build967590049/b315/config.test -test.testlogfile=/tmp/go-build967590049/b315/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true go 7luPlZJ6G ache/uv/0.11.1/x-I (dns block)
    • Triggering command: /tmp/go-build2594907654/b319/config.test /tmp/go-build2594907654/b319/config.test -test.testlogfile=/tmp/go-build2594907654/b319/testlog.txt -test.paniconexit0 -test.timeout=10m0s rds/�� lib/rustlib/x86_embed-bitcode=no lib/rustlib/x86_-C -guard/target/debug/deps/rustcME6R4b/symbols.o -guard/target/de/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/de-unsafeptr=false -guard/target/de/tmp/go-build702816338/b094/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.03.rcgu.o -gua�� -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.05.rcgu/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.06.rcgu/tmp/go-build702816338/b314/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.07.rcgu.o -guard/target/de/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/de/tmp/go-build702816338/b238/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.10.rcgu.o -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.11.rcgu.o (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build967590049/b330/launcher.test /tmp/go-build967590049/b330/launcher.test -test.testlogfile=/tmp/go-build967590049/b330/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/c-p s/block.go 64/pkg/tool/linu-lang=go1.25 (dns block)
    • Triggering command: /tmp/go-build2594907654/b334/launcher.test /tmp/go-build2594907654/b334/launcher.test -test.testlogfile=/tmp/go-build2594907654/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s -gua�� -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.05.rcgu/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.06.rcgu/tmp/go-build702816338/b309/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.07.rcgu.o -guard/target/de/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/de/tmp/go-build702816338/b209/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.10.rcgu.o -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.11.rcgu.o -gua�� -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.13.rcgugrep -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.14.rcgu-qE d30fe3.rlib ithub-guard/rust/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/link vendor/golang.or-o -lang=go1.25 known-linux-gnu/-importcfg (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build967590049/b330/launcher.test /tmp/go-build967590049/b330/launcher.test -test.testlogfile=/tmp/go-build967590049/b330/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/c-p s/block.go 64/pkg/tool/linu-lang=go1.25 (dns block)
    • Triggering command: /tmp/go-build2594907654/b334/launcher.test /tmp/go-build2594907654/b334/launcher.test -test.testlogfile=/tmp/go-build2594907654/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s -gua�� -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.05.rcgu/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.06.rcgu/tmp/go-build702816338/b309/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.07.rcgu.o -guard/target/de/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -guard/target/de/tmp/go-build702816338/b209/vet.cfg -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.10.rcgu.o -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.11.rcgu.o -gua�� -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.13.rcgugrep -guard/target/debug/deps/serde_derive-f8a9da973ea849b8.serde_derive.291b3f11bb186a3e-cgu.14.rcgu-qE d30fe3.rlib ithub-guard/rust/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/link vendor/golang.or-o -lang=go1.25 known-linux-gnu/-importcfg (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build967590049/b339/mcp.test /tmp/go-build967590049/b339/mcp.test -test.testlogfile=/tmp/go-build967590049/b339/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/c-errorsas (dns block)
    • Triggering command: /tmp/go-build2594907654/b343/mcp.test /tmp/go-build2594907654/b343/mcp.test -test.testlogfile=/tmp/go-build2594907654/b343/testlog.txt -test.paniconexit0 -test.timeout=10m0s -gua�� -guard/target/debug/deps/github_guard-d6a03f414dc506a0.2j8pqcxyyfybe7nghv8aqe5ar.1du650a.rcgu.o -guard/target/debug/deps/github_guard-d6a03f414dc506a0.2qy5imqpflz26hi4smzxan76o.1du650a.rcgu.o d30fe3.rlib ebug.go mime/multipart known-linux-gnu/--version known-linux-gnu/lib/rustlib/x86_prefer-dynamic know�� ithub-guard/rust-guard/target/debug/deps/rustc0eIFPy/symbols.o ithub-guard/rust-guard/target/debug/deps/github_guard-d6a03f414dc506a0.0bxgpkkxlbr5ndvint8fbouk8test x_amd64/vet ithub-guard/rustgrep ithub-guard/rust-qE ithub-guard/rust(create|run) x_amd64/vet (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Add trusted-users list to AllowOnly guard policy for user integrity elevation feat: add trusted-users list to AllowOnly guard policy for user integrity elevation Mar 26, 2026
Copilot AI requested a review from lpcox March 26, 2026 16:05
@lpcox lpcox marked this pull request as ready for review March 26, 2026 17:19
Copilot AI review requested due to automatic review settings March 26, 2026 17:19
@lpcox lpcox merged commit 204ff33 into main Mar 26, 2026
28 checks passed
@lpcox lpcox deleted the copilot/add-trusted-users-list-to-allowonly-policy branch March 26, 2026 17:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for a trusted-users allowlist to the GitHub guard’s allow-only policy so specific external collaborators can be elevated to approved integrity without lowering min-integrity globally.

Changes:

  • Extends Go guard policy parsing/normalization/serialization to include trusted-users, with validation + case-insensitive deduplication.
  • Plumbs trusted-users through proxy/gateway initialization into the WASM guard (nested under allow-only), and updates the Go payload builder signature accordingly.
  • Updates Rust WASM guard policy context and integrity elevation logic to treat configured trusted users similarly to trusted bots; adds tests for detection/elevation/precedence.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/server/guard_init.go Updates LabelAgent payload construction call site (passes nil trusted-users for gateway mode).
internal/proxy/proxy.go Adds TrustedUsers to proxy config and forwards it into guard initialization.
internal/guard/wasm.go Accepts/validates trusted-users in strict payload validation; injects trusted users into allow-only during payload build.
internal/guard/wasm_test.go Adds Go tests for strict validation and payload injection of trusted-users.
internal/config/guard_policy.go Adds TrustedUsers to policy structs + JSON (un)marshal; normalizes/dedups trusted users.
internal/config/guard_policy_test.go Adds Go tests for parsing, normalization, and marshal behavior around trusted-users.
internal/cmd/proxy.go Adds --trusted-users flag and wires it into proxy config.
guards/github-guard/rust-guard/src/lib.rs Extends allow-only policy deserialization and passes trusted users into PolicyContext.
guards/github-guard/rust-guard/src/labels/helpers.rs Adds trusted_users to context + is_trusted_user() and uses it in integrity elevation paths.
guards/github-guard/rust-guard/src/labels/tool_rules.rs Extends tool-level elevation checks to include is_trusted_user().
guards/github-guard/rust-guard/src/labels/mod.rs Adds Rust unit tests for trusted user detection/elevation and precedence.
Comments suppressed due to low confidence (1)

guards/github-guard/rust-guard/src/labels/tool_rules.rs:195

  • Blocked-users precedence is not enforced in this trusted elevation block: a login present in blocked_users but also in trusted_users (or a trusted bot) will be promoted to approved integrity. This violates the stated precedence (blocked-users > trusted-users). Consider checking is_blocked_user(login, ctx) first and returning blocked_integrity (or preventing any max_integrity elevation) before applying trusted user/bot promotion.
                        // Elevate trusted first-party bots and trusted users to approved
                        if let Some(ref login) = facts.author_login {
                            if is_trusted_first_party_bot(login)
                                || is_configured_trusted_bot(login, ctx)
                                || is_trusted_user(login, ctx)
                            {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +506 to +514
// If allow-only is absent, the injection is skipped and buildStrictLabelAgentPayload
// will reject the payload when called with the missing allow-only key.
users := make([]interface{}, len(trustedUsers))
for i, u := range trustedUsers {
users[i] = u
}
// Inject into allow-only object if present
if allowOnly, ok := payload["allow-only"].(map[string]interface{}); ok {
allowOnly["trusted-users"] = users
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildLabelAgentPayload injects trusted-users only when the policy uses the canonical "allow-only" key. However, buildStrictLabelAgentPayload explicitly supports the legacy top-level key "allowonly" for backward compatibility, so a legacy policy would silently skip trusted-users injection. Consider injecting into "allowonly" as well (or normalizing the key first) and add a test covering legacy allowonly + --trusted-users.

Suggested change
// If allow-only is absent, the injection is skipped and buildStrictLabelAgentPayload
// will reject the payload when called with the missing allow-only key.
users := make([]interface{}, len(trustedUsers))
for i, u := range trustedUsers {
users[i] = u
}
// Inject into allow-only object if present
if allowOnly, ok := payload["allow-only"].(map[string]interface{}); ok {
allowOnly["trusted-users"] = users
// If allow-only/allowonly is absent, the injection is skipped and
// buildStrictLabelAgentPayload will reject the payload when called
// with the missing allow-only key.
users := make([]interface{}, len(trustedUsers))
for i, u := range trustedUsers {
users[i] = u
}
// Inject into canonical allow-only object if present
if allowOnly, ok := payload["allow-only"].(map[string]interface{}); ok {
allowOnly["trusted-users"] = users
} else if allowOnlyLegacy, ok := payload["allowonly"].(map[string]interface{}); ok {
// Also support legacy top-level allowonly key for backward compatibility.
allowOnlyLegacy["trusted-users"] = users

Copilot uses AI. Check for mistakes.
}

// Validate trusted-users if present inside allow-only.
// Must be a non-empty array of non-empty strings when present.
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trusted-users validation comment says the list "must be a non-empty array" when present, but the implementation currently accepts an empty array (it only type-checks and validates entries). Either enforce len(arr) > 0 or adjust the comment to match the actual behavior.

Suggested change
// Must be a non-empty array of non-empty strings when present.
// Must be an array of non-empty strings when present; an empty array is allowed.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to 134
// Elevate trusted first-party bots and trusted users to approved
if let Some(ref login) = info.author_login {
if is_trusted_first_party_bot(login)
|| is_configured_trusted_bot(login, ctx)
|| is_trusted_user(login, ctx)
{
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocked-users precedence is not enforced here: a login that is in blocked_users but also matches is_trusted_user/is_trusted_* will be elevated to approved integrity in apply_tool_labels. This conflicts with the documented rule that blocked_users overrides trusted_users/bots. Consider importing is_blocked_user (and/or blocked_integrity) and short-circuiting to blocked integrity before applying any trusted elevation, or at least gating the elevation on !is_blocked_user(login, ctx).

This issue also appears on line 190 of the same file.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add trusted-users list to AllowOnly guard policy for user integrity elevation

3 participants