diff --git a/.changelog/25871.txt b/.changelog/25871.txt new file mode 100644 index 000000000..e8e2a8241 --- /dev/null +++ b/.changelog/25871.txt @@ -0,0 +1,3 @@ +```release-note:improvement +identity: Allow ACL policies to be applied to a namespace +``` diff --git a/command/acl_policy_apply.go b/command/acl_policy_apply.go index 1ec5cc105..6a396e222 100644 --- a/command/acl_policy_apply.go +++ b/command/acl_policy_apply.go @@ -35,14 +35,13 @@ Apply Options: -description Specifies a human readable description for the policy. + -namespace + Attaches the policy to the specified namespace. + -job Attaches the policy to the specified job. Requires that -namespace is also set. - -namespace - Attaches the policy to the specified namespace. Requires that -job is - also set. - -group Attaches the policy to the specified task group. Requires that -namespace and -job are also set. diff --git a/nomad/auth/auth.go b/nomad/auth/auth.go index a2d88a507..95a4fed7d 100644 --- a/nomad/auth/auth.go +++ b/nomad/auth/auth.go @@ -641,5 +641,23 @@ func (s *Authenticator) ResolvePoliciesForClaims(claims *structs.IdentityClaims) } } + iter, err = snap.ACLPolicyByNamespace(nil, alloc.Namespace) + if err != nil { + return nil, err + } + for { + raw := iter.Next() + if raw == nil { + break + } + + policy := raw.(*structs.ACLPolicy) + if policy.JobACL == nil { + continue + } + + policies = append(policies, policy) + } + return policies, nil } diff --git a/nomad/auth/auth_test.go b/nomad/auth/auth_test.go index 9be3bb78a..5de234474 100644 --- a/nomad/auth/auth_test.go +++ b/nomad/auth/auth_test.go @@ -1109,6 +1109,18 @@ func TestResolveClaims(t *testing.T) { JobID: claims.JobID, } + // policy for namespace + policy8 := mock.ACLPolicy() + policy8.JobACL = &structs.JobACL{ + Namespace: claims.Namespace, + } + + // policy for different namespace + policy9 := mock.ACLPolicy() + policy9.JobACL = &structs.JobACL{ + Namespace: "another", + } + aclObj, err := auth.resolveClaims(claims) must.Nil(t, aclObj) must.EqError(t, err, "allocation does not exist") @@ -1127,7 +1139,7 @@ func TestResolveClaims(t *testing.T) { // Add the policies index++ err = auth.getState().UpsertACLPolicies(structs.MsgTypeTestSetup, index, []*structs.ACLPolicy{ - policy0, policy1, policy2, policy3, policy4, policy5, policy6, policy7}) + policy0, policy1, policy2, policy3, policy4, policy5, policy6, policy7, policy8, policy9}) must.NoError(t, err) // Re-resolve and check that the resulting ACL looks reasonable @@ -1146,8 +1158,8 @@ func TestResolveClaims(t *testing.T) { policies, err := auth.ResolvePoliciesForClaims(claims) must.NoError(t, err) - must.Len(t, 3, policies) - must.SliceContainsAll(t, policies, []*structs.ACLPolicy{policy1, policy2, policy3}) + must.Len(t, 4, policies) + must.SliceContainsAll(t, policies, []*structs.ACLPolicy{policy1, policy2, policy3, policy8}) // Check the dispatch claims aclObj3, err := auth.resolveClaims(dispatchClaims) @@ -1157,8 +1169,8 @@ func TestResolveClaims(t *testing.T) { dispatchPolicies, err := auth.ResolvePoliciesForClaims(dispatchClaims) must.NoError(t, err) - must.Len(t, 3, dispatchPolicies) - must.SliceContainsAll(t, dispatchPolicies, []*structs.ACLPolicy{policy1, policy2, policy3}) + must.Len(t, 4, dispatchPolicies) + must.SliceContainsAll(t, dispatchPolicies, []*structs.ACLPolicy{policy1, policy2, policy3, policy8}) } diff --git a/nomad/state/schema.go b/nomad/state/schema.go index 8337d312a..021e7660e 100644 --- a/nomad/state/schema.go +++ b/nomad/state/schema.go @@ -907,10 +907,10 @@ func (a *ACLPolicyJobACLFieldIndex) FromObject(obj interface{}) (bool, []byte, e if ns == "" { return false, nil, nil } + jobID := policy.JobACL.JobID if jobID == "" { - return false, nil, fmt.Errorf( - "object %#v is not a valid ACLPolicy: Namespace without JobID", obj) + return true, []byte(ns + "\x00\x00"), nil } val := ns + "\x00" + jobID + "\x00" @@ -919,19 +919,27 @@ func (a *ACLPolicyJobACLFieldIndex) FromObject(obj interface{}) (bool, []byte, e // FromArgs is used to build an exact index lookup based on arguments func (a *ACLPolicyJobACLFieldIndex) FromArgs(args ...interface{}) ([]byte, error) { - if len(args) != 2 { - return nil, fmt.Errorf("must provide two arguments") + if len(args) < 1 || len(args) > 2 { + return nil, fmt.Errorf("must provide one or two arguments") } arg0, ok := args[0].(string) if !ok { return nil, fmt.Errorf("argument must be a string: %#v", args[0]) } + + if len(args) == 1 { + // Add two null characters to fully terminate a + // namespace only entry + return []byte(arg0 + "\x00\x00"), nil + } + arg1, ok := args[1].(string) if !ok { return nil, fmt.Errorf("argument must be a string: %#v", args[0]) } - // Add the null character as a terminator + // Add the null character as a separator between the + // namespace and job id and one for the terminator arg0 += "\x00" + arg1 + "\x00" return []byte(arg0), nil } diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index 1f8fcce24..a00656f65 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -6127,6 +6127,18 @@ func (s *StateStore) ACLPolicyByJob(ws memdb.WatchSet, ns, jobID string) (memdb. return iter, nil } +func (s *StateStore) ACLPolicyByNamespace(ws memdb.WatchSet, ns string) (memdb.ResultIterator, error) { + txn := s.db.ReadTxn() + + iter, err := txn.Get("acl_policy", "job", ns) + if err != nil { + return nil, fmt.Errorf("acl policy lookup failed: %v", err) + } + ws.Add(iter.WatchCh()) + + return iter, nil +} + // ACLPolicies returns an iterator over all the acl policies func (s *StateStore) ACLPolicies(ws memdb.WatchSet) (memdb.ResultIterator, error) { txn := s.db.ReadTxn() diff --git a/nomad/state/state_store_test.go b/nomad/state/state_store_test.go index 2b11c8ec7..59e326cc4 100644 --- a/nomad/state/state_store_test.go +++ b/nomad/state/state_store_test.go @@ -9142,6 +9142,55 @@ func TestStateStore_UpsertACLPolicy(t *testing.T) { } } +func TestStateStore_ACLPolicyByNamespace(t *testing.T) { + ci.Parallel(t) + + state := testStateStore(t) + policy := mock.ACLPolicy() + policy.JobACL = &structs.JobACL{ + Namespace: "default", + } + policy1 := mock.ACLPolicy() + policy1.JobACL = &structs.JobACL{ + Namespace: "default", + JobID: "test-ack-job-name", + } + policy2 := mock.ACLPolicy() + policy2.JobACL = &structs.JobACL{ + Namespace: "default", + JobID: "testing-job", + } + policy3 := mock.ACLPolicy() + policy3.JobACL = &structs.JobACL{ + Namespace: "testing", + JobID: "test-job", + } + + err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{policy, policy1, policy2}) + must.NoError(t, err) + + iter, err := state.ACLPolicyByNamespace(nil, "default") + must.NoError(t, err) + + out := iter.Next() + must.NotNil(t, out) + must.Eq(t, policy, out.(*structs.ACLPolicy)) + + count := 0 + for { + if iter.Next() == nil { + break + } + + count++ + } + must.Eq(t, 0, count) + + iter, err = state.ACLPolicyByNamespace(nil, "testing") + must.NoError(t, err) + must.Nil(t, iter.Next()) +} + func TestStateStore_DeleteACLPolicy(t *testing.T) { ci.Parallel(t) diff --git a/website/content/docs/concepts/workload-identity.mdx b/website/content/docs/concepts/workload-identity.mdx index 3caf74cf2..38834ab21 100644 --- a/website/content/docs/concepts/workload-identity.mdx +++ b/website/content/docs/concepts/workload-identity.mdx @@ -155,7 +155,7 @@ nomad acl policy apply \ redis-policy ./policy.hcl ``` -And you can apply this policy to all groups in the job by omitting both the +You can apply this policy to all groups in the job by omitting both the `-group` and `-task` flag: ```shell-session @@ -164,6 +164,14 @@ nomad acl policy apply \ redis-policy ./policy.hcl ``` +And you can apply this policy to all jobs in the namespace by omitting the +`-job`, `-group`, and `-task` flag: + +```shell-session +nomad acl policy apply \ + -namespace default redis-policy ./policy.hcl +``` + ### Task API It can be convenient to combine workload identity with Nomad's [Task API]