Skip to content

wi: new endpoint for listing workload attached ACL policies #25588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9b27f84
acl: obsolete ResolveToken and use WhoAmI RPC endpoint instead
pkazmierczak Mar 28, 2025
0362ab6
Update command/agent/acl_endpoint.go
pkazmierczak Mar 28, 2025
3aff3b2
better error message
pkazmierczak Mar 31, 2025
0fa3bfb
restore RPC
pkazmierczak Mar 31, 2025
7db690e
WhoAmI RPC should set query meta
pkazmierczak Mar 31, 2025
6e72924
set reply index if there's an ACL token
pkazmierczak Apr 1, 2025
654661d
draft
pkazmierczak Apr 2, 2025
a59e92d
url update
pkazmierczak Apr 3, 2025
1454528
typo in the api route
pkazmierczak Apr 3, 2025
605bc14
move SelfPolicy method to ACLPolicies
pkazmierczak Apr 3, 2025
7435e19
add check for WI policies in acl token self CLI
pkazmierczak Apr 3, 2025
d8f496a
fix URL handling
pkazmierczak Apr 3, 2025
d4e97f6
changelog
pkazmierczak Apr 3, 2025
1683ae7
documentation
pkazmierczak Apr 3, 2025
3ae337a
API doc
pkazmierczak Apr 3, 2025
50686ec
cli unit test
pkazmierczak Apr 3, 2025
8565bf2
acl endpoint test
pkazmierczak Apr 3, 2025
38df7db
oopsie forgot to remove some copy pasta from Schmichael
pkazmierczak Apr 3, 2025
7d8e6ce
more elaborate acl policy self output
pkazmierczak Apr 4, 2025
739ecc6
reformat mdx tables
pkazmierczak Apr 4, 2025
56dfa5f
, n
pkazmierczak Apr 25, 2025
d229c19
s/auth methods/policies
pkazmierczak May 6, 2025
4053a48
list policies regardless of WI
pkazmierczak May 6, 2025
06d0e0c
Apply suggestions from code review
pkazmierczak May 19, 2025
da46b31
always return ACLPolicyListStub
pkazmierczak May 19, 2025
31e6e4a
applied Tim's comments
pkazmierczak May 19, 2025
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
3 changes: 3 additions & 0 deletions .changelog/25588.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
wi: new API endpoint for listing workload-attached ACL policies
```
11 changes: 11 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ func (a *ACLPolicies) Info(policyName string, q *QueryOptions) (*ACLPolicy, *Que
return &resp, wm, nil
}

// Self is used to query policies attached to a workload identity
func (a *ACLPolicies) Self(q *QueryOptions) ([]*ACLPolicyListStub, *QueryMeta, error) {
var resp []*ACLPolicyListStub
wm, err := a.client.query("/v1/acl/policy/self", &resp, q)
if err != nil {
return nil, nil, err
}
return resp, wm, nil
}

// ACLTokens is used to query the ACL token endpoints.
type ACLTokens struct {
client *Client
Expand Down Expand Up @@ -509,6 +519,7 @@ func (a *ACLAuth) Login(req *ACLLoginRequest, q *WriteOptions) (*ACLToken, *Writ
type ACLPolicyListStub struct {
Name string
Description string
JobACL *JobACL
CreateIndex uint64
ModifyIndex uint64
}
Expand Down
2 changes: 1 addition & 1 deletion api/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func assertQueryMeta(t *testing.T, qm *QueryMeta) {
t.Helper()

must.NotEq(t, 0, qm.LastIndex, must.Sprint("bad index"))
must.NotEq(t, 0, qm.LastIndex, must.Sprint("expected QueryMeta.LastIndex to be != 0"))
must.True(t, qm.KnownLeader, must.Sprint("expected a known leader but gone none"))
}

Expand Down
129 changes: 129 additions & 0 deletions command/acl_policy_self.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package command

import (
"fmt"
"strings"

"github.com/posener/complete"
)

type ACLPolicySelfCommand struct {
Meta

json bool
tmpl string
}

func (c *ACLPolicySelfCommand) Help() string {
helpText := `
Usage: nomad acl policy self

Self is used to fetch information about the policy assigned to the current
workload identity or ACL token.

General Options:

` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `

ACL List Options:

-json
Output the ACL policies in a JSON format.

-t
Format and display the ACL policies using a Go template.
`
return strings.TrimSpace(helpText)
}

func (c *ACLPolicySelfCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}

func (c *ACLPolicySelfCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}

func (c *ACLPolicySelfCommand) Synopsis() string {
return "Lookup self ACL policy assigned to the workload identity or ACL token"
}

func (c *ACLPolicySelfCommand) Name() string { return "acl policy self" }

func (c *ACLPolicySelfCommand) Run(args []string) int {
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&c.json, "json", false, "")
flags.StringVar(&c.tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
}

// Check that we have no arguments
args = flags.Args()
if l := len(args); l != 0 {
c.Ui.Error("This command takes no arguments")
c.Ui.Error(commandErrorText(c))
return 1
}

// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}

policies, _, err := client.ACLPolicies().Self(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error fetching WI policies: %s", err))
return 1
}

if len(policies) == 0 {
c.Ui.Output("No policies found.")
} else {
if c.json || len(c.tmpl) > 0 {
out, err := Format(c.json, c.tmpl, policies)
if err != nil {
c.Ui.Error(err.Error())
return 1
}

c.Ui.Output(out)
return 0
}

output := make([]string, 0, len(policies)+1)
output = append(output, "Name|Job ID|Group Name|Task Name")
for _, p := range policies {
var outputString string
if p.JobACL == nil {
outputString = fmt.Sprintf("%s|%s|%s|%s", p.Name, "<unavailable>", "<unavailable>", "<unavailable>")
} else {
outputString = fmt.Sprintf(
"%s|%s|%s|%s",
p.Name, formatJobACL(p.JobACL.JobID), formatJobACL(p.JobACL.Group), formatJobACL(p.JobACL.Task),
)
}
output = append(output, outputString)
}

c.Ui.Output(formatList(output))
}
return 0
}

func formatJobACL(jobACL string) string {
if jobACL == "" {
return "<not specified>"
}
return jobACL
}
79 changes: 79 additions & 0 deletions command/acl_policy_self_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package command

import (
"testing"

"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/shoenig/test/must"
)

func TestACLPolicySelfCommand_ViaEnvVar(t *testing.T) {
config := func(c *agent.Config) {
c.ACL.Enabled = true
}

srv, _, url := testServer(t, true, config)
defer srv.Shutdown()

state := srv.Agent.Server().State()

// Bootstrap an initial ACL token
token := srv.RootToken
must.NotNil(t, token)

// Create a minimal job
job := mock.MinJob()

// Add a job policy
polArgs := structs.ACLPolicyUpsertRequest{
Policies: []*structs.ACLPolicy{
{
Name: "nw",
Description: "test job can write to nodes",
Rules: `node { policy = "write" }`,
JobACL: &structs.JobACL{
Namespace: job.Namespace,
JobID: job.ID,
},
},
},
WriteRequest: structs.WriteRequest{
Region: job.Region,
AuthToken: token.SecretID,
Namespace: job.Namespace,
},
}
polReply := structs.GenericResponse{}
must.NoError(t, srv.RPC("ACL.UpsertPolicies", &polArgs, &polReply))
must.NonZero(t, polReply.WriteMeta.Index)

ui := cli.NewMockUi()
cmd := &ACLPolicySelfCommand{Meta: Meta{Ui: ui, flagAddress: url}}

allocs := testutil.WaitForRunningWithToken(t, srv.RPC, job, token.SecretID)
must.Len(t, 1, allocs)

alloc, err := state.AllocByID(nil, allocs[0].ID)
must.NoError(t, err)
must.MapContainsKey(t, alloc.SignedIdentities, "t")
wid := alloc.SignedIdentities["t"]

// Fetch info on policies with a JWT
t.Setenv("NOMAD_TOKEN", wid)
code := cmd.Run([]string{"-address=" + url})
must.Zero(t, code)

// Check the output
out := ui.OutputWriter.String()
must.StrContains(t, out, polArgs.Policies[0].Name)

// make sure we put the job ACLs in there, too
must.StrContains(t, out, polArgs.Policies[0].JobACL.JobID)
}
32 changes: 25 additions & 7 deletions command/acl_token_self.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package command

import (
"fmt"
"os"
"strings"

"github.com/hashicorp/nomad/helper"
"github.com/posener/complete"
)

Expand Down Expand Up @@ -63,14 +65,30 @@ func (c *ACLTokenSelfCommand) Run(args []string) int {
return 1
}

// Get the specified token information
token, _, err := client.ACLTokens().Self(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error fetching self token: %s", err))
// Check what kind of token we have available
envToken := os.Getenv("NOMAD_TOKEN")
if envToken == "" {
c.Ui.Error("No token present in the environment")
return 1
}

// Format the output
outputACLToken(c.Ui, token)
return 0
// Does this look like a Nomad ACL token?
if helper.IsUUID(envToken) {
token, _, err := client.ACLTokens().Self(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error fetching self token: %s", err))
return 1
}
// Format the output
outputACLToken(c.Ui, token)
return 0
}

policies, _, err := client.ACLPolicies().Self(nil)
if err == nil && len(policies) > 0 {
c.Ui.Info("No ACL token found but there are ACL policies attached to this workload identity. You can query them with acl policy self command.")
return 0
}
c.Ui.Error("No ACL tokens, nor ACL policies attached to a workload identity found.")
return 1
}
Loading
Loading