Merge pull request #15062 from hashicorp/post-1.4.2-release
Post 1.4.2 release
This commit is contained in:
commit
8ac41c167f
|
@ -0,0 +1,7 @@
|
|||
```release-note:security
|
||||
variables: Fixed a bug where non-sensitive variable metadata (paths and raft indexes) was exposed via the template `nomadVarList` function to other jobs in the same namespace.
|
||||
```
|
||||
|
||||
```release-note:bug
|
||||
variables: Fixed a bug where getting empty results from listing variables resulted in a permission denied error.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:security
|
||||
event stream: Fixed a bug where ACL token expiration was not checked when emitting events
|
||||
```
|
|
@ -45,6 +45,15 @@ rules:
|
|||
...
|
||||
... := $T.handleMixedAuthEndpoint(...)
|
||||
...
|
||||
# Pattern used by endpoints that support both normal ACLs and
|
||||
# workload identity but break authentication and authorization up
|
||||
- pattern-not-inside: |
|
||||
if done, err := $A.$B.forward($METHOD, ...); done {
|
||||
return err
|
||||
}
|
||||
...
|
||||
... := $T.authorize(...)
|
||||
...
|
||||
# Pattern used by some Node endpoints.
|
||||
- pattern-not-inside: |
|
||||
if done, err := $A.$B.forward($METHOD, ...); done {
|
||||
|
|
78
CHANGELOG.md
78
CHANGELOG.md
|
@ -1,3 +1,53 @@
|
|||
## 1.4.2 (October 26, 2022)
|
||||
|
||||
SECURITY:
|
||||
|
||||
* event stream: Fixed a bug where ACL token expiration was not checked when emitting events [[GH-15013](https://github.com/hashicorp/nomad/issues/15013)]
|
||||
* variables: Fixed a bug where non-sensitive variable metadata (paths and raft indexes) was exposed via the template `nomadVarList` function to other jobs in the same namespace. [[GH-15012](https://github.com/hashicorp/nomad/issues/15012)]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* cli: Added `-id-prefix-template` option to `nomad job dispatch` [[GH-14631](https://github.com/hashicorp/nomad/issues/14631)]
|
||||
* cli: add nomad fmt to the CLI [[GH-14779](https://github.com/hashicorp/nomad/issues/14779)]
|
||||
* deps: update go-memdb for goroutine leak fix [[GH-14983](https://github.com/hashicorp/nomad/issues/14983)]
|
||||
* docker: improve memory usage for docker_logger [[GH-14875](https://github.com/hashicorp/nomad/issues/14875)]
|
||||
* event stream: Added ACL role topic with create and delete types [[GH-14923](https://github.com/hashicorp/nomad/issues/14923)]
|
||||
* scheduler: Allow jobs not requiring network resources even when no network is fingerprinted [[GH-14300](https://github.com/hashicorp/nomad/issues/14300)]
|
||||
* ui: adds searching and filtering to the topology page [[GH-14913](https://github.com/hashicorp/nomad/issues/14913)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* acl: Callers should be able to read policies linked via roles to the token used [[GH-14982](https://github.com/hashicorp/nomad/issues/14982)]
|
||||
* acl: Ensure all federated servers meet v.1.4.0 minimum before ACL roles can be written [[GH-14908](https://github.com/hashicorp/nomad/issues/14908)]
|
||||
* acl: Fixed a bug where Nomad version checking for one-time tokens was enforced across regions [[GH-14912](https://github.com/hashicorp/nomad/issues/14912)]
|
||||
* cli: prevent a panic when the Nomad API returns an error while collecting a debug bundle [[GH-14992](https://github.com/hashicorp/nomad/issues/14992)]
|
||||
* client: Check ACL token expiry when resolving token within ACL cache [[GH-14922](https://github.com/hashicorp/nomad/issues/14922)]
|
||||
* client: Fixed a bug where Nomad could not detect cores on recent RHEL systems [[GH-15027](https://github.com/hashicorp/nomad/issues/15027)]
|
||||
* client: Fixed a bug where network fingerprinters were not reloaded when the client configuration was reloaded with SIGHUP [[GH-14615](https://github.com/hashicorp/nomad/issues/14615)]
|
||||
* client: Resolve ACL roles within client ACL cache [[GH-14922](https://github.com/hashicorp/nomad/issues/14922)]
|
||||
* consul: Fixed a bug where services continuously re-registered [[GH-14917](https://github.com/hashicorp/nomad/issues/14917)]
|
||||
* consul: atomically register checks on initial service registration [[GH-14944](https://github.com/hashicorp/nomad/issues/14944)]
|
||||
* deps: Update hashicorp/consul-template to 90370e07bf621811826b803fb633dadbfb4cf287; fixes template rerendering issues when only user or group set [[GH-15045](https://github.com/hashicorp/nomad/issues/15045)]
|
||||
* deps: Update hashicorp/raft to v1.3.11; fixes unstable leadership on server removal [[GH-15021](https://github.com/hashicorp/nomad/issues/15021)]
|
||||
* event stream: Check ACL token expiry when resolving tokens [[GH-14923](https://github.com/hashicorp/nomad/issues/14923)]
|
||||
* event stream: Resolve ACL roles within ACL tokens [[GH-14923](https://github.com/hashicorp/nomad/issues/14923)]
|
||||
* keyring: Fixed a bug where `nomad system gc` forced a root keyring rotation. [[GH-15009](https://github.com/hashicorp/nomad/issues/15009)]
|
||||
* keyring: Fixed a bug where if a key is rotated immediately following a leader election, plans that are in-flight may get signed before the new leader has the key. Allow for a short timeout-and-retry to avoid rejecting plans. [[GH-14987](https://github.com/hashicorp/nomad/issues/14987)]
|
||||
* keyring: Fixed a bug where keyring initialization is blocked by un-upgraded federated regions [[GH-14901](https://github.com/hashicorp/nomad/issues/14901)]
|
||||
* keyring: Fixed a bug where root keyring garbage collection configuration values were not respected. [[GH-15009](https://github.com/hashicorp/nomad/issues/15009)]
|
||||
* keyring: Fixed a bug where root keyring initialization could occur before the raft FSM on the leader was verified to be up-to-date. [[GH-14987](https://github.com/hashicorp/nomad/issues/14987)]
|
||||
* keyring: Fixed a bug where root keyring replication could make incorrectly stale queries and exit early if those queries did not return the expected key. [[GH-14987](https://github.com/hashicorp/nomad/issues/14987)]
|
||||
* keyring: Fixed a bug where the root keyring replicator's rate limiting would be skipped if the keyring replication exceeded the burst rate. [[GH-14987](https://github.com/hashicorp/nomad/issues/14987)]
|
||||
* keyring: Removed root key garbage collection to avoid orphaned workload identities [[GH-15034](https://github.com/hashicorp/nomad/issues/15034)]
|
||||
* nomad native service discovery: Ensure all local servers meet v.1.3.0 minimum before service registrations can be written [[GH-14924](https://github.com/hashicorp/nomad/issues/14924)]
|
||||
* scheduler: Fixed a bug where version checking for disconnected clients handling was enforced across regions [[GH-14912](https://github.com/hashicorp/nomad/issues/14912)]
|
||||
* servicedisco: Fixed a bug where job using checks could land on incompatible client [[GH-14868](https://github.com/hashicorp/nomad/issues/14868)]
|
||||
* services: Fixed a regression where check task validation stopped allowing some configurations [[GH-14864](https://github.com/hashicorp/nomad/issues/14864)]
|
||||
* ui: Fixed line charts to update x-axis (time) where relevant [[GH-14814](https://github.com/hashicorp/nomad/issues/14814)]
|
||||
* ui: Fixes an issue where service tags would bleed past the edge of the screen [[GH-14832](https://github.com/hashicorp/nomad/issues/14832)]
|
||||
* variables: Fixed a bug where Nomad version checking was not enforced for writing to variables [[GH-14912](https://github.com/hashicorp/nomad/issues/14912)]
|
||||
* variables: Fixed a bug where getting empty results from listing variables resulted in a permission denied error. [[GH-15012](https://github.com/hashicorp/nomad/issues/15012)]
|
||||
|
||||
## 1.4.1 (October 06, 2022)
|
||||
|
||||
BUG FIXES:
|
||||
|
@ -78,6 +128,23 @@ BUG FIXES:
|
|||
* template: Fixed a bug where the `splay` timeout was not being applied when `change_mode` was set to `script`. [[GH-14749](https://github.com/hashicorp/nomad/issues/14749)]
|
||||
* ui: Remove extra space when displaying the version in the menu footer. [[GH-14457](https://github.com/hashicorp/nomad/issues/14457)]
|
||||
|
||||
## 1.3.7 (October 26, 2022)
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* deps: update go-memdb for goroutine leak fix [[GH-14983](https://github.com/hashicorp/nomad/issues/14983)]
|
||||
* docker: improve memory usage for docker_logger [[GH-14875](https://github.com/hashicorp/nomad/issues/14875)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* acl: Fixed a bug where Nomad version checking for one-time tokens was enforced across regions [[GH-14911](https://github.com/hashicorp/nomad/issues/14911)]
|
||||
* client: Fixed a bug where Nomad could not detect cores on recent RHEL systems [[GH-15027](https://github.com/hashicorp/nomad/issues/15027)]
|
||||
* consul: Fixed a bug where services continuously re-registered [[GH-14917](https://github.com/hashicorp/nomad/issues/14917)]
|
||||
* consul: atomically register checks on initial service registration [[GH-14944](https://github.com/hashicorp/nomad/issues/14944)]
|
||||
* deps: Update hashicorp/raft to v1.3.11; fixes unstable leadership on server removal [[GH-15021](https://github.com/hashicorp/nomad/issues/15021)]
|
||||
* nomad native service discovery: Ensure all local servers meet v.1.3.0 minimum before service registrations can be written [[GH-14924](https://github.com/hashicorp/nomad/issues/14924)]
|
||||
* scheduler: Fixed a bug where version checking for disconnected clients handling was enforced across regions [[GH-14911](https://github.com/hashicorp/nomad/issues/14911)]
|
||||
|
||||
## 1.3.6 (October 04, 2022)
|
||||
|
||||
SECURITY:
|
||||
|
@ -388,6 +455,17 @@ BUG FIXES:
|
|||
* ui: fix broken link to task-groups in the Recent Allocations table in the Job Detail overview page. [[GH-12765](https://github.com/hashicorp/nomad/issues/12765)]
|
||||
* ui: fix the unit for the task row memory usage metric [[GH-11980](https://github.com/hashicorp/nomad/issues/11980)]
|
||||
|
||||
## 1.2.14 (October 26, 2022)
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* deps: update go-memdb for goroutine leak fix [[GH-14983](https://github.com/hashicorp/nomad/issues/14983)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* acl: Fixed a bug where Nomad version checking for one-time tokens was enforced across regions [[GH-14910](https://github.com/hashicorp/nomad/issues/14910)]
|
||||
* deps: Update hashicorp/raft to v1.3.11; fixes unstable leadership on server removal [[GH-15021](https://github.com/hashicorp/nomad/issues/15021)]
|
||||
|
||||
## 1.2.13 (October 04, 2022)
|
||||
|
||||
SECURITY:
|
||||
|
|
|
@ -37,7 +37,7 @@ PROTO_COMPARE_TAG ?= v1.0.3$(if $(findstring ent,$(GO_TAGS)),+ent,)
|
|||
|
||||
# LAST_RELEASE is the git sha of the latest release corresponding to this branch. main should have the latest
|
||||
# published release, and release branches should point to the latest published release in the X.Y release line.
|
||||
LAST_RELEASE ?= v1.4.1
|
||||
LAST_RELEASE ?= v1.4.2
|
||||
|
||||
default: help
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -59,9 +59,13 @@ func (e *Event) stream(conn io.ReadWriteCloser) {
|
|||
// start subscription to publisher
|
||||
var subscription *stream.Subscription
|
||||
var subErr error
|
||||
|
||||
// Track whether the ACL token being used has an expiry time.
|
||||
var expiryTime *time.Time
|
||||
|
||||
// Check required ACL permissions for requested Topics
|
||||
if e.srv.config.ACLEnabled {
|
||||
subscription, subErr = publisher.SubscribeWithACLCheck(subReq)
|
||||
subscription, expiryTime, subErr = publisher.SubscribeWithACLCheck(subReq)
|
||||
} else {
|
||||
subscription, subErr = publisher.Subscribe(subReq)
|
||||
}
|
||||
|
@ -93,6 +97,16 @@ func (e *Event) stream(conn io.ReadWriteCloser) {
|
|||
return
|
||||
}
|
||||
|
||||
// Ensure the token being used is not expired before we any events
|
||||
// to subscribers.
|
||||
if expiryTime != nil && expiryTime.Before(time.Now().UTC()) {
|
||||
select {
|
||||
case errCh <- structs.ErrTokenExpired:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Continue if there are no events
|
||||
if len(events.Events) == 0 {
|
||||
continue
|
||||
|
|
|
@ -15,11 +15,13 @@ import (
|
|||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||
"github.com/hashicorp/nomad/acl"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/stream"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -625,3 +627,117 @@ OUTER:
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEventStream_ACLTokenExpiry ensure a subscription does not receive events
|
||||
// and is closed once the token has expired.
|
||||
func TestEventStream_ACLTokenExpiry(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start our test server and wait until we have a leader.
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Create and upsert and ACL token which has a short expiry set.
|
||||
aclTokenWithExpiry := mock.ACLManagementToken()
|
||||
aclTokenWithExpiry.ExpirationTime = pointer.Of(time.Now().Add(2 * time.Second))
|
||||
|
||||
must.NoError(t, testServer.fsm.State().UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{aclTokenWithExpiry}))
|
||||
|
||||
req := structs.EventStreamRequest{
|
||||
Topics: map[structs.Topic][]string{"Job": {"*"}},
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: testServer.Region(),
|
||||
Namespace: structs.DefaultNamespace,
|
||||
AuthToken: aclTokenWithExpiry.SecretID,
|
||||
},
|
||||
}
|
||||
|
||||
handler, err := testServer.StreamingRpcHandler("Event.Stream")
|
||||
must.NoError(t, err)
|
||||
|
||||
p1, p2 := net.Pipe()
|
||||
defer p1.Close()
|
||||
defer p2.Close()
|
||||
|
||||
errCh := make(chan error)
|
||||
streamMsg := make(chan *structs.EventStreamWrapper)
|
||||
|
||||
go handler(p2)
|
||||
|
||||
go func() {
|
||||
decoder := codec.NewDecoder(p1, structs.MsgpackHandle)
|
||||
for {
|
||||
var msg structs.EventStreamWrapper
|
||||
if err := decoder.Decode(&msg); err != nil {
|
||||
if err == io.EOF || strings.Contains(err.Error(), "closed") {
|
||||
return
|
||||
}
|
||||
errCh <- fmt.Errorf("error decoding: %w", err)
|
||||
}
|
||||
|
||||
streamMsg <- &msg
|
||||
}
|
||||
}()
|
||||
|
||||
publisher, err := testServer.State().EventBroker()
|
||||
must.NoError(t, err)
|
||||
|
||||
jobEvent := structs.JobEvent{
|
||||
Job: mock.Job(),
|
||||
}
|
||||
|
||||
// send req
|
||||
encoder := codec.NewEncoder(p1, structs.MsgpackHandle)
|
||||
must.Nil(t, encoder.Encode(req))
|
||||
|
||||
// publish some events
|
||||
publisher.Publish(&structs.Events{Index: uint64(1), Events: []structs.Event{{Topic: structs.TopicJob, Payload: jobEvent}}})
|
||||
publisher.Publish(&structs.Events{Index: uint64(2), Events: []structs.Event{{Topic: structs.TopicJob, Payload: jobEvent}}})
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(4*time.Second))
|
||||
defer cancel()
|
||||
|
||||
errChStream := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
errChStream <- ctx.Err()
|
||||
return
|
||||
case err := <-errCh:
|
||||
errChStream <- err
|
||||
return
|
||||
case msg := <-streamMsg:
|
||||
if msg.Error == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
errChStream <- msg.Error
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Generate a timeout for the test and for the expiry. The expiry timeout
|
||||
// is used to trigger an update which will close the subscription as the
|
||||
// event stream only reacts to change in state.
|
||||
testTimeout := time.After(4 * time.Second)
|
||||
expiryTimeout := time.After(time.Until(*aclTokenWithExpiry.ExpirationTime))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-testTimeout:
|
||||
t.Fatal("timeout waiting for event stream to close")
|
||||
case err := <-errCh:
|
||||
t.Fatal(err)
|
||||
case <-expiryTimeout:
|
||||
publisher.Publish(&structs.Events{Index: uint64(1), Events: []structs.Event{{Topic: structs.TopicJob, Payload: jobEvent}}})
|
||||
case err := <-errChStream:
|
||||
// Success
|
||||
must.StrContains(t, err.Error(), "ACL token expired")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,19 +109,25 @@ func (e *EventBroker) Publish(events *structs.Events) {
|
|||
e.publishCh <- events
|
||||
}
|
||||
|
||||
// SubscribeWithACLCheck validates the SubscribeRequest's token and requested Topics
|
||||
// to ensure that the tokens privileges are sufficient enough.
|
||||
func (e *EventBroker) SubscribeWithACLCheck(req *SubscribeRequest) (*Subscription, error) {
|
||||
aclObj, err := aclObjFromSnapshotForTokenSecretID(e.aclDelegate.TokenProvider(), e.aclCache, req.Token)
|
||||
// SubscribeWithACLCheck validates the SubscribeRequest's token and requested
|
||||
// topics to ensure that the tokens privileges are sufficient. It will also
|
||||
// return the token expiry time, if any. It is the callers responsibility to
|
||||
// check this before publishing events to the caller.
|
||||
func (e *EventBroker) SubscribeWithACLCheck(req *SubscribeRequest) (*Subscription, *time.Time, error) {
|
||||
aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(e.aclDelegate.TokenProvider(), e.aclCache, req.Token)
|
||||
if err != nil {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
return nil, nil, structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if allowed := aclAllowsSubscription(aclObj, req); !allowed {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
return nil, nil, structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return e.Subscribe(req)
|
||||
sub, err := e.Subscribe(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sub, expiryTime, nil
|
||||
}
|
||||
|
||||
// Subscribe returns a new Subscription for a given request. A Subscription
|
||||
|
@ -203,13 +209,19 @@ func (e *EventBroker) handleACLUpdates(ctx context.Context) {
|
|||
continue
|
||||
}
|
||||
|
||||
aclObj, err := aclObjFromSnapshotForTokenSecretID(e.aclDelegate.TokenProvider(), e.aclCache, tokenSecretID)
|
||||
aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(e.aclDelegate.TokenProvider(), e.aclCache, tokenSecretID)
|
||||
if err != nil || aclObj == nil {
|
||||
e.logger.Error("failed resolving ACL for secretID, closing subscriptions", "error", err)
|
||||
e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
|
||||
continue
|
||||
}
|
||||
|
||||
if expiryTime != nil && expiryTime.Before(time.Now().UTC()) {
|
||||
e.logger.Info("ACL token is expired, closing subscriptions")
|
||||
e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
|
||||
continue
|
||||
}
|
||||
|
||||
e.subscriptions.closeSubscriptionFunc(tokenSecretID, func(sub *Subscription) bool {
|
||||
return !aclAllowsSubscription(aclObj, sub.req)
|
||||
})
|
||||
|
@ -245,13 +257,19 @@ func (e *EventBroker) checkSubscriptionsAgainstACLChange() {
|
|||
continue
|
||||
}
|
||||
|
||||
aclObj, err := aclObjFromSnapshotForTokenSecretID(aclSnapshot, e.aclCache, tokenSecretID)
|
||||
aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(aclSnapshot, e.aclCache, tokenSecretID)
|
||||
if err != nil || aclObj == nil {
|
||||
e.logger.Debug("failed resolving ACL for secretID, closing subscriptions", "error", err)
|
||||
e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
|
||||
continue
|
||||
}
|
||||
|
||||
if expiryTime != nil && expiryTime.Before(time.Now().UTC()) {
|
||||
e.logger.Info("ACL token is expired, closing subscriptions")
|
||||
e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
|
||||
continue
|
||||
}
|
||||
|
||||
e.subscriptions.closeSubscriptionFunc(tokenSecretID, func(sub *Subscription) bool {
|
||||
return !aclAllowsSubscription(aclObj, sub.req)
|
||||
})
|
||||
|
@ -259,23 +277,24 @@ func (e *EventBroker) checkSubscriptionsAgainstACLChange() {
|
|||
}
|
||||
|
||||
func aclObjFromSnapshotForTokenSecretID(
|
||||
aclSnapshot ACLTokenProvider, aclCache *lru.TwoQueueCache, tokenSecretID string) (*acl.ACL, error) {
|
||||
aclSnapshot ACLTokenProvider, aclCache *lru.TwoQueueCache, tokenSecretID string) (
|
||||
*acl.ACL, *time.Time, error) {
|
||||
|
||||
aclToken, err := aclSnapshot.ACLTokenBySecretID(nil, tokenSecretID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if aclToken == nil {
|
||||
return nil, structs.ErrTokenNotFound
|
||||
return nil, nil, structs.ErrTokenNotFound
|
||||
}
|
||||
if aclToken.IsExpired(time.Now().UTC()) {
|
||||
return nil, structs.ErrTokenExpired
|
||||
return nil, nil, structs.ErrTokenExpired
|
||||
}
|
||||
|
||||
// Check if this is a management token
|
||||
if aclToken.Type == structs.ACLManagementToken {
|
||||
return acl.ManagementACL, nil
|
||||
return acl.ManagementACL, aclToken.ExpirationTime, nil
|
||||
}
|
||||
|
||||
aclPolicies := make([]*structs.ACLPolicy, 0, len(aclToken.Policies)+len(aclToken.Roles))
|
||||
|
@ -283,7 +302,7 @@ func aclObjFromSnapshotForTokenSecretID(
|
|||
for _, policyName := range aclToken.Policies {
|
||||
policy, err := aclSnapshot.ACLPolicyByName(nil, policyName)
|
||||
if err != nil || policy == nil {
|
||||
return nil, errors.New("error finding acl policy")
|
||||
return nil, nil, errors.New("error finding acl policy")
|
||||
}
|
||||
aclPolicies = append(aclPolicies, policy)
|
||||
}
|
||||
|
@ -294,7 +313,7 @@ func aclObjFromSnapshotForTokenSecretID(
|
|||
|
||||
role, err := aclSnapshot.GetACLRoleByID(nil, roleLink.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if role == nil {
|
||||
continue
|
||||
|
@ -303,13 +322,17 @@ func aclObjFromSnapshotForTokenSecretID(
|
|||
for _, policyLink := range role.Policies {
|
||||
policy, err := aclSnapshot.ACLPolicyByName(nil, policyLink.Name)
|
||||
if err != nil || policy == nil {
|
||||
return nil, errors.New("error finding acl policy")
|
||||
return nil, nil, errors.New("error finding acl policy")
|
||||
}
|
||||
aclPolicies = append(aclPolicies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
return structs.CompileACLObject(aclCache, aclPolicies)
|
||||
aclObj, err := structs.CompileACLObject(aclCache, aclPolicies)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return aclObj, aclToken.ExpirationTime, nil
|
||||
}
|
||||
|
||||
type ACLTokenProvider interface {
|
||||
|
|
|
@ -514,13 +514,14 @@ func TestEventBroker_handleACLUpdates_policyUpdated(t *testing.T) {
|
|||
ns = structs.DefaultNamespace
|
||||
}
|
||||
|
||||
sub, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
|
||||
sub, expiryTime, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
|
||||
Topics: map[structs.Topic][]string{
|
||||
tc.event.Topic: {"*"},
|
||||
},
|
||||
Namespace: ns,
|
||||
Token: secretID,
|
||||
})
|
||||
require.Nil(t, expiryTime)
|
||||
|
||||
if tc.initialSubErr {
|
||||
require.Error(t, err)
|
||||
|
@ -811,11 +812,12 @@ func TestEventBroker_handleACLUpdates_roleUpdated(t *testing.T) {
|
|||
ns = tc.event.Namespace
|
||||
}
|
||||
|
||||
sub, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
|
||||
sub, expiryTime, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
|
||||
Topics: map[structs.Topic][]string{tc.event.Topic: {"*"}},
|
||||
Namespace: ns,
|
||||
Token: tokenSecretID,
|
||||
})
|
||||
require.Nil(t, expiryTime)
|
||||
|
||||
if tc.initialSubErr {
|
||||
require.Error(t, err)
|
||||
|
@ -931,12 +933,13 @@ func TestEventBroker_handleACLUpdates_tokenExpiry(t *testing.T) {
|
|||
Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tc.inputToken.SecretID}),
|
||||
}
|
||||
|
||||
sub, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
|
||||
sub, expiryTime, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
|
||||
Topics: map[structs.Topic][]string{structs.TopicAll: {"*"}},
|
||||
Token: tc.inputToken.SecretID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sub)
|
||||
require.NotNil(t, expiryTime)
|
||||
|
||||
// Publish an event and check that there is a new item in the
|
||||
// subscription queue.
|
||||
|
|
|
@ -218,7 +218,7 @@ func (sv *Variables) Read(args *structs.VariablesReadRequest, reply *structs.Var
|
|||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "variables", "read"}, time.Now())
|
||||
|
||||
_, err := sv.handleMixedAuthEndpoint(args.QueryOptions,
|
||||
_, _, err := sv.handleMixedAuthEndpoint(args.QueryOptions,
|
||||
acl.PolicyRead, args.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -269,8 +269,7 @@ func (sv *Variables) List(
|
|||
return sv.listAllVariables(args, reply)
|
||||
}
|
||||
|
||||
aclObj, err := sv.handleMixedAuthEndpoint(args.QueryOptions,
|
||||
acl.PolicyList, args.Prefix)
|
||||
aclObj, claims, err := sv.authenticate(args.QueryOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -299,9 +298,12 @@ func (sv *Variables) List(
|
|||
filters := []paginator.Filter{
|
||||
paginator.GenericFilter{
|
||||
Allow: func(raw interface{}) (bool, error) {
|
||||
sv := raw.(*structs.VariableEncrypted)
|
||||
return strings.HasPrefix(sv.Path, args.Prefix) &&
|
||||
(aclObj == nil || aclObj.AllowVariableOperation(sv.Namespace, sv.Path, acl.PolicyList)), nil
|
||||
v := raw.(*structs.VariableEncrypted)
|
||||
if !strings.HasPrefix(v.Path, args.Prefix) {
|
||||
return false, nil
|
||||
}
|
||||
err := sv.authorize(aclObj, claims, v.Namespace, acl.PolicyList, v.Path)
|
||||
return err == nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -345,43 +347,23 @@ func (sv *Variables) List(
|
|||
|
||||
// listAllVariables is used to list variables held within
|
||||
// state where the caller has used the namespace wildcard identifier.
|
||||
func (s *Variables) listAllVariables(
|
||||
func (sv *Variables) listAllVariables(
|
||||
args *structs.VariablesListRequest,
|
||||
reply *structs.VariablesListResponse) error {
|
||||
|
||||
// Perform token resolution. The request already goes through forwarding
|
||||
// and metrics setup before being called.
|
||||
aclObj, err := s.srv.ResolveToken(args.AuthToken)
|
||||
aclObj, claims, err := sv.authenticate(args.QueryOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// allowFunc checks whether the caller has the read-job capability on the
|
||||
// passed namespace.
|
||||
allowFunc := func(ns string) bool {
|
||||
return aclObj.AllowVariableOperation(ns, "", acl.PolicyList)
|
||||
}
|
||||
|
||||
// Set up and return the blocking query.
|
||||
return s.srv.blockingRPC(&blockingOptions{
|
||||
return sv.srv.blockingRPC(&blockingOptions{
|
||||
queryOpts: &args.QueryOptions,
|
||||
queryMeta: &reply.QueryMeta,
|
||||
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
||||
|
||||
// Identify which namespaces the caller has access to. If they do
|
||||
// not have access to any, send them an empty response. Otherwise,
|
||||
// handle any error in a traditional manner.
|
||||
_, err := allowedNSes(aclObj, stateStore, allowFunc)
|
||||
switch err {
|
||||
case structs.ErrPermissionDenied:
|
||||
reply.Data = make([]*structs.VariableMetadata, 0)
|
||||
return nil
|
||||
case nil:
|
||||
// Fallthrough.
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all the variables stored within state.
|
||||
iter, err := stateStore.Variables(ws)
|
||||
if err != nil {
|
||||
|
@ -396,15 +378,17 @@ func (s *Variables) listAllVariables(
|
|||
paginator.StructsTokenizerOptions{
|
||||
WithNamespace: true,
|
||||
WithID: true,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
filters := []paginator.Filter{
|
||||
paginator.GenericFilter{
|
||||
Allow: func(raw interface{}) (bool, error) {
|
||||
sv := raw.(*structs.VariableEncrypted)
|
||||
return strings.HasPrefix(sv.Path, args.Prefix) &&
|
||||
(aclObj == nil || aclObj.AllowVariableOperation(sv.Namespace, sv.Path, acl.PolicyList)), nil
|
||||
v := raw.(*structs.VariableEncrypted)
|
||||
if !strings.HasPrefix(v.Path, args.Prefix) {
|
||||
return false, nil
|
||||
}
|
||||
err := sv.authorize(aclObj, claims, v.Namespace, acl.PolicyList, v.Path)
|
||||
return err == nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -413,8 +397,8 @@ func (s *Variables) listAllVariables(
|
|||
// responsible for appending a variable to the stubs array.
|
||||
paginatorImpl, err := paginator.NewPaginator(iter, tokenizer, filters, args.QueryOptions,
|
||||
func(raw interface{}) error {
|
||||
sv := raw.(*structs.VariableEncrypted)
|
||||
svStub := sv.VariableMetadata
|
||||
v := raw.(*structs.VariableEncrypted)
|
||||
svStub := v.VariableMetadata
|
||||
svs = append(svs, &svStub)
|
||||
return nil
|
||||
})
|
||||
|
@ -437,7 +421,7 @@ func (s *Variables) listAllVariables(
|
|||
|
||||
// Use the index table to populate the query meta as we have no way
|
||||
// of tracking the max index on deletes.
|
||||
return s.srv.setReplyQueryMeta(stateStore, state.TableVariables, &reply.QueryMeta)
|
||||
return sv.srv.setReplyQueryMeta(stateStore, state.TableVariables, &reply.QueryMeta)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -475,24 +459,31 @@ func (sv *Variables) decrypt(v *structs.VariableEncrypted) (*structs.VariableDec
|
|||
|
||||
// handleMixedAuthEndpoint is a helper to handle auth on RPC endpoints that can
|
||||
// either be called by external clients or by workload identity
|
||||
func (sv *Variables) handleMixedAuthEndpoint(args structs.QueryOptions, cap, pathOrPrefix string) (*acl.ACL, error) {
|
||||
func (sv *Variables) handleMixedAuthEndpoint(args structs.QueryOptions, cap, pathOrPrefix string) (*acl.ACL, *structs.IdentityClaims, error) {
|
||||
|
||||
aclObj, claims, err := sv.authenticate(args)
|
||||
if err != nil {
|
||||
return aclObj, claims, err
|
||||
}
|
||||
err = sv.authorize(aclObj, claims, args.RequestNamespace(), cap, pathOrPrefix)
|
||||
if err != nil {
|
||||
return aclObj, claims, err
|
||||
}
|
||||
|
||||
return aclObj, claims, nil
|
||||
}
|
||||
|
||||
func (sv *Variables) authenticate(args structs.QueryOptions) (*acl.ACL, *structs.IdentityClaims, error) {
|
||||
|
||||
// Perform the initial token resolution.
|
||||
aclObj, err := sv.srv.ResolveToken(args.AuthToken)
|
||||
if err == nil {
|
||||
// Perform our ACL validation. If the object is nil, this means ACLs
|
||||
// are not enabled, otherwise trigger the allowed namespace function.
|
||||
if aclObj != nil {
|
||||
if !aclObj.AllowVariableOperation(args.RequestNamespace(), pathOrPrefix, cap) {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
return aclObj, nil
|
||||
return aclObj, nil, nil
|
||||
}
|
||||
if helper.IsUUID(args.AuthToken) {
|
||||
// early return for ErrNotFound or other errors if it's formed
|
||||
// like an ACLToken.SecretID
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Attempt to verify the token as a JWT with a workload
|
||||
|
@ -502,27 +493,46 @@ func (sv *Variables) handleMixedAuthEndpoint(args structs.QueryOptions, cap, pat
|
|||
metrics.IncrCounter([]string{
|
||||
"nomad", "variables", "invalid_allocation_identity"}, 1)
|
||||
sv.logger.Trace("allocation identity was not valid", "error", err)
|
||||
return nil, structs.ErrPermissionDenied
|
||||
return nil, nil, structs.ErrPermissionDenied
|
||||
}
|
||||
return nil, claims, nil
|
||||
}
|
||||
|
||||
func (sv *Variables) authorize(aclObj *acl.ACL, claims *structs.IdentityClaims, ns, cap, pathOrPrefix string) error {
|
||||
|
||||
if aclObj == nil && claims == nil {
|
||||
return nil // ACLs aren't enabled
|
||||
}
|
||||
|
||||
// The workload identity gets access to paths that match its
|
||||
// identity, without having to go thru the ACL system
|
||||
err = sv.authValidatePrefix(claims, args.RequestNamespace(), pathOrPrefix)
|
||||
if err == nil {
|
||||
return aclObj, nil
|
||||
// Perform normal ACL validation. If the ACL object is nil, that means we're
|
||||
// working with an identity claim.
|
||||
if aclObj != nil {
|
||||
if !aclObj.AllowVariableOperation(ns, pathOrPrefix, cap) {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the workload identity doesn't match the implicit permissions
|
||||
// given to paths, check for its attached ACL policies
|
||||
aclObj, err = sv.srv.ResolveClaims(claims)
|
||||
if err != nil {
|
||||
return nil, err // this only returns an error when the state store has gone wrong
|
||||
if claims != nil {
|
||||
// The workload identity gets access to paths that match its
|
||||
// identity, without having to go thru the ACL system
|
||||
err := sv.authValidatePrefix(claims, ns, pathOrPrefix)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the workload identity doesn't match the implicit permissions
|
||||
// given to paths, check for its attached ACL policies
|
||||
aclObj, err = sv.srv.ResolveClaims(claims)
|
||||
if err != nil {
|
||||
return err // this only returns an error when the state store has gone wrong
|
||||
}
|
||||
if aclObj != nil && aclObj.AllowVariableOperation(
|
||||
ns, pathOrPrefix, cap) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if aclObj != nil && aclObj.AllowVariableOperation(
|
||||
args.RequestNamespace(), pathOrPrefix, cap) {
|
||||
return aclObj, nil
|
||||
}
|
||||
return nil, structs.ErrPermissionDenied
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// authValidatePrefix asserts that the requested path is valid for
|
||||
|
|
|
@ -50,10 +50,15 @@ func TestVariablesEndpoint_auth(t *testing.T) {
|
|||
alloc3.Namespace = ns
|
||||
alloc3.Job.ParentID = jobID
|
||||
|
||||
alloc4 := mock.Alloc()
|
||||
alloc4.ClientStatus = structs.AllocClientStatusRunning
|
||||
alloc4.Job.Namespace = ns
|
||||
alloc4.Namespace = ns
|
||||
|
||||
store := srv.fsm.State()
|
||||
must.NoError(t, store.UpsertNamespaces(1000, []*structs.Namespace{{Name: ns}}))
|
||||
must.NoError(t, store.UpsertAllocs(
|
||||
structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc1, alloc2, alloc3}))
|
||||
structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc1, alloc2, alloc3, alloc4}))
|
||||
|
||||
claims1 := alloc1.ToTaskIdentityClaims(nil, "web")
|
||||
idToken, err := srv.encrypter.SignClaims(claims1)
|
||||
|
@ -77,6 +82,10 @@ func TestVariablesEndpoint_auth(t *testing.T) {
|
|||
idTokenParts[2] = strings.Join(sig, "")
|
||||
invalidIDToken := strings.Join(idTokenParts, ".")
|
||||
|
||||
claims4 := alloc4.ToTaskIdentityClaims(alloc4.Job, "web")
|
||||
wiOnlyToken, err := srv.encrypter.SignClaims(claims4)
|
||||
must.NoError(t, err)
|
||||
|
||||
policy := mock.ACLPolicy()
|
||||
policy.Rules = `namespace "nondefault-namespace" {
|
||||
variables {
|
||||
|
@ -98,8 +107,8 @@ func TestVariablesEndpoint_auth(t *testing.T) {
|
|||
must.NoError(t, err)
|
||||
|
||||
t.Run("terminal alloc should be denied", func(t *testing.T) {
|
||||
_, err = srv.staticEndpoints.Variables.handleMixedAuthEndpoint(
|
||||
structs.QueryOptions{AuthToken: idToken, Namespace: ns}, "n/a",
|
||||
_, _, err = srv.staticEndpoints.Variables.handleMixedAuthEndpoint(
|
||||
structs.QueryOptions{AuthToken: idToken, Namespace: ns}, acl.PolicyList,
|
||||
fmt.Sprintf("nomad/jobs/%s/web/web", jobID))
|
||||
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
||||
})
|
||||
|
@ -110,8 +119,8 @@ func TestVariablesEndpoint_auth(t *testing.T) {
|
|||
structs.MsgTypeTestSetup, 1200, []*structs.Allocation{alloc1}))
|
||||
|
||||
t.Run("wrong namespace should be denied", func(t *testing.T) {
|
||||
_, err = srv.staticEndpoints.Variables.handleMixedAuthEndpoint(
|
||||
structs.QueryOptions{AuthToken: idToken, Namespace: structs.DefaultNamespace}, "n/a",
|
||||
_, _, err = srv.staticEndpoints.Variables.handleMixedAuthEndpoint(
|
||||
structs.QueryOptions{AuthToken: idToken, Namespace: structs.DefaultNamespace}, acl.PolicyList,
|
||||
fmt.Sprintf("nomad/jobs/%s/web/web", jobID))
|
||||
must.EqError(t, err, structs.ErrPermissionDenied.Error())
|
||||
})
|
||||
|
@ -126,35 +135,35 @@ func TestVariablesEndpoint_auth(t *testing.T) {
|
|||
{
|
||||
name: "valid claim for path with task secret",
|
||||
token: idToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid claim for path with group secret",
|
||||
token: idToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web", jobID),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid claim for path with job secret",
|
||||
token: idToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s", jobID),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid claim for path with dispatch job secret",
|
||||
token: idDispatchToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s", jobID),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid claim for path with namespace secret",
|
||||
token: idToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyRead,
|
||||
path: "nomad/jobs",
|
||||
expectedErr: nil,
|
||||
},
|
||||
|
@ -189,14 +198,14 @@ func TestVariablesEndpoint_auth(t *testing.T) {
|
|||
{
|
||||
name: "valid claim with no permissions denied by path",
|
||||
token: noPermissionsToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/w", jobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "valid claim with no permissions allowed by namespace",
|
||||
token: noPermissionsToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyList,
|
||||
path: "nomad/jobs",
|
||||
expectedErr: nil,
|
||||
},
|
||||
|
@ -207,37 +216,23 @@ func TestVariablesEndpoint_auth(t *testing.T) {
|
|||
path: fmt.Sprintf("nomad/jobs/%s/w", jobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "extra trailing slash is denied",
|
||||
token: idToken,
|
||||
cap: "n/a",
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/", jobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "invalid prefix is denied",
|
||||
token: idToken,
|
||||
cap: "n/a",
|
||||
path: fmt.Sprintf("nomad/jobs/%s/w", jobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "missing auth token is denied",
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "invalid signature is denied",
|
||||
token: invalidIDToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "invalid claim for dispatched ID",
|
||||
token: idDispatchToken,
|
||||
cap: "n/a",
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s", alloc3.JobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
|
@ -255,12 +250,106 @@ func TestVariablesEndpoint_auth(t *testing.T) {
|
|||
path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
|
||||
{
|
||||
name: "WI token can read own task",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/web", alloc4.JobID),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "WI token can list own task",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/web", alloc4.JobID),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "WI token can read own group",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web", alloc4.JobID),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "WI token can list own group",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web", alloc4.JobID),
|
||||
expectedErr: nil,
|
||||
},
|
||||
|
||||
{
|
||||
name: "WI token cannot read another task in group",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/other", alloc4.JobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "WI token cannot list another task in group",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/other", alloc4.JobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "WI token cannot read another task in group",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/other", alloc4.JobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "WI token cannot list a task in another group",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/other/web", alloc4.JobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "WI token cannot read a task in another group",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyRead,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/other/web", alloc4.JobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "WI token cannot read a group in another job",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyRead,
|
||||
path: "nomad/jobs/other/web/web",
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "WI token cannot list a group in another job",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyList,
|
||||
path: "nomad/jobs/other/web/web",
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
|
||||
{
|
||||
name: "WI token extra trailing slash is denied",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/web/", alloc4.JobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
{
|
||||
name: "WI token invalid prefix is denied",
|
||||
token: wiOnlyToken,
|
||||
cap: acl.PolicyList,
|
||||
path: fmt.Sprintf("nomad/jobs/%s/w", alloc4.JobID),
|
||||
expectedErr: structs.ErrPermissionDenied,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := srv.staticEndpoints.Variables.handleMixedAuthEndpoint(
|
||||
_, _, err := srv.staticEndpoints.Variables.handleMixedAuthEndpoint(
|
||||
structs.QueryOptions{AuthToken: tc.token, Namespace: ns}, tc.cap, tc.path)
|
||||
if tc.expectedErr == nil {
|
||||
must.NoError(t, err)
|
||||
|
@ -453,6 +542,80 @@ func TestVariablesEndpoint_Apply_ACL(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestVariablesEndpoint_ListFiltering(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
srv, _, shutdown := TestACLServer(t, func(c *Config) {
|
||||
c.NumSchedulers = 0 // Prevent automatic dequeue
|
||||
})
|
||||
defer shutdown()
|
||||
testutil.WaitForLeader(t, srv.RPC)
|
||||
codec := rpcClient(t, srv)
|
||||
|
||||
ns := "nondefault-namespace"
|
||||
idx := uint64(1000)
|
||||
|
||||
alloc := mock.Alloc()
|
||||
alloc.Job.ID = "job1"
|
||||
alloc.JobID = "job1"
|
||||
alloc.TaskGroup = "group"
|
||||
alloc.Job.TaskGroups[0].Name = "group"
|
||||
alloc.ClientStatus = structs.AllocClientStatusRunning
|
||||
alloc.Job.Namespace = ns
|
||||
alloc.Namespace = ns
|
||||
|
||||
store := srv.fsm.State()
|
||||
must.NoError(t, store.UpsertNamespaces(idx, []*structs.Namespace{{Name: ns}}))
|
||||
idx++
|
||||
must.NoError(t, store.UpsertAllocs(
|
||||
structs.MsgTypeTestSetup, idx, []*structs.Allocation{alloc}))
|
||||
|
||||
claims := alloc.ToTaskIdentityClaims(alloc.Job, "web")
|
||||
token, err := srv.encrypter.SignClaims(claims)
|
||||
must.NoError(t, err)
|
||||
|
||||
writeVar := func(ns, path string) {
|
||||
idx++
|
||||
sv := mock.VariableEncrypted()
|
||||
sv.Namespace = ns
|
||||
sv.Path = path
|
||||
resp := store.VarSet(idx, &structs.VarApplyStateRequest{
|
||||
Op: structs.VarOpSet,
|
||||
Var: sv,
|
||||
})
|
||||
must.NoError(t, resp.Error)
|
||||
}
|
||||
|
||||
writeVar(ns, "nomad/jobs/job1/group/web")
|
||||
writeVar(ns, "nomad/jobs/job1/group")
|
||||
writeVar(ns, "nomad/jobs/job1")
|
||||
|
||||
writeVar(ns, "nomad/jobs/job1/group/other")
|
||||
writeVar(ns, "nomad/jobs/job1/other/web")
|
||||
writeVar(ns, "nomad/jobs/job2/group/web")
|
||||
|
||||
req := &structs.VariablesListRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Namespace: ns,
|
||||
Prefix: "nomad",
|
||||
AuthToken: token,
|
||||
Region: "global",
|
||||
},
|
||||
}
|
||||
var resp structs.VariablesListResponse
|
||||
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Variables.List", req, &resp))
|
||||
found := []string{}
|
||||
for _, variable := range resp.Data {
|
||||
found = append(found, variable.Path)
|
||||
}
|
||||
expect := []string{
|
||||
"nomad/jobs/job1",
|
||||
"nomad/jobs/job1/group",
|
||||
"nomad/jobs/job1/group/web",
|
||||
}
|
||||
must.Eq(t, expect, found)
|
||||
|
||||
}
|
||||
|
||||
func TestVariablesEndpoint_ComplexACLPolicies(t *testing.T) {
|
||||
|
||||
ci.Parallel(t)
|
||||
|
@ -560,11 +723,12 @@ namespace "*" {}
|
|||
testListPrefix("prod", "project", 1, nil)
|
||||
testListPrefix("prod", "", 4, nil)
|
||||
|
||||
testListPrefix("other", "system", 0, structs.ErrPermissionDenied)
|
||||
testListPrefix("other", "config/system", 0, structs.ErrPermissionDenied)
|
||||
testListPrefix("other", "config", 0, structs.ErrPermissionDenied)
|
||||
testListPrefix("other", "project", 0, structs.ErrPermissionDenied)
|
||||
testListPrefix("other", "", 0, structs.ErrPermissionDenied)
|
||||
// list gives empty but no error!
|
||||
testListPrefix("other", "system", 0, nil)
|
||||
testListPrefix("other", "config/system", 0, nil)
|
||||
testListPrefix("other", "config", 0, nil)
|
||||
testListPrefix("other", "project", 0, nil)
|
||||
testListPrefix("other", "", 0, nil)
|
||||
|
||||
testListPrefix("*", "system", 1, nil)
|
||||
testListPrefix("*", "config/system", 1, nil)
|
||||
|
|
|
@ -11,7 +11,7 @@ var (
|
|||
GitDescribe string
|
||||
|
||||
// The main version number that is being run at the moment.
|
||||
Version = "1.4.2"
|
||||
Version = "1.4.3"
|
||||
|
||||
// A pre-release marker for the version. If this is "" (empty string)
|
||||
// then it means that it is a final release. Otherwise, this is a pre-release
|
||||
|
|
Loading…
Reference in New Issue