fixing merge conflicts part 3

This commit is contained in:
trujillo-adam 2022-03-15 15:25:03 -07:00
commit 667976c94f
3151 changed files with 39933 additions and 23904 deletions

3
.changelog/12298.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
connect: reduce raft apply on CA configuration when no change is performed
```

3
.changelog/12307.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
server: partly fix config entry replication issue that prevents replication in some circumstances
```

3
.changelog/12362.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
server: suppress spurious blocking query returns where multiple config entries are involved
```

3
.changelog/12400.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Added Tags tab to gateways(just like exists for non-gateway services)
```

3
.changelog/12442.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:enhancement
agent: Allow client agents to perform keyring operations
```

3
.changelog/12443.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
connect: update Envoy supported version of 1.20 to 1.20.2
```

7
.changelog/12444.txt Normal file
View File

@ -0,0 +1,7 @@
```release-note:bug
catalog: compare node names case insensitively in more places
```
```release-note:bug
state: fix bug blocking snapshot restore when a node check and node differed in casing of the node string
```

3
.changelog/12449.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
connect: Update supported Envoy versions to include 1.19.3 and 1.18.6
```

3
.changelog/12461.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Move icons away from depending on a CSS preprocessor
```

3
.changelog/12470.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:enhancement
acl: Provide fuller detail in the error messsage when an ACL denies access.
```

3
.changelog/12478.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: In the datacenter selector order Datacenters by Primary, Local then alpanumerically
```

3
.changelog/12479.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Fixes a visual bug where our loading icon can look cut off
```

3
.changelog/12496.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
raft: upgrade to v1.3.6 which fixes a bug where a read replica node could attempt bootstrapping raft and prevent other nodes from bootstrapping at all
```

3
.changelog/12512.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
server: fix spurious blocking query suppression for discovery chains
```

3
.changelog/12529.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
server: Ensure that service-defaults `Meta` is returned with the response to the `ConfigEntry.ResolveServiceConfig` RPC.
```

3
.changelog/_1563.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
server: **(Enterprise only)** Namespace deletion will now attempt to delete as many namespaced config entries as possible instead of halting on the first deletion that failed.
```

3
.changelog/_1595.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
acl: **(Enterprise Only)** fixes a bug preventing ACL policies configured with datacenter restrictions from being created if the cluster had been upgraded to Consul 1.11+ from an earlier version.
```

View File

@ -22,7 +22,7 @@ references:
test-results: &TEST_RESULTS_DIR /tmp/test-results
cache:
yarn: &YARN_CACHE_KEY consul-ui-v6-{{ checksum "ui/yarn.lock" }}
yarn: &YARN_CACHE_KEY consul-ui-v7-{{ checksum "ui/yarn.lock" }}
environment: &ENVIRONMENT
TEST_RESULTS_DIR: *TEST_RESULTS_DIR
@ -889,20 +889,20 @@ jobs:
path: *TEST_RESULTS_DIR
- run: *notify-slack-failure
envoy-integration-test-1_18_4:
envoy-integration-test-1_18_6:
<<: *ENVOY_TESTS
environment:
ENVOY_VERSION: "1.18.4"
ENVOY_VERSION: "1.18.6"
envoy-integration-test-1_19_1:
envoy-integration-test-1_19_3:
<<: *ENVOY_TESTS
environment:
ENVOY_VERSION: "1.19.1"
ENVOY_VERSION: "1.19.3"
envoy-integration-test-1_20_1:
envoy-integration-test-1_20_2:
<<: *ENVOY_TESTS
environment:
ENVOY_VERSION: "1.20.1"
ENVOY_VERSION: "1.20.2"
# run integration tests for the connect ca providers
test-connect-ca-providers:
@ -1148,13 +1148,13 @@ workflows:
- envoy-integration-test-1_17_4:
requires:
- dev-build
- envoy-integration-test-1_18_4:
- envoy-integration-test-1_18_6:
requires:
- dev-build
- envoy-integration-test-1_19_1:
- envoy-integration-test-1_19_3:
requires:
- dev-build
- envoy-integration-test-1_20_1:
- envoy-integration-test-1_20_2:
requires:
- dev-build

View File

@ -242,10 +242,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Docker Build (Action)
uses: hashicorp/actions-docker-build@v1
with:

View File

@ -150,3 +150,81 @@ event "verify" {
on = "fail"
}
}
## These are promotion and post-publish events
## they should be added to the end of the file after the verify event stanza.
event "trigger-staging" {
// This event is dispatched by the bob trigger-promotion command
// and is required - do not delete.
}
event "promote-staging" {
depends = ["trigger-staging"]
action "promote-staging" {
organization = "hashicorp"
repository = "crt-workflows-common"
workflow = "promote-staging"
}
notification {
on = "always"
}
}
event "promote-staging-docker" {
depends = ["promote-staging"]
action "promote-staging-docker" {
organization = "hashicorp"
repository = "crt-workflows-common"
workflow = "promote-staging-docker"
}
notification {
on = "always"
}
}
event "trigger-production" {
// This event is dispatched by the bob trigger-promotion command
// and is required - do not delete.
}
event "promote-production" {
depends = ["trigger-production"]
action "promote-production" {
organization = "hashicorp"
repository = "crt-workflows-common"
workflow = "promote-production"
}
notification {
on = "always"
}
}
event "promote-production-docker" {
depends = ["promote-production"]
action "promote-production-docker" {
organization = "hashicorp"
repository = "crt-workflows-common"
workflow = "promote-production-docker"
}
notification {
on = "always"
}
}
event "promote-production-packaging" {
depends = ["promote-production-docker"]
action "promote-production-packaging" {
organization = "hashicorp"
repository = "crt-workflows-common"
workflow = "promote-production-packaging"
}
notification {
on = "always"
}
}

View File

@ -1,3 +1,36 @@
## 1.11.4 (February 28, 2022)
FEATURES:
* ca: support using an external root CA with the vault CA provider [[GH-11910](https://github.com/hashicorp/consul/issues/11910)]
IMPROVEMENTS:
* connect: Update supported Envoy versions to include 1.19.3 and 1.18.6 [[GH-12449](https://github.com/hashicorp/consul/issues/12449)]
* connect: update Envoy supported version of 1.20 to 1.20.2 [[GH-12433](https://github.com/hashicorp/consul/issues/12433)]
* connect: update Envoy supported version of 1.20 to 1.20.2 [[GH-12443](https://github.com/hashicorp/consul/issues/12443)]
* debug: reduce the capture time for trace to only a single interval instead of the full duration to make trace.out easier to open without running into OOM errors. [[GH-12359](https://github.com/hashicorp/consul/issues/12359)]
* raft: add additional logging of snapshot restore progress [[GH-12325](https://github.com/hashicorp/consul/issues/12325)]
* rpc: improve blocking queries for items that do not exist, by continuing to block until they exist (or the timeout). [[GH-12110](https://github.com/hashicorp/consul/issues/12110)]
* sentinel: **(Enterprise Only)** Sentinel now uses SHA256 to generate policy ids
* server: conditionally avoid writing a config entry to raft if it was already the same [[GH-12321](https://github.com/hashicorp/consul/issues/12321)]
* server: suppress spurious blocking query returns where multiple config entries are involved [[GH-12362](https://github.com/hashicorp/consul/issues/12362)]
BUG FIXES:
* agent: Parse datacenter from Create/Delete requests for AuthMethods and BindingRules. [[GH-12370](https://github.com/hashicorp/consul/issues/12370)]
* areas: **(Enterprise Only)** Fixes a bug when using Yamux pool ( for servers version 1.7.3 and later), the entire pool was locked while connecting to a remote location, which could potentially take a long time. [[GH-1368](https://github.com/hashicorp/consul/issues/1368)]
* catalog: compare node names case insensitively in more places [[GH-12444](https://github.com/hashicorp/consul/issues/12444)]
* checks: populate interval and timeout when registering services [[GH-11138](https://github.com/hashicorp/consul/issues/11138)]
* local: fixes a data race in anti-entropy sync that could cause the wrong tags to be applied to a service when EnableTagOverride is used [[GH-12324](https://github.com/hashicorp/consul/issues/12324)]
* raft: fixed a race condition in leadership transfer that could result in reelection of the current leader [[GH-12325](https://github.com/hashicorp/consul/issues/12325)]
* server: **(Enterprise only)** Namespace deletion will now attempt to delete as many namespaced config entries as possible instead of halting on the first deletion that failed.
* server: partly fix config entry replication issue that prevents replication in some circumstances [[GH-12307](https://github.com/hashicorp/consul/issues/12307)]
* state: fix bug blocking snapshot restore when a node check and node differed in casing of the node string [[GH-12444](https://github.com/hashicorp/consul/issues/12444)]
* ui: Ensure we always display the Policy default preview in the Namespace editing form [[GH-12316](https://github.com/hashicorp/consul/issues/12316)]
* ui: Fix missing helper javascript error [[GH-12358](https://github.com/hashicorp/consul/issues/12358)]
* xds: Fixed Envoy http features such as outlier detection and retry policy not working correctly with transparent proxy. [[GH-12385](https://github.com/hashicorp/consul/issues/12385)]
## 1.11.3 (February 11, 2022)
IMPROVEMENTS:
@ -206,6 +239,37 @@ NOTES:
* Renamed the `agent_master` field to `agent_recovery` in the `acl-tokens.json` file in which tokens are persisted on-disk (when `acl.enable_token_persistence` is enabled) [[GH-11744](https://github.com/hashicorp/consul/issues/11744)]
## 1.10.9 (February 28, 2022)
SECURITY:
* agent: Use SHA256 instead of MD5 to generate persistence file names.
FEATURES:
* ca: support using an external root CA with the vault CA provider [[GH-11910](https://github.com/hashicorp/consul/issues/11910)]
IMPROVEMENTS:
* connect: Update supported Envoy versions to include 1.18.6 [[GH-12450](https://github.com/hashicorp/consul/issues/12450)]
* connect: update Envoy supported version of 1.20 to 1.20.2 [[GH-12434](https://github.com/hashicorp/consul/issues/12434)]
* debug: reduce the capture time for trace to only a single interval instead of the full duration to make trace.out easier to open without running into OOM errors. [[GH-12359](https://github.com/hashicorp/consul/issues/12359)]
* raft: add additional logging of snapshot restore progress [[GH-12325](https://github.com/hashicorp/consul/issues/12325)]
* rpc: improve blocking queries for items that do not exist, by continuing to block until they exist (or the timeout). [[GH-12110](https://github.com/hashicorp/consul/issues/12110)]
* sentinel: **(Enterprise Only)** Sentinel now uses SHA256 to generate policy ids
* server: conditionally avoid writing a config entry to raft if it was already the same [[GH-12321](https://github.com/hashicorp/consul/issues/12321)]
* server: suppress spurious blocking query returns where multiple config entries are involved [[GH-12362](https://github.com/hashicorp/consul/issues/12362)]
BUG FIXES:
* agent: Parse datacenter from Create/Delete requests for AuthMethods and BindingRules. [[GH-12370](https://github.com/hashicorp/consul/issues/12370)]
* areas: **(Enterprise Only)** Fixes a bug when using Yamux pool ( for servers version 1.7.3 and later), the entire pool was locked while connecting to a remote location, which could potentially take a long time. [[GH-1368](https://github.com/hashicorp/consul/issues/1368)]
* raft: fixed a race condition in leadership transfer that could result in reelection of the current leader [[GH-12325](https://github.com/hashicorp/consul/issues/12325)]
* server: **(Enterprise only)** Namespace deletion will now attempt to delete as many namespaced config entries as possible instead of halting on the first deletion that failed.
* server: partly fix config entry replication issue that prevents replication in some circumstances [[GH-12307](https://github.com/hashicorp/consul/issues/12307)]
* ui: Ensure we always display the Policy default preview in the Namespace editing form [[GH-12316](https://github.com/hashicorp/consul/issues/12316)]
* xds: Fixed Envoy http features such as outlier detection and retry policy not working correctly with transparent proxy. [[GH-12385](https://github.com/hashicorp/consul/issues/12385)]
## 1.10.8 (February 11, 2022)
SECURITY:
@ -554,6 +618,25 @@ NOTES:
* legal: **(Enterprise only)** Enterprise binary downloads will now include a copy of the EULA and Terms of Evaluation in the zip archive
## 1.9.16 (February 28, 2022)
FEATURES:
* ca: support using an external root CA with the vault CA provider [[GH-11910](https://github.com/hashicorp/consul/issues/11910)]
IMPROVEMENTS:
* sentinel: **(Enterprise Only)** Sentinel now uses SHA256 to generate policy ids
* server: conditionally avoid writing a config entry to raft if it was already the same [[GH-12321](https://github.com/hashicorp/consul/issues/12321)]
BUG FIXES:
* agent: Parse datacenter from Create/Delete requests for AuthMethods and BindingRules. [[GH-12370](https://github.com/hashicorp/consul/issues/12370)]
* areas: **(Enterprise Only)** Fixes a bug when using Yamux pool ( for servers version 1.7.3 and later), the entire pool was locked while connecting to a remote location, which could potentially take a long time. [[GH-1368](https://github.com/hashicorp/consul/issues/1368)]
* server: **(Enterprise only)** Namespace deletion will now attempt to delete as many namespaced config entries as possible instead of halting on the first deletion that failed.
* server: partly fix config entry replication issue that prevents replication in some circumstances [[GH-12307](https://github.com/hashicorp/consul/issues/12307)]
* ui: Ensure we always display the Policy default preview in the Namespace editing form [[GH-12316](https://github.com/hashicorp/consul/issues/12316)]
## 1.9.15 (February 11, 2022)
IMPROVEMENTS:

View File

@ -178,8 +178,8 @@ endif
# linux builds a linux binary independent of the source platform
linux:
mkdir -p bin
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin -ldflags "$(GOLDFLAGS)" -tags "$(GOTAGS)"
@mkdir -p ./pkg/bin/linux_amd64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./pkg/bin/linux_amd64 -ldflags "$(GOLDFLAGS)" -tags "$(GOTAGS)"
# dist builds binaries for all platforms and packages them for distribution
dist:
@ -330,15 +330,15 @@ ifeq ("$(CIRCLECI)","true")
# Run in CI
gotestsum --format=short-verbose --junitfile "$(TEST_RESULTS_DIR)/gotestsum-report.xml" -- -cover -coverprofile=coverage.txt ./agent/connect/ca
# Run leader tests that require Vault
gotestsum --format=short-verbose --junitfile "$(TEST_RESULTS_DIR)/gotestsum-report-leader.xml" -- -cover -coverprofile=coverage-leader.txt -run '.*_Vault_' ./agent/consul
gotestsum --format=short-verbose --junitfile "$(TEST_RESULTS_DIR)/gotestsum-report-leader.xml" -- -cover -coverprofile=coverage-leader.txt -run Vault ./agent/consul
# Run agent tests that require Vault
gotestsum --format=short-verbose --junitfile "$(TEST_RESULTS_DIR)/gotestsum-report-agent.xml" -- -cover -coverprofile=coverage-agent.txt -run '.*_Vault_' ./agent
gotestsum --format=short-verbose --junitfile "$(TEST_RESULTS_DIR)/gotestsum-report-agent.xml" -- -cover -coverprofile=coverage-agent.txt -run Vault ./agent
else
# Run locally
@echo "Running /agent/connect/ca tests in verbose mode"
@go test -v ./agent/connect/ca
@go test -v ./agent/consul -run '.*_Vault_'
@go test -v ./agent -run '.*_Vault_'
@go test -v ./agent/consul -run Vault
@go test -v ./agent -run Vault
endif
proto: $(PROTOGOFILES) $(PROTOGOBINFILES)

View File

@ -161,6 +161,279 @@ type Authorizer interface {
// Embedded Interface for Consul Enterprise specific ACL enforcement
enterpriseAuthorizer
// ToAllowAuthorizer is needed until we can use ResolveResult in all the places this interface is used.
ToAllowAuthorizer() AllowAuthorizer
}
// AllowAuthorizer is a wrapper to expose the *Allowed methods.
// This and the ToAllowAuthorizer function exist to tide us over until the ResolveResult struct
// is moved into acl.
type AllowAuthorizer struct {
Authorizer
}
// ACLReadAllowed checks for permission to list all the ACLs
func (a AllowAuthorizer) ACLReadAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.ACLRead(ctx) != Allow {
return PermissionDeniedByACLUnnamed(a, ctx, ResourceACL, AccessRead)
}
return nil
}
// ACLWriteAllowed checks for permission to manipulate ACLs
func (a AllowAuthorizer) ACLWriteAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.ACLWrite(ctx) != Allow {
return PermissionDeniedByACLUnnamed(a, ctx, ResourceACL, AccessWrite)
}
return nil
}
// AgentReadAllowed checks for permission to read from agent endpoints for a
// given node.
func (a AllowAuthorizer) AgentReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.AgentRead(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceAgent, AccessRead, name)
}
return nil
}
// AgentWriteAllowed checks for permission to make changes via agent endpoints
// for a given node.
func (a AllowAuthorizer) AgentWriteAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.AgentWrite(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceAgent, AccessWrite, name)
}
return nil
}
// EventReadAllowed determines if a specific event can be queried.
func (a AllowAuthorizer) EventReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.EventRead(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceEvent, AccessRead, name)
}
return nil
}
// EventWriteAllowed determines if a specific event may be fired.
func (a AllowAuthorizer) EventWriteAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.EventWrite(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceEvent, AccessWrite, name)
}
return nil
}
// IntentionDefaultAllowAllowed determines the default authorized behavior
// when no intentions match a Connect request.
func (a AllowAuthorizer) IntentionDefaultAllowAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.IntentionDefaultAllow(ctx) != Allow {
// This is a bit nuanced, in that this isn't set by a rule, but inherited globally
// TODO(acl-error-enhancements) revisit when we have full accessor info
return PermissionDeniedError{Cause: "Denied by intention default"}
}
return nil
}
// IntentionReadAllowed determines if a specific intention can be read.
func (a AllowAuthorizer) IntentionReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.IntentionRead(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceIntention, AccessRead, name)
}
return nil
}
// IntentionWriteAllowed determines if a specific intention can be
// created, modified, or deleted.
func (a AllowAuthorizer) IntentionWriteAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.IntentionWrite(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceIntention, AccessWrite, name)
}
return nil
}
// KeyListAllowed checks for permission to list keys under a prefix
func (a AllowAuthorizer) KeyListAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.KeyList(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceKey, AccessList, name)
}
return nil
}
// KeyReadAllowed checks for permission to read a given key
func (a AllowAuthorizer) KeyReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.KeyRead(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceKey, AccessRead, name)
}
return nil
}
// KeyWriteAllowed checks for permission to write a given key
func (a AllowAuthorizer) KeyWriteAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.KeyWrite(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceKey, AccessWrite, name)
}
return nil
}
// KeyWritePrefixAllowed checks for permission to write to an
// entire key prefix. This means there must be no sub-policies
// that deny a write.
func (a AllowAuthorizer) KeyWritePrefixAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.KeyWritePrefix(name, ctx) != Allow {
// TODO(acl-error-enhancements) revisit this message; we may need to do some extra plumbing inside of KeyWritePrefix to
// return properly detailed information.
return PermissionDeniedByACL(a, ctx, ResourceKey, AccessWrite, name)
}
return nil
}
// KeyringReadAllowed determines if the encryption keyring used in
// the gossip layer can be read.
func (a AllowAuthorizer) KeyringReadAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.KeyringRead(ctx) != Allow {
return PermissionDeniedByACLUnnamed(a, ctx, ResourceKeyring, AccessRead)
}
return nil
}
// KeyringWriteAllowed determines if the keyring can be manipulated
func (a AllowAuthorizer) KeyringWriteAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.KeyringWrite(ctx) != Allow {
return PermissionDeniedByACLUnnamed(a, ctx, ResourceKeyring, AccessWrite)
}
return nil
}
// MeshReadAllowed determines if the read-only Consul mesh functions
// can be used.
func (a AllowAuthorizer) MeshReadAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.MeshRead(ctx) != Allow {
return PermissionDeniedByACLUnnamed(a, ctx, ResourceMesh, AccessRead)
}
return nil
}
// MeshWriteAllowed determines if the state-changing Consul mesh
// functions can be used.
func (a AllowAuthorizer) MeshWriteAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.MeshWrite(ctx) != Allow {
return PermissionDeniedByACLUnnamed(a, ctx, ResourceMesh, AccessWrite)
}
return nil
}
// NodeReadAllowed checks for permission to read (discover) a given node.
func (a AllowAuthorizer) NodeReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.NodeRead(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceNode, AccessRead, name)
}
return nil
}
// NodeReadAllAllowed checks for permission to read (discover) all nodes.
func (a AllowAuthorizer) NodeReadAllAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.NodeReadAll(ctx) != Allow {
// This is only used to gate certain UI functions right now (e.g metrics)
return PermissionDeniedByACL(a, ctx, ResourceNode, AccessRead, "all nodes")
}
return nil
}
// NodeWriteAllowed checks for permission to create or update (register) a
// given node.
func (a AllowAuthorizer) NodeWriteAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.NodeWrite(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceNode, AccessWrite, name)
}
return nil
}
// OperatorReadAllowed determines if the read-only Consul operator functions
// can be used.
func (a AllowAuthorizer) OperatorReadAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.OperatorRead(ctx) != Allow {
return PermissionDeniedByACLUnnamed(a, ctx, ResourceOperator, AccessRead)
}
return nil
}
// OperatorWriteAllowed determines if the state-changing Consul operator
// functions can be used.
func (a AllowAuthorizer) OperatorWriteAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.OperatorWrite(ctx) != Allow {
return PermissionDeniedByACLUnnamed(a, ctx, ResourceOperator, AccessWrite)
}
return nil
}
// PreparedQueryReadAllowed determines if a specific prepared query can be read
// to show its contents (this is not used for execution).
func (a AllowAuthorizer) PreparedQueryReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.PreparedQueryRead(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceQuery, AccessRead, name)
}
return nil
}
// PreparedQueryWriteAllowed determines if a specific prepared query can be
// created, modified, or deleted.
func (a AllowAuthorizer) PreparedQueryWriteAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.PreparedQueryWrite(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceQuery, AccessWrite, name)
}
return nil
}
// ServiceReadAllowed checks for permission to read a given service
func (a AllowAuthorizer) ServiceReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.ServiceRead(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceService, AccessRead, name)
}
return nil
}
// ServiceReadAllAllowed checks for permission to read all services
func (a AllowAuthorizer) ServiceReadAllAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.ServiceReadAll(ctx) != Allow {
// This is only used to gate certain UI functions right now (e.g metrics)
return PermissionDeniedByACL(a, ctx, ResourceService, AccessRead, "all services") // read
}
return nil
}
// ServiceWriteAllowed checks for permission to create or update a given
// service
func (a AllowAuthorizer) ServiceWriteAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.ServiceWrite(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceService, AccessWrite, name)
}
return nil
}
// SessionReadAllowed checks for permission to read sessions for a given node.
func (a AllowAuthorizer) SessionReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.SessionRead(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceSession, AccessRead, name)
}
return nil
}
// SessionWriteAllowed checks for permission to create sessions for a given
// node.
func (a AllowAuthorizer) SessionWriteAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.SessionWrite(name, ctx) != Allow {
return PermissionDeniedByACL(a, ctx, ResourceSession, AccessWrite, name)
}
return nil
}
// SnapshotAllowed checks for permission to take and restore snapshots.
func (a AllowAuthorizer) SnapshotAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.Snapshot(ctx) != Allow {
// Implementation of this currently just checks acl write
return PermissionDeniedByACLUnnamed(a, ctx, ResourceACL, AccessWrite)
}
return nil
}
func Enforce(authz Authorizer, rsc Resource, segment string, access string, ctx *AuthorizerContext) (EnforcementDecision, error) {

View File

@ -204,6 +204,10 @@ func (m *mockAuthorizer) Snapshot(ctx *AuthorizerContext) EnforcementDecision {
return ret.Get(0).(EnforcementDecision)
}
func (p *mockAuthorizer) ToAllowAuthorizer() AllowAuthorizer {
return AllowAuthorizer{Authorizer: p}
}
func TestACL_Enforce(t *testing.T) {
type testCase struct {
method string

View File

@ -256,3 +256,7 @@ func (c *ChainedAuthorizer) Snapshot(entCtx *AuthorizerContext) EnforcementDecis
return authz.Snapshot(entCtx)
})
}
func (c *ChainedAuthorizer) ToAllowAuthorizer() AllowAuthorizer {
return AllowAuthorizer{Authorizer: c}
}

View File

@ -99,6 +99,10 @@ func (authz testAuthorizer) Snapshot(*AuthorizerContext) EnforcementDecision {
return EnforcementDecision(authz)
}
func (authz testAuthorizer) ToAllowAuthorizer() AllowAuthorizer {
return AllowAuthorizer{Authorizer: &authz}
}
func TestChainedAuthorizer(t *testing.T) {
t.Run("No Authorizers", func(t *testing.T) {
authz := NewChainedAuthorizer([]Authorizer{})

View File

@ -106,7 +106,7 @@ func (e PermissionDeniedError) Error() string {
fmt.Fprintf(&message, " lacks permission '%s:%s'", e.Resource, e.AccessLevel.String())
if e.ResourceID.Name != "" {
fmt.Fprintf(&message, " %s", e.ResourceID.ToString())
fmt.Fprintf(&message, " on %s", e.ResourceID.ToString())
}
return message.String()
}

View File

@ -29,7 +29,7 @@ func TestPermissionDeniedError(t *testing.T) {
},
{
err: PermissionDeniedByACL(&auth1, nil, ResourceService, AccessRead, "foobar"),
expected: "Permission denied: provided accessor lacks permission 'service:read' foobar",
expected: "Permission denied: provided accessor lacks permission 'service:read' on foobar",
},
{
err: PermissionDeniedByACLUnnamed(&auth1, nil, ResourceService, AccessRead),

View File

@ -787,3 +787,7 @@ func (p *policyAuthorizer) SessionWrite(node string, _ *AuthorizerContext) Enfor
}
return Default
}
func (p *policyAuthorizer) ToAllowAuthorizer() AllowAuthorizer {
return AllowAuthorizer{Authorizer: p}
}

View File

@ -240,6 +240,10 @@ func (s *staticAuthorizer) Snapshot(_ *AuthorizerContext) EnforcementDecision {
return Deny
}
func (s *staticAuthorizer) ToAllowAuthorizer() AllowAuthorizer {
return AllowAuthorizer{Authorizer: s}
}
// AllowAll returns an Authorizer that allows all operations
func AllowAll() Authorizer {
return allowAll

47
acl/testing.go Normal file
View File

@ -0,0 +1,47 @@
package acl
import (
"github.com/stretchr/testify/require"
"regexp"
"testing"
)
func RequirePermissionDeniedError(t testing.TB, err error, _ Authorizer, _ *AuthorizerContext, resource Resource, accessLevel AccessLevel, resourceID string) {
t.Helper()
if err == nil {
t.Fatal("An error is expected but got nil.")
}
if v, ok := err.(PermissionDeniedError); ok {
require.Equal(t, v.Resource, resource)
require.Equal(t, v.AccessLevel, accessLevel)
require.Equal(t, v.ResourceID.Name, resourceID)
} else {
t.Fatalf("Expected a permission denied error got %T %vp", err, err)
}
}
func RequirePermissionDeniedMessage(t testing.TB, msg string, auth Authorizer, _ *AuthorizerContext, resource Resource, accessLevel AccessLevel, resourceID string) {
require.NotEmpty(t, msg, "expected non-empty error message")
var resourceIDFound string
if auth == nil {
expr := "^Permission denied" + `: provided accessor lacks permission '(\S*):(\S*)' on (.*)\s*$`
re, _ := regexp.Compile(expr)
matched := re.FindStringSubmatch(msg)
require.Equal(t, string(resource), matched[1], "resource")
require.Equal(t, accessLevel.String(), matched[2], "access level")
resourceIDFound = matched[3]
} else {
expr := "^Permission denied" + `: accessor '(\S*)' lacks permission '(\S*):(\S*)' on (.*)\s*$`
re, _ := regexp.Compile(expr)
matched := re.FindStringSubmatch(msg)
require.Equal(t, auth, matched[1], "auth")
require.Equal(t, string(resource), matched[2], "resource")
require.Equal(t, accessLevel.String(), matched[3], "access level")
resourceIDFound = matched[4]
}
// AuthorizerContext information should be checked here
require.Contains(t, resourceIDFound, resourceID, "resource id")
}

View File

@ -43,15 +43,15 @@ func (a *Agent) vetServiceRegisterWithAuthorizer(authz acl.Authorizer, service *
// Vet the service itself.
service.FillAuthzContext(&authzContext)
if authz.ServiceWrite(service.Service, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessWrite, service.Service)
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(service.Service, &authzContext); err != nil {
return err
}
// Vet any service that might be getting overwritten.
if existing := a.State.Service(service.CompoundServiceID()); existing != nil {
existing.FillAuthzContext(&authzContext)
if authz.ServiceWrite(existing.Service, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessWrite, existing.Service)
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(existing.Service, &authzContext); err != nil {
return err
}
}
@ -59,8 +59,8 @@ func (a *Agent) vetServiceRegisterWithAuthorizer(authz acl.Authorizer, service *
// since it can be discovered as an instance of that service.
if service.Kind == structs.ServiceKindConnectProxy {
service.FillAuthzContext(&authzContext)
if authz.ServiceWrite(service.Proxy.DestinationServiceName, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessWrite, service.Proxy.DestinationServiceName)
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(service.Proxy.DestinationServiceName, &authzContext); err != nil {
return err
}
}
@ -73,8 +73,9 @@ func (a *Agent) vetServiceUpdateWithAuthorizer(authz acl.Authorizer, serviceID s
// Vet any changes based on the existing services's info.
if existing := a.State.Service(serviceID); existing != nil {
existing.FillAuthzContext(&authzContext)
if authz.ServiceWrite(existing.Service, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessWrite, existing.Service)
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(existing.Service, &authzContext); err != nil {
return err
}
} else {
// Take care if modifying this error message.
@ -95,13 +96,13 @@ func (a *Agent) vetCheckRegisterWithAuthorizer(authz acl.Authorizer, check *stru
// Vet the check itself.
if len(check.ServiceName) > 0 {
if authz.ServiceWrite(check.ServiceName, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessWrite, check.ServiceName)
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(check.ServiceName, &authzContext); err != nil {
return err
}
} else {
// N.B. Should this authzContext be derived from a.AgentEnterpriseMeta()
if authz.NodeWrite(a.config.NodeName, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceNode, acl.AccessWrite, a.config.NodeName)
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(a.config.NodeName, &authzContext); err != nil {
return err
}
}
@ -109,13 +110,13 @@ func (a *Agent) vetCheckRegisterWithAuthorizer(authz acl.Authorizer, check *stru
if existing := a.State.Check(check.CompoundCheckID()); existing != nil {
if len(existing.ServiceName) > 0 {
// N.B. Should this authzContext be derived from existing.EnterpriseMeta?
if authz.ServiceWrite(existing.ServiceName, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessWrite, existing.ServiceName)
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(existing.ServiceName, &authzContext); err != nil {
return err
}
} else {
// N.B. Should this authzContext be derived from a.AgentEnterpriseMeta()
if authz.NodeWrite(a.config.NodeName, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceNode, acl.AccessWrite, a.config.NodeName)
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(a.config.NodeName, &authzContext); err != nil {
return err
}
}
}
@ -130,12 +131,12 @@ func (a *Agent) vetCheckUpdateWithAuthorizer(authz acl.Authorizer, checkID struc
// Vet any changes based on the existing check's info.
if existing := a.State.Check(checkID); existing != nil {
if len(existing.ServiceName) > 0 {
if authz.ServiceWrite(existing.ServiceName, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessWrite, existing.ServiceName)
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(existing.ServiceName, &authzContext); err != nil {
return err
}
} else {
if authz.NodeWrite(a.config.NodeName, &authzContext) != acl.Allow {
return acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceNode, acl.AccessWrite, a.config.NodeName)
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(a.config.NodeName, &authzContext); err != nil {
return err
}
}
} else {

View File

@ -707,6 +707,7 @@ func (a *Agent) listenAndServeGRPC() error {
xdsServer := xds.NewServer(
a.logger.Named(logging.Envoy),
a.config.ConnectServerlessPluginEnabled,
a.proxyConfig,
func(id string) (acl.Authorizer, error) {
return a.delegate.ResolveTokenAndDefaultMeta(id, nil, nil)

View File

@ -60,8 +60,8 @@ func (s *HTTPHandlers) AgentSelf(resp http.ResponseWriter, req *http.Request) (i
// Authorize using the agent's own enterprise meta, not the token.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.AgentRead(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().AgentReadAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
var cs lib.CoordinateSet
@ -150,8 +150,8 @@ func (s *HTTPHandlers) AgentMetrics(resp http.ResponseWriter, req *http.Request)
// Authorize using the agent's own enterprise meta, not the token.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.AgentRead(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().AgentReadAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
if enablePrometheusOutput(req) {
if s.agent.config.Telemetry.PrometheusOpts.Expiration < 1 {
@ -187,8 +187,8 @@ func (s *HTTPHandlers) AgentMetricsStream(resp http.ResponseWriter, req *http.Re
// Authorize using the agent's own enterprise meta, not the token.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.AgentRead(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().AgentReadAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
flusher, ok := resp.(http.Flusher)
@ -240,8 +240,8 @@ func (s *HTTPHandlers) AgentReload(resp http.ResponseWriter, req *http.Request)
// Authorize using the agent's own enterprise meta, not the token.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.AgentWrite(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().AgentWriteAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
return nil, s.agent.ReloadConfig()
@ -440,8 +440,8 @@ func (s *HTTPHandlers) AgentService(resp http.ResponseWriter, req *http.Request)
}
var authzContext acl.AuthorizerContext
svc.FillAuthzContext(&authzContext)
if authz.ServiceRead(svc.Service, &authzContext) != acl.Allow {
return "", nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(svc.Service, &authzContext); err != nil {
return "", nil, err
}
// Calculate the content hash over the response, minus the hash field
@ -621,8 +621,9 @@ func (s *HTTPHandlers) AgentJoin(resp http.ResponseWriter, req *http.Request) (i
// Authorize using the agent's own enterprise meta, not the token.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.AgentWrite(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceAgent, acl.AccessWrite, s.agent.config.NodeName)
if err := authz.ToAllowAuthorizer().AgentWriteAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
// Get the request partition and default to that of the agent.
@ -666,8 +667,8 @@ func (s *HTTPHandlers) AgentLeave(resp http.ResponseWriter, req *http.Request) (
// Authorize using the agent's own enterprise meta, not the token.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.AgentWrite(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().AgentWriteAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
if err := s.agent.Leave(); err != nil {
@ -685,8 +686,8 @@ func (s *HTTPHandlers) AgentForceLeave(resp http.ResponseWriter, req *http.Reque
return nil, err
}
// TODO(partitions): should this be possible in a partition?
if authz.OperatorWrite(nil) != acl.Allow {
return nil, acl.PermissionDeniedByACLUnnamed(authz, nil, acl.ResourceOperator, acl.AccessWrite)
if err := authz.ToAllowAuthorizer().OperatorWriteAllowed(nil); err != nil {
return nil, err
}
// Get the request partition and default to that of the agent.
@ -1007,8 +1008,8 @@ func (s *HTTPHandlers) AgentHealthServiceByID(resp http.ResponseWriter, req *htt
dc := s.agent.config.Datacenter
if service := s.agent.State.Service(sid); service != nil {
if authz.ServiceRead(service.Service, &authzContext) != acl.Allow {
return nil, acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessRead, service.Service)
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(service.Service, &authzContext); err != nil {
return nil, err
}
code, status, healthChecks := agentHealthService(sid, s)
if returnTextPlain(req) {
@ -1060,8 +1061,8 @@ func (s *HTTPHandlers) AgentHealthServiceByName(resp http.ResponseWriter, req *h
return nil, err
}
if authz.ServiceRead(serviceName, &authzContext) != acl.Allow {
return nil, acl.PermissionDeniedByACL(authz, &authzContext, acl.ResourceService, acl.AccessRead, serviceName)
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(serviceName, &authzContext); err != nil {
return nil, err
}
if !s.validateRequestPartition(resp, &entMeta) {
@ -1374,8 +1375,8 @@ func (s *HTTPHandlers) AgentNodeMaintenance(resp http.ResponseWriter, req *http.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.NodeWrite(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
if enable {
@ -1399,8 +1400,8 @@ func (s *HTTPHandlers) AgentMonitor(resp http.ResponseWriter, req *http.Request)
// Authorize using the agent's own enterprise meta, not the token.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.AgentRead(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().AgentReadAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
// Get the provided loglevel.
@ -1482,8 +1483,8 @@ func (s *HTTPHandlers) AgentToken(resp http.ResponseWriter, req *http.Request) (
// Authorize using the agent's own enterprise meta, not the token.
var authzContext acl.AuthorizerContext
s.agent.AgentEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.AgentWrite(s.agent.config.NodeName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().AgentWriteAllowed(s.agent.config.NodeName, &authzContext); err != nil {
return nil, err
}
// The body is just the token, but it's in a JSON object so we can add
@ -1683,8 +1684,8 @@ func (s *HTTPHandlers) AgentHost(resp http.ResponseWriter, req *http.Request) (i
}
// TODO(partitions): should this be possible in a partition?
if authz.OperatorRead(nil) != acl.Allow {
return nil, acl.PermissionDeniedByACLUnnamed(authz, nil, acl.ResourceOperator, acl.AccessRead)
if err := authz.ToAllowAuthorizer().OperatorReadAllowed(nil); err != nil {
return nil, err
}
return debug.CollectHostInfo(), nil

View File

@ -2,19 +2,21 @@ package agent
import (
"fmt"
"github.com/hashicorp/consul/api"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/serf/coordinate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testrpc"
)
func TestCatalogRegister_Service_InvalidAddress(t *testing.T) {
@ -412,42 +414,28 @@ func TestCatalogNodes_DistanceSort(t *testing.T) {
Address: "127.0.0.1",
}
var out struct{}
if err := a.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, a.RPC("Catalog.Register", args, &out))
args = &structs.RegisterRequest{
Datacenter: "dc1",
Node: "bar",
Address: "127.0.0.2",
}
if err := a.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, a.RPC("Catalog.Register", args, &out))
// Nobody has coordinates set so this will still return them in the
// order they are indexed.
req, _ := http.NewRequest("GET", "/v1/catalog/nodes?dc=dc1&near=foo", nil)
resp := httptest.NewRecorder()
obj, err := a.srv.CatalogNodes(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, err)
assertIndex(t, resp)
nodes := obj.(structs.Nodes)
if len(nodes) != 3 {
t.Fatalf("bad: %v", obj)
}
if nodes[0].Node != "bar" {
t.Fatalf("bad: %v", nodes)
}
if nodes[1].Node != "foo" {
t.Fatalf("bad: %v", nodes)
}
if nodes[2].Node != a.Config.NodeName {
t.Fatalf("bad: %v", nodes)
}
require.Len(t, nodes, 3)
require.Equal(t, "bar", nodes[0].Node)
require.Equal(t, "foo", nodes[1].Node)
require.Equal(t, a.Config.NodeName, nodes[2].Node)
// Send an update for the node and wait for it to get applied.
arg := structs.CoordinateUpdateRequest{
@ -455,33 +443,21 @@ func TestCatalogNodes_DistanceSort(t *testing.T) {
Node: "foo",
Coord: coordinate.NewCoordinate(coordinate.DefaultConfig()),
}
if err := a.RPC("Coordinate.Update", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, a.RPC("Coordinate.Update", &arg, &out))
time.Sleep(300 * time.Millisecond)
// Query again and now foo should have moved to the front of the line.
req, _ = http.NewRequest("GET", "/v1/catalog/nodes?dc=dc1&near=foo", nil)
resp = httptest.NewRecorder()
obj, err = a.srv.CatalogNodes(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, err)
assertIndex(t, resp)
nodes = obj.(structs.Nodes)
if len(nodes) != 3 {
t.Fatalf("bad: %v", obj)
}
if nodes[0].Node != "foo" {
t.Fatalf("bad: %v", nodes)
}
if nodes[1].Node != "bar" {
t.Fatalf("bad: %v", nodes)
}
if nodes[2].Node != a.Config.NodeName {
t.Fatalf("bad: %v", nodes)
}
require.Len(t, nodes, 3)
require.Equal(t, "foo", nodes[0].Node)
require.Equal(t, "bar", nodes[1].Node)
require.Equal(t, a.Config.NodeName, nodes[2].Node)
}
func TestCatalogServices(t *testing.T) {

View File

@ -2,6 +2,7 @@ package checks
import (
"fmt"
"strings"
"sync"
"time"
@ -246,7 +247,7 @@ func (c *CheckAlias) processChecks(checks []*structs.HealthCheck, CheckIfService
msg := "No checks found."
serviceFound := false
for _, chk := range checks {
if c.Node != "" && c.Node != chk.Node {
if c.Node != "" && !strings.EqualFold(c.Node, chk.Node) {
continue
}
serviceMatch := c.ServiceID.Matches(chk.CompoundServiceID())

View File

@ -454,46 +454,55 @@ func TestCheckAlias_remoteNodeOnlyPassing(t *testing.T) {
func TestCheckAlias_remoteNodeOnlyCritical(t *testing.T) {
t.Parallel()
notify := newMockAliasNotify()
chkID := structs.NewCheckID(types.CheckID("foo"), nil)
rpc := &mockRPC{}
chk := &CheckAlias{
Node: "remote",
CheckID: chkID,
Notify: notify,
RPC: rpc,
run := func(t *testing.T, responseNodeName string) {
notify := newMockAliasNotify()
chkID := structs.NewCheckID(types.CheckID("foo"), nil)
rpc := &mockRPC{}
chk := &CheckAlias{
Node: "remote",
CheckID: chkID,
Notify: notify,
RPC: rpc,
}
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{
HealthChecks: []*structs.HealthCheck{
// Should ignore non-matching node
{
Node: "A",
ServiceID: "web",
Status: api.HealthCritical,
},
// Should ignore any services
{
Node: responseNodeName,
ServiceID: "db",
Status: api.HealthCritical,
},
// Match
{
Node: responseNodeName,
Status: api.HealthCritical,
},
},
})
chk.Start()
defer chk.Stop()
retry.Run(t, func(r *retry.R) {
if got, want := notify.State(chkID), api.HealthCritical; got != want {
r.Fatalf("got state %q want %q", got, want)
}
})
}
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{
HealthChecks: []*structs.HealthCheck{
// Should ignore non-matching node
{
Node: "A",
ServiceID: "web",
Status: api.HealthCritical,
},
// Should ignore any services
{
Node: "remote",
ServiceID: "db",
Status: api.HealthCritical,
},
// Match
{
Node: "remote",
Status: api.HealthCritical,
},
},
t.Run("same case node name", func(t *testing.T) {
run(t, "remote")
})
chk.Start()
defer chk.Stop()
retry.Run(t, func(r *retry.R) {
if got, want := notify.State(chkID), api.HealthCritical; got != want {
r.Fatalf("got state %q want %q", got, want)
}
t.Run("lowercase node name", func(t *testing.T) {
run(t, "ReMoTe")
})
}

View File

@ -669,6 +669,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
connectEnabled := boolVal(c.Connect.Enabled)
connectCAProvider := stringVal(c.Connect.CAProvider)
connectCAConfig := c.Connect.CAConfig
serverlessPluginEnabled := boolVal(c.Connect.EnableServerlessPlugin)
// autoEncrypt and autoConfig implicitly turns on connect which is why
// they need to be above other settings that rely on connect.
@ -979,6 +980,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
ConnectCAProvider: connectCAProvider,
ConnectCAConfig: connectCAConfig,
ConnectMeshGatewayWANFederationEnabled: connectMeshGatewayWANFederationEnabled,
ConnectServerlessPluginEnabled: serverlessPluginEnabled,
ConnectSidecarMinPort: sidecarMinPort,
ConnectSidecarMaxPort: sidecarMaxPort,
ConnectTestCALeafRootChangeSpread: b.durationVal("connect.test_ca_leaf_root_change_spread", c.Connect.TestCALeafRootChangeSpread),

View File

@ -612,6 +612,7 @@ type Connect struct {
CAProvider *string `mapstructure:"ca_provider"`
CAConfig map[string]interface{} `mapstructure:"ca_config"`
MeshGatewayWANFederationEnabled *bool `mapstructure:"enable_mesh_gateway_wan_federation"`
EnableServerlessPlugin *bool `mapstructure:"enable_serverless_plugin"`
// TestCALeafRootChangeSpread controls how long after a CA roots change before new leaft certs will be generated.
// This is only tuned in tests, generally set to 1ns to make tests deterministic with when to expect updated leaf

View File

@ -486,6 +486,12 @@ type RuntimeConfig struct {
// and servers in a cluster for correct connect operation.
ConnectEnabled bool
// ConnectServerlessPluginEnabled opts the agent into the serverless plugin.
// This plugin allows services to be configured as AWS Lambdas. After the
// Lambda service is configured, Connect services can invoke the Lambda
// service like any other upstream.
ConnectServerlessPluginEnabled bool
// ConnectSidecarMinPort is the inclusive start of the range of ports
// allocated to the agent for asigning to sidecar services where no port is
// specified.

View File

@ -5538,6 +5538,7 @@ func TestLoad_FullConfig(t *testing.T) {
"CSRMaxConcurrent": float64(2),
},
ConnectMeshGatewayWANFederationEnabled: false,
ConnectServerlessPluginEnabled: true,
DNSAddrs: []net.Addr{tcpAddr("93.95.95.81:7001"), udpAddr("93.95.95.81:7001")},
DNSARecordLimit: 29907,
DNSAllowStale: true,

View File

@ -127,6 +127,7 @@
"ConnectCAProvider": "",
"ConnectEnabled": false,
"ConnectMeshGatewayWANFederationEnabled": false,
"ConnectServerlessPluginEnabled": false,
"ConnectSidecarMaxPort": 0,
"ConnectSidecarMinPort": 0,
"ConnectTestCALeafRootChangeSpread": "0s",

View File

@ -210,6 +210,7 @@ connect {
}
enable_mesh_gateway_wan_federation = false
enabled = true
enable_serverless_plugin = true
}
gossip_lan {
gossip_nodes = 6

View File

@ -209,7 +209,8 @@
"csr_max_concurrent": 2
},
"enable_mesh_gateway_wan_federation": false,
"enabled": true
"enabled": true,
"enable_serverless_plugin": true
},
"gossip_lan" : {
"gossip_nodes": 6,

View File

@ -32,3 +32,7 @@ func NewKindName(kind, name string, entMeta *structs.EnterpriseMeta) KindName {
ret.Normalize()
return ret
}
func NewKindNameForEntry(entry structs.ConfigEntry) KindName {
return NewKindName(entry.GetKind(), entry.GetName(), entry.GetEnterpriseMeta())
}

View File

@ -0,0 +1,57 @@
package configentry
import (
"github.com/hashicorp/consul/agent/structs"
)
// ResolvedServiceConfigSet is a wrapped set of raw cross-referenced config
// entries necessary for the ConfigEntry.ResolveServiceConfig RPC process.
//
// None of these are defaulted.
type ResolvedServiceConfigSet struct {
ServiceDefaults map[structs.ServiceID]*structs.ServiceConfigEntry
ProxyDefaults map[string]*structs.ProxyConfigEntry
}
func (r *ResolvedServiceConfigSet) IsEmpty() bool {
return len(r.ServiceDefaults) == 0 && len(r.ProxyDefaults) == 0
}
func (r *ResolvedServiceConfigSet) GetServiceDefaults(sid structs.ServiceID) *structs.ServiceConfigEntry {
if r.ServiceDefaults == nil {
return nil
}
return r.ServiceDefaults[sid]
}
func (r *ResolvedServiceConfigSet) GetProxyDefaults(partition string) *structs.ProxyConfigEntry {
if r.ProxyDefaults == nil {
return nil
}
return r.ProxyDefaults[partition]
}
func (r *ResolvedServiceConfigSet) AddServiceDefaults(entry *structs.ServiceConfigEntry) {
if entry == nil {
return
}
if r.ServiceDefaults == nil {
r.ServiceDefaults = make(map[structs.ServiceID]*structs.ServiceConfigEntry)
}
sid := structs.NewServiceID(entry.Name, &entry.EnterpriseMeta)
r.ServiceDefaults[sid] = entry
}
func (r *ResolvedServiceConfigSet) AddProxyDefaults(entry *structs.ProxyConfigEntry) {
if entry == nil {
return
}
if r.ProxyDefaults == nil {
r.ProxyDefaults = make(map[string]*structs.ProxyConfigEntry)
}
r.ProxyDefaults[entry.PartitionOrDefault()] = entry
}

View File

@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"os/exec"
"strings"
"sync"
"github.com/hashicorp/go-hclog"
@ -86,6 +87,13 @@ func TestConsulProvider(t testing.T, d ConsulProviderStateDelegate) *ConsulProvi
// These tests may be skipped in CI. They are run as part of a separate
// integration test suite.
func SkipIfVaultNotPresent(t testing.T) {
// Try to safeguard against tests that will never run in CI.
// This substring should match the pattern used by the
// test-connect-ca-providers CI job.
if !strings.Contains(t.Name(), "Vault") {
t.Fatalf("test name must contain Vault, otherwise CI will never run it")
}
vaultBinaryName := os.Getenv("VAULT_BINARY_NAME")
if vaultBinaryName == "" {
vaultBinaryName = "vault"

View File

@ -65,8 +65,8 @@ func (a *Agent) ConnectAuthorize(token string,
return returnErr(err)
}
if authz.ServiceWrite(req.Target, &authzContext) != acl.Allow {
return returnErr(acl.ErrPermissionDenied)
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(req.Target, &authzContext); err != nil {
return returnErr(err)
}
if !uriService.MatchesPartition(req.TargetPartition()) {

View File

@ -286,8 +286,8 @@ func (a *ACL) TokenRead(args *structs.ACLTokenGetRequest, reply *structs.ACLToke
// secrets will be redacted
if authz, err = a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
}
@ -354,8 +354,8 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.ACLToken.EnterpriseMeta, &authzContext)
if err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
_, token, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, args.ACLToken.AccessorID, &args.ACLToken.EnterpriseMeta)
@ -425,8 +425,8 @@ func (a *ACL) TokenSet(args *structs.ACLTokenSetRequest, reply *structs.ACLToken
var authzContext acl.AuthorizerContext
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.ACLToken.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
return a.tokenSetInternal(args, reply, false)
@ -830,8 +830,8 @@ func (a *ACL) TokenDelete(args *structs.ACLTokenDeleteRequest, reply *string) er
var authzContext acl.AuthorizerContext
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
if _, err := uuid.ParseUUID(args.TokenID); err != nil {
@ -919,8 +919,8 @@ func (a *ACL) TokenList(args *structs.ACLTokenListRequest, reply *structs.ACLTok
// merge the token default meta into the requests meta
args.EnterpriseMeta.Merge(&requestMeta)
args.EnterpriseMeta.FillAuthzContext(&authzContext)
if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
var methodMeta *structs.EnterpriseMeta
@ -1026,8 +1026,8 @@ func (a *ACL) PolicyRead(args *structs.ACLPolicyGetRequest, reply *structs.ACLPo
var authzContext acl.AuthorizerContext
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
@ -1107,8 +1107,8 @@ func (a *ACL) PolicySet(args *structs.ACLPolicySetRequest, reply *structs.ACLPol
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.Policy.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
policy := &args.Policy
@ -1237,8 +1237,8 @@ func (a *ACL) PolicyDelete(args *structs.ACLPolicyDeleteRequest, reply *string)
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
_, policy, err := a.srv.fsm.State().ACLPolicyGetByID(nil, args.PolicyID, &args.EnterpriseMeta)
@ -1288,8 +1288,8 @@ func (a *ACL) PolicyList(args *structs.ACLPolicyListRequest, reply *structs.ACLP
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext)
if err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
@ -1412,8 +1412,8 @@ func (a *ACL) RoleRead(args *structs.ACLRoleGetRequest, reply *structs.ACLRoleRe
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
@ -1493,8 +1493,8 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.Role.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
role := &args.Role
@ -1651,8 +1651,8 @@ func (a *ACL) RoleDelete(args *structs.ACLRoleDeleteRequest, reply *string) erro
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
_, role, err := a.srv.fsm.State().ACLRoleGetByID(nil, args.RoleID, &args.EnterpriseMeta)
@ -1698,8 +1698,8 @@ func (a *ACL) RoleList(args *structs.ACLRoleListRequest, reply *structs.ACLRoleL
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext)
if err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
@ -1797,8 +1797,8 @@ func (a *ACL) BindingRuleRead(args *structs.ACLBindingRuleGetRequest, reply *str
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext)
if err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
@ -1840,8 +1840,8 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
// Verify token is permitted to modify ACLs
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.BindingRule.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
var existing *structs.ACLBindingRule
@ -1969,8 +1969,8 @@ func (a *ACL) BindingRuleDelete(args *structs.ACLBindingRuleDeleteRequest, reply
// Verify token is permitted to modify ACLs
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
_, rule, err := a.srv.fsm.State().ACLBindingRuleGetByID(nil, args.BindingRuleID, &args.EnterpriseMeta)
@ -2017,8 +2017,8 @@ func (a *ACL) BindingRuleList(args *structs.ACLBindingRuleListRequest, reply *st
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext)
if err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
@ -2056,8 +2056,8 @@ func (a *ACL) AuthMethodRead(args *structs.ACLAuthMethodGetRequest, reply *struc
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
@ -2101,8 +2101,8 @@ func (a *ACL) AuthMethodSet(args *structs.ACLAuthMethodSetRequest, reply *struct
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.AuthMethod.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
method := &args.AuthMethod
@ -2213,8 +2213,8 @@ func (a *ACL) AuthMethodDelete(args *structs.ACLAuthMethodDeleteRequest, reply *
if authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext); err != nil {
return err
} else if authz.ACLWrite(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLWriteAllowed(&authzContext); err != nil {
return err
}
_, method, err := a.srv.fsm.State().ACLAuthMethodGetByName(nil, args.AuthMethodName, &args.EnterpriseMeta)
@ -2267,8 +2267,8 @@ func (a *ACL) AuthMethodList(args *structs.ACLAuthMethodListRequest, reply *stru
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, &authzContext)
if err != nil {
return err
} else if authz.ACLRead(&authzContext) != acl.Allow {
return acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return err
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,

View File

@ -3,6 +3,7 @@ package consul
import (
"fmt"
"sort"
"strings"
"time"
"github.com/armon/go-metrics"
@ -191,15 +192,15 @@ func servicePreApply(service *structs.NodeService, authz acl.Authorizer, authzCt
// later if version 0.8 is enabled, so we can eventually just
// delete this and do all the ACL checks down there.
if service.Service != structs.ConsulServiceName {
if authz.ServiceWrite(service.Service, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(service.Service, &authzContext); err != nil {
return err
}
}
// Proxies must have write permission on their destination
if service.Kind == structs.ServiceKindConnectProxy {
if authz.ServiceWrite(service.Proxy.DestinationServiceName, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(service.Proxy.DestinationServiceName, &authzContext); err != nil {
return err
}
}
@ -240,16 +241,18 @@ func vetRegisterWithACL(
// privileges.
needsNode := ns == nil || subj.ChangesNode(ns.Node)
if needsNode && authz.NodeWrite(subj.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if needsNode {
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(subj.Node, &authzContext); err != nil {
return err
}
}
// Vet the service change. This includes making sure they can register
// the given service, and that we can write to any existing service that
// is being modified by id (if any).
if subj.Service != nil {
if authz.ServiceWrite(subj.Service.Service, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(subj.Service.Service, &authzContext); err != nil {
return err
}
if ns != nil {
@ -262,7 +265,7 @@ func vetRegisterWithACL(
var secondaryCtx acl.AuthorizerContext
other.FillAuthzContext(&secondaryCtx)
if authz.ServiceWrite(other.Service, &secondaryCtx) != acl.Allow {
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(other.Service, &secondaryCtx); err != nil {
return acl.ErrPermissionDenied
}
}
@ -285,15 +288,15 @@ func vetRegisterWithACL(
// note in state_store.go to ban this down there in Consul 0.8,
// but it's good to leave this here because it's required for
// correctness wrt. ACLs.
if check.Node != subj.Node {
if !strings.EqualFold(check.Node, subj.Node) {
return fmt.Errorf("Node '%s' for check '%s' doesn't match register request node '%s'",
check.Node, check.CheckID, subj.Node)
}
// Node-level check.
if check.ServiceID == "" {
if authz.NodeWrite(subj.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(subj.Node, &authzContext); err != nil {
return err
}
continue
}
@ -323,8 +326,8 @@ func vetRegisterWithACL(
var secondaryCtx acl.AuthorizerContext
other.FillAuthzContext(&secondaryCtx)
if authz.ServiceWrite(other.Service, &secondaryCtx) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(other.Service, &secondaryCtx); err != nil {
return err
}
}
@ -407,7 +410,8 @@ func vetDeregisterWithACL(
// Allow service deregistration if the token has write permission for the node.
// This accounts for cases where the agent no longer has a token with write permission
// on the service to deregister it.
if authz.NodeWrite(subj.Node, &authzContext) == acl.Allow {
nodeWriteErr := authz.ToAllowAuthorizer().NodeWriteAllowed(subj.Node, &authzContext)
if nodeWriteErr == nil {
return nil
}
@ -422,8 +426,8 @@ func vetDeregisterWithACL(
ns.FillAuthzContext(&authzContext)
if authz.ServiceWrite(ns.Service, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(ns.Service, &authzContext); err != nil {
return err
}
} else if subj.CheckID != "" {
if nc == nil {
@ -433,18 +437,18 @@ func vetDeregisterWithACL(
nc.FillAuthzContext(&authzContext)
if nc.ServiceID != "" {
if authz.ServiceWrite(nc.ServiceName, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(nc.ServiceName, &authzContext); err != nil {
return err
}
} else {
if authz.NodeWrite(subj.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(subj.Node, &authzContext); err != nil {
return err
}
}
} else {
// Since NodeWrite is not given - otherwise the earlier check
// would've returned already - we can deny here.
return acl.ErrPermissionDenied
return nodeWriteErr
}
return nil
@ -646,6 +650,8 @@ func (c *Catalog) ServiceNodes(args *structs.ServiceSpecificRequest, reply *stru
// If we're doing a connect query, we need read access to the service
// we're trying to find proxies for, so check that.
if args.Connect {
// TODO(acl-error-enhancements) can this be improved? What happens if we returned an error here?
// Is this similar to filters where we might want to return a hint?
if authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
// Just return nil, which will return an empty response (tested)
return nil
@ -861,8 +867,8 @@ func (c *Catalog) GatewayServices(args *structs.ServiceSpecificRequest, reply *s
return err
}
if authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(args.ServiceName, &authzContext); err != nil {
return err
}
return c.srv.blockingQuery(
@ -925,8 +931,8 @@ func (c *Catalog) VirtualIPForService(args *structs.ServiceSpecificRequest, repl
return err
}
if authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(args.ServiceName, &authzContext); err != nil {
return err
}
state := c.srv.fsm.State()

View File

@ -19,6 +19,7 @@ import (
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/stringslice"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/consul/types"
@ -3431,42 +3432,49 @@ service "gateway" {
}
func TestVetRegisterWithACL(t *testing.T) {
t.Parallel()
appendAuthz := func(t *testing.T, defaultAuthz acl.Authorizer, rules string) acl.Authorizer {
policy, err := acl.NewPolicyFromSource(rules, acl.SyntaxCurrent, nil, nil)
require.NoError(t, err)
authz, err := acl.NewPolicyAuthorizerWithDefaults(defaultAuthz, []*acl.Policy{policy}, nil)
require.NoError(t, err)
return authz
}
t.Run("With an 'allow all' authorizer the update should be allowed", func(t *testing.T) {
args := &structs.RegisterRequest{
Node: "nope",
Address: "127.0.0.1",
}
// With an "allow all" authorizer the update should be allowed.
require.NoError(t, vetRegisterWithACL(acl.ManageAll(), args, nil))
})
var perms acl.Authorizer = acl.DenyAll()
args := &structs.RegisterRequest{
Node: "nope",
Address: "127.0.0.1",
}
// With an "allow all" authorizer the update should be allowed.
if err := vetRegisterWithACL(acl.ManageAll(), args, nil); err != nil {
t.Fatalf("err: %v", err)
}
// Create a basic node policy.
policy, err := acl.NewPolicyFromSource(`
node "node" {
policy = "write"
}
`, acl.SyntaxLegacy, nil, nil)
if err != nil {
t.Fatalf("err %v", err)
}
perms, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
perms = appendAuthz(t, perms, `
node "node" {
policy = "write"
} `)
// With that policy, the update should now be blocked for node reasons.
err = vetRegisterWithACL(perms, args, nil)
if !acl.IsErrPermissionDenied(err) {
t.Fatalf("bad: %v", err)
}
err := vetRegisterWithACL(perms, args, nil)
require.True(t, acl.IsErrPermissionDenied(err))
// Now use a permitted node name.
args.Node = "node"
if err := vetRegisterWithACL(perms, args, nil); err != nil {
t.Fatalf("err: %v", err)
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.1",
}
require.NoError(t, vetRegisterWithACL(perms, args, nil))
// Build some node info that matches what we have now.
ns := &structs.NodeServices{
@ -3478,183 +3486,220 @@ node "node" {
}
// Try to register a service, which should be blocked.
args.Service = &structs.NodeService{
Service: "service",
ID: "my-id",
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "service",
ID: "my-id",
},
}
err = vetRegisterWithACL(perms, args, ns)
if !acl.IsErrPermissionDenied(err) {
t.Fatalf("bad: %v", err)
}
require.True(t, acl.IsErrPermissionDenied(err))
// Chain on a basic service policy.
policy, err = acl.NewPolicyFromSource(`
service "service" {
policy = "write"
}
`, acl.SyntaxLegacy, nil, nil)
if err != nil {
t.Fatalf("err %v", err)
}
perms, err = acl.NewPolicyAuthorizerWithDefaults(perms, []*acl.Policy{policy}, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
perms = appendAuthz(t, perms, `
service "service" {
policy = "write"
} `)
// With the service ACL, the update should go through.
if err := vetRegisterWithACL(perms, args, ns); err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, vetRegisterWithACL(perms, args, ns))
// Add an existing service that they are clobbering and aren't allowed
// to write to.
ns.Services["my-id"] = &structs.NodeService{
Service: "other",
ID: "my-id",
ns = &structs.NodeServices{
Node: &structs.Node{
Node: "node",
Address: "127.0.0.1",
},
Services: map[string]*structs.NodeService{
"my-id": {
Service: "other",
ID: "my-id",
},
},
}
err = vetRegisterWithACL(perms, args, ns)
if !acl.IsErrPermissionDenied(err) {
t.Fatalf("bad: %v", err)
}
require.True(t, acl.IsErrPermissionDenied(err))
// Chain on a policy that allows them to write to the other service.
policy, err = acl.NewPolicyFromSource(`
service "other" {
policy = "write"
}
`, acl.SyntaxLegacy, nil, nil)
if err != nil {
t.Fatalf("err %v", err)
}
perms, err = acl.NewPolicyAuthorizerWithDefaults(perms, []*acl.Policy{policy}, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
perms = appendAuthz(t, perms, `
service "other" {
policy = "write"
} `)
// Now it should go through.
if err := vetRegisterWithACL(perms, args, ns); err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, vetRegisterWithACL(perms, args, ns))
// Try creating the node and the service at once by having no existing
// node record. This should be ok since we have node and service
// permissions.
if err := vetRegisterWithACL(perms, args, nil); err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, vetRegisterWithACL(perms, args, nil))
// Add a node-level check to the member, which should be rejected.
args.Check = &structs.HealthCheck{
Node: "node",
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "service",
ID: "my-id",
},
Check: &structs.HealthCheck{
Node: "node",
},
}
err = vetRegisterWithACL(perms, args, ns)
if err == nil || !strings.Contains(err.Error(), "check member must be nil") {
t.Fatalf("bad: %v", err)
}
testutil.RequireErrorContains(t, err, "check member must be nil")
// Move the check into the slice, but give a bad node name.
args.Check.Node = "nope"
args.Checks = append(args.Checks, args.Check)
args.Check = nil
err = vetRegisterWithACL(perms, args, ns)
if err == nil || !strings.Contains(err.Error(), "doesn't match register request node") {
t.Fatalf("bad: %v", err)
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "service",
ID: "my-id",
},
Checks: []*structs.HealthCheck{
{
Node: "nope",
},
},
}
err = vetRegisterWithACL(perms, args, ns)
testutil.RequireErrorContains(t, err, "doesn't match register request node")
// Fix the node name, which should now go through.
args.Checks[0].Node = "node"
if err := vetRegisterWithACL(perms, args, ns); err != nil {
t.Fatalf("err: %v", err)
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "service",
ID: "my-id",
},
Checks: []*structs.HealthCheck{
{
Node: "node",
},
},
}
require.NoError(t, vetRegisterWithACL(perms, args, ns))
// Add a service-level check.
args.Checks = append(args.Checks, &structs.HealthCheck{
Node: "node",
ServiceID: "my-id",
})
if err := vetRegisterWithACL(perms, args, ns); err != nil {
t.Fatalf("err: %v", err)
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "service",
ID: "my-id",
},
Checks: []*structs.HealthCheck{
{
Node: "node",
},
{
Node: "node",
ServiceID: "my-id",
},
},
}
require.NoError(t, vetRegisterWithACL(perms, args, ns))
// Try creating everything at once. This should be ok since we have all
// the permissions we need. It also makes sure that we can register a
// new node, service, and associated checks.
if err := vetRegisterWithACL(perms, args, nil); err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, vetRegisterWithACL(perms, args, nil))
// Nil out the service registration, which'll skip the special case
// and force us to look at the ns data (it will look like we are
// writing to the "other" service which also has "my-id").
args.Service = nil
if err := vetRegisterWithACL(perms, args, ns); err != nil {
t.Fatalf("err: %v", err)
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.1",
Checks: []*structs.HealthCheck{
{
Node: "node",
},
{
Node: "node",
ServiceID: "my-id",
},
},
}
require.NoError(t, vetRegisterWithACL(perms, args, ns))
// Chain on a policy that forbids them to write to the other service.
policy, err = acl.NewPolicyFromSource(`
service "other" {
policy = "deny"
}
`, acl.SyntaxLegacy, nil, nil)
if err != nil {
t.Fatalf("err %v", err)
}
perms, err = acl.NewPolicyAuthorizerWithDefaults(perms, []*acl.Policy{policy}, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
perms = appendAuthz(t, perms, `
service "other" {
policy = "deny"
} `)
// This should get rejected.
err = vetRegisterWithACL(perms, args, ns)
if !acl.IsErrPermissionDenied(err) {
t.Fatalf("bad: %v", err)
}
require.True(t, acl.IsErrPermissionDenied(err))
// Change the existing service data to point to a service name they
// car write to. This should go through.
ns.Services["my-id"] = &structs.NodeService{
Service: "service",
ID: "my-id",
}
if err := vetRegisterWithACL(perms, args, ns); err != nil {
t.Fatalf("err: %v", err)
// can write to. This should go through.
ns = &structs.NodeServices{
Node: &structs.Node{
Node: "node",
Address: "127.0.0.1",
},
Services: map[string]*structs.NodeService{
"my-id": {
Service: "service",
ID: "my-id",
},
},
}
require.NoError(t, vetRegisterWithACL(perms, args, ns))
// Chain on a policy that forbids them to write to the node.
policy, err = acl.NewPolicyFromSource(`
node "node" {
policy = "deny"
}
`, acl.SyntaxLegacy, nil, nil)
if err != nil {
t.Fatalf("err %v", err)
}
perms, err = acl.NewPolicyAuthorizerWithDefaults(perms, []*acl.Policy{policy}, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
perms = appendAuthz(t, perms, `
node "node" {
policy = "deny"
} `)
// This should get rejected because there's a node-level check in here.
err = vetRegisterWithACL(perms, args, ns)
if !acl.IsErrPermissionDenied(err) {
t.Fatalf("bad: %v", err)
}
require.True(t, acl.IsErrPermissionDenied(err))
// Change the node-level check into a service check, and then it should
// go through.
args.Checks[0].ServiceID = "my-id"
if err := vetRegisterWithACL(perms, args, ns); err != nil {
t.Fatalf("err: %v", err)
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.1",
Checks: []*structs.HealthCheck{
{
Node: "node",
ServiceID: "my-id",
},
{
Node: "node",
ServiceID: "my-id",
},
},
}
require.NoError(t, vetRegisterWithACL(perms, args, ns))
// Finally, attempt to update the node part of the data and make sure
// that gets rejected since they no longer have permissions.
args.Address = "127.0.0.2"
err = vetRegisterWithACL(perms, args, ns)
if !acl.IsErrPermissionDenied(err) {
t.Fatalf("bad: %v", err)
args = &structs.RegisterRequest{
Node: "node",
Address: "127.0.0.2",
Checks: []*structs.HealthCheck{
{
Node: "node",
ServiceID: "my-id",
},
{
Node: "node",
ServiceID: "my-id",
},
},
}
err = vetRegisterWithACL(perms, args, ns)
require.True(t, acl.IsErrPermissionDenied(err))
}
func TestVetDeregisterWithACL(t *testing.T) {

View File

@ -10,8 +10,10 @@ import (
"github.com/hashicorp/go-hclog"
memdb "github.com/hashicorp/go-memdb"
"github.com/mitchellh/copystructure"
hashstructure_v2 "github.com/mitchellh/hashstructure/v2"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
)
@ -87,8 +89,8 @@ func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *bool) error
return err
}
if !args.Entry.CanWrite(authz) {
return acl.ErrPermissionDenied
if err := args.Entry.CanWrite(authz); err != nil {
return err
}
if args.Op != structs.ConfigEntryUpsert && args.Op != structs.ConfigEntryUpsertCAS {
@ -192,8 +194,8 @@ func (c *ConfigEntry) Get(args *structs.ConfigEntryQuery, reply *structs.ConfigE
}
lookupEntry.GetEnterpriseMeta().Merge(&args.EnterpriseMeta)
if !lookupEntry.CanRead(authz) {
return acl.ErrPermissionDenied
if err := lookupEntry.CanRead(authz); err != nil {
return err
}
return c.srv.blockingQuery(
@ -236,6 +238,10 @@ func (c *ConfigEntry) List(args *structs.ConfigEntryQuery, reply *structs.Indexe
}
}
var (
priorHash uint64
ranOnce bool
)
return c.srv.blockingQuery(
&args.QueryOptions,
&reply.QueryMeta,
@ -248,7 +254,8 @@ func (c *ConfigEntry) List(args *structs.ConfigEntryQuery, reply *structs.Indexe
// Filter the entries returned by ACL permissions.
filteredEntries := make([]structs.ConfigEntry, 0, len(entries))
for _, entry := range entries {
if !entry.CanRead(authz) {
if err := entry.CanRead(authz); err != nil {
// TODO we may wish to extract more details from this error to aid user comprehension
reply.QueryMeta.ResultsFilteredByACLs = true
continue
}
@ -258,6 +265,26 @@ func (c *ConfigEntry) List(args *structs.ConfigEntryQuery, reply *structs.Indexe
reply.Kind = args.Kind
reply.Index = index
reply.Entries = filteredEntries
// Generate a hash of the content driving this response. Use it to
// determine if the response is identical to a prior wakeup.
newHash, err := hashstructure_v2.Hash(filteredEntries, hashstructure_v2.FormatV2, nil)
if err != nil {
return fmt.Errorf("error hashing reply for spurious wakeup suppression: %w", err)
}
if ranOnce && priorHash == newHash {
priorHash = newHash
return errNotChanged
} else {
priorHash = newHash
ranOnce = true
}
if len(reply.Entries) == 0 {
return errNotFound
}
return nil
})
}
@ -309,7 +336,8 @@ func (c *ConfigEntry) ListAll(args *structs.ConfigEntryListAllRequest, reply *st
// Filter the entries returned by ACL permissions or by the provided kinds.
filteredEntries := make([]structs.ConfigEntry, 0, len(entries))
for _, entry := range entries {
if !entry.CanRead(authz) {
if err := entry.CanRead(authz); err != nil {
// TODO we may wish to extract more details from this error to aid user comprehension
reply.QueryMeta.ResultsFilteredByACLs = true
continue
}
@ -360,8 +388,8 @@ func (c *ConfigEntry) Delete(args *structs.ConfigEntryRequest, reply *structs.Co
return err
}
if !args.Entry.CanWrite(authz) {
return acl.ErrPermissionDenied
if err := args.Entry.CanWrite(authz); err != nil {
return err
}
// Only delete and delete-cas ops are supported. If the caller erroneously
@ -413,119 +441,22 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r
if err != nil {
return err
}
if authz.ServiceRead(args.Name, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(args.Name, &authzContext); err != nil {
return err
}
var (
priorHash uint64
ranOnce bool
)
return c.srv.blockingQuery(
&args.QueryOptions,
&reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error {
var thisReply structs.ServiceConfigResponse
thisReply.MeshGateway.Mode = structs.MeshGatewayModeDefault
// TODO(freddy) Refactor this into smaller set of state store functions
// Pass the WatchSet to both the service and proxy config lookups. If either is updated during the
// blocking query, this function will be rerun and these state store lookups will both be current.
// We use the default enterprise meta to look up the global proxy defaults because they are not namespaced.
_, proxyEntry, err := state.ConfigEntry(ws, structs.ProxyDefaults, structs.ProxyConfigGlobal, &args.EnterpriseMeta)
if err != nil {
return err
}
var (
proxyConf *structs.ProxyConfigEntry
proxyConfGlobalProtocol string
ok bool
upstreamIDs = args.UpstreamIDs
legacyUpstreams = false
)
if proxyEntry != nil {
proxyConf, ok = proxyEntry.(*structs.ProxyConfigEntry)
if !ok {
return fmt.Errorf("invalid proxy config type %T", proxyEntry)
}
// Apply the proxy defaults to the sidecar's proxy config
mapCopy, err := copystructure.Copy(proxyConf.Config)
if err != nil {
return fmt.Errorf("failed to copy global proxy-defaults: %v", err)
}
thisReply.ProxyConfig = mapCopy.(map[string]interface{})
thisReply.Mode = proxyConf.Mode
thisReply.TransparentProxy = proxyConf.TransparentProxy
thisReply.MeshGateway = proxyConf.MeshGateway
thisReply.Expose = proxyConf.Expose
// Extract the global protocol from proxyConf for upstream configs.
rawProtocol := proxyConf.Config["protocol"]
if rawProtocol != nil {
proxyConfGlobalProtocol, ok = rawProtocol.(string)
if !ok {
return fmt.Errorf("invalid protocol type %T", rawProtocol)
}
}
}
index, serviceEntry, err := state.ConfigEntry(ws, structs.ServiceDefaults, args.Name, &args.EnterpriseMeta)
if err != nil {
return err
}
thisReply.Index = index
var serviceConf *structs.ServiceConfigEntry
if serviceEntry != nil {
serviceConf, ok = serviceEntry.(*structs.ServiceConfigEntry)
if !ok {
return fmt.Errorf("invalid service config type %T", serviceEntry)
}
if serviceConf.Expose.Checks {
thisReply.Expose.Checks = true
}
if len(serviceConf.Expose.Paths) >= 1 {
thisReply.Expose.Paths = serviceConf.Expose.Paths
}
if serviceConf.MeshGateway.Mode != structs.MeshGatewayModeDefault {
thisReply.MeshGateway.Mode = serviceConf.MeshGateway.Mode
}
if serviceConf.Protocol != "" {
if thisReply.ProxyConfig == nil {
thisReply.ProxyConfig = make(map[string]interface{})
}
thisReply.ProxyConfig["protocol"] = serviceConf.Protocol
}
if serviceConf.TransparentProxy.OutboundListenerPort != 0 {
thisReply.TransparentProxy.OutboundListenerPort = serviceConf.TransparentProxy.OutboundListenerPort
}
if serviceConf.TransparentProxy.DialedDirectly {
thisReply.TransparentProxy.DialedDirectly = serviceConf.TransparentProxy.DialedDirectly
}
if serviceConf.Mode != structs.ProxyModeDefault {
thisReply.Mode = serviceConf.Mode
}
}
// First collect all upstreams into a set of seen upstreams.
// Upstreams can come from:
// - Explicitly from proxy registrations, and therefore as an argument to this RPC endpoint
// - Implicitly from centralized upstream config in service-defaults
seenUpstreams := map[structs.ServiceID]struct{}{}
upstreamIDs := args.UpstreamIDs
legacyUpstreams := false
var (
noUpstreamArgs = len(upstreamIDs) == 0 && len(args.Upstreams) == 0
// Check the args and the resolved value. If it was exclusively set via a config entry, then args.Mode
// will never be transparent because the service config request does not use the resolved value.
tproxy = args.Mode == structs.ProxyModeTransparent || thisReply.Mode == structs.ProxyModeTransparent
)
// The upstreams passed as arguments to this endpoint are the upstreams explicitly defined in a proxy registration.
// If no upstreams were passed, then we should only returned the resolved config if the proxy in transparent mode.
// Otherwise we would return a resolved upstream config to a proxy with no configured upstreams.
if noUpstreamArgs && !tproxy {
*reply = thisReply
return nil
}
// The request is considered legacy if the deprecated args.Upstream was used
if len(upstreamIDs) == 0 && len(args.Upstreams) > 0 {
@ -533,136 +464,276 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r
upstreamIDs = make([]structs.ServiceID, 0)
for _, upstream := range args.Upstreams {
// Before Consul namespaces were released, the Upstreams provided to the endpoint did not contain the namespace.
// Because of this we attach the enterprise meta of the request, which will just be the default namespace.
// Before Consul namespaces were released, the Upstreams
// provided to the endpoint did not contain the namespace.
// Because of this we attach the enterprise meta of the
// request, which will just be the default namespace.
sid := structs.NewServiceID(upstream, &args.EnterpriseMeta)
upstreamIDs = append(upstreamIDs, sid)
}
}
// First store all upstreams that were provided in the request
for _, sid := range upstreamIDs {
if _, ok := seenUpstreams[sid]; !ok {
seenUpstreams[sid] = struct{}{}
}
}
// Fetch all relevant config entries.
// Then store upstreams inferred from service-defaults and mapify the overrides.
var (
upstreamConfigs = make(map[structs.ServiceID]*structs.UpstreamConfig)
upstreamDefaults *structs.UpstreamConfig
// usConfigs stores the opaque config map for each upstream and is keyed on the upstream's ID.
usConfigs = make(map[structs.ServiceID]map[string]interface{})
index, entries, err := state.ReadResolvedServiceConfigEntries(
ws,
args.Name,
&args.EnterpriseMeta,
upstreamIDs,
args.Mode,
)
if serviceConf != nil && serviceConf.UpstreamConfig != nil {
for i, override := range serviceConf.UpstreamConfig.Overrides {
if override.Name == "" {
c.logger.Warn(
"Skipping UpstreamConfig.Overrides entry without a required name field",
"entryIndex", i,
"kind", serviceConf.GetKind(),
"name", serviceConf.GetName(),
"namespace", serviceConf.GetEnterpriseMeta().NamespaceOrEmpty(),
)
continue // skip this impossible condition
}
seenUpstreams[override.ServiceID()] = struct{}{}
upstreamConfigs[override.ServiceID()] = override
}
if serviceConf.UpstreamConfig.Defaults != nil {
upstreamDefaults = serviceConf.UpstreamConfig.Defaults
// Store the upstream defaults under a wildcard key so that they can be applied to
// upstreams that are inferred from intentions and do not have explicit upstream configuration.
cfgMap := make(map[string]interface{})
upstreamDefaults.MergeInto(cfgMap)
wildcard := structs.NewServiceID(structs.WildcardSpecifier, args.WithWildcardNamespace())
usConfigs[wildcard] = cfgMap
}
if err != nil {
return err
}
for upstream := range seenUpstreams {
resolvedCfg := make(map[string]interface{})
// The protocol of an upstream is resolved in this order:
// 1. Default protocol from proxy-defaults (how all services should be addressed)
// 2. Protocol for upstream service defined in its service-defaults (how the upstream wants to be addressed)
// 3. Protocol defined for the upstream in the service-defaults.(upstream_config.defaults|upstream_config.overrides) of the downstream
// (how the downstream wants to address it)
protocol := proxyConfGlobalProtocol
_, upstreamSvcDefaults, err := state.ConfigEntry(ws, structs.ServiceDefaults, upstream.ID, &upstream.EnterpriseMeta)
if err != nil {
return err
}
if upstreamSvcDefaults != nil {
cfg, ok := upstreamSvcDefaults.(*structs.ServiceConfigEntry)
if !ok {
return fmt.Errorf("invalid service config type %T", upstreamSvcDefaults)
}
if cfg.Protocol != "" {
protocol = cfg.Protocol
}
}
if protocol != "" {
resolvedCfg["protocol"] = protocol
}
// Merge centralized defaults for all upstreams before configuration for specific upstreams
if upstreamDefaults != nil {
upstreamDefaults.MergeInto(resolvedCfg)
}
// The MeshGateway value from the proxy registration overrides the one from upstream_defaults
// because it is specific to the proxy instance.
//
// The goal is to flatten the mesh gateway mode in this order:
// 0. Value from centralized upstream_defaults
// 1. Value from local proxy registration
// 2. Value from centralized upstream_config
// 3. Value from local upstream definition. This last step is done in the client's service manager.
if !args.MeshGateway.IsZero() {
resolvedCfg["mesh_gateway"] = args.MeshGateway
}
if upstreamConfigs[upstream] != nil {
upstreamConfigs[upstream].MergeInto(resolvedCfg)
}
if len(resolvedCfg) > 0 {
usConfigs[upstream] = resolvedCfg
}
// Generate a hash of the config entry content driving this
// response. Use it to determine if the response is identical to a
// prior wakeup.
newHash, err := hashstructure_v2.Hash(entries, hashstructure_v2.FormatV2, nil)
if err != nil {
return fmt.Errorf("error hashing reply for spurious wakeup suppression: %w", err)
}
// don't allocate the slices just to not fill them
if len(usConfigs) == 0 {
*reply = thisReply
return nil
}
if legacyUpstreams {
// For legacy upstreams we return a map that is only keyed on the string ID, since they precede namespaces
thisReply.UpstreamConfigs = make(map[string]map[string]interface{})
for us, conf := range usConfigs {
thisReply.UpstreamConfigs[us.ID] = conf
}
if ranOnce && priorHash == newHash {
priorHash = newHash
reply.Index = index
// NOTE: the prior response is still alive inside of *reply, which
// is desirable
return errNotChanged
} else {
thisReply.UpstreamIDConfigs = make(structs.OpaqueUpstreamConfigs, 0, len(usConfigs))
for us, conf := range usConfigs {
thisReply.UpstreamIDConfigs = append(thisReply.UpstreamIDConfigs,
structs.OpaqueUpstreamConfig{Upstream: us, Config: conf})
}
priorHash = newHash
ranOnce = true
}
thisReply, err := c.computeResolvedServiceConfig(
args,
upstreamIDs,
legacyUpstreams,
entries,
)
if err != nil {
return err
}
thisReply.Index = index
*reply = *thisReply
if entries.IsEmpty() {
// No config entries factored into this reply; it's a default.
return errNotFound
}
*reply = thisReply
return nil
})
}
func (c *ConfigEntry) computeResolvedServiceConfig(
args *structs.ServiceConfigRequest,
upstreamIDs []structs.ServiceID,
legacyUpstreams bool,
entries *configentry.ResolvedServiceConfigSet,
) (*structs.ServiceConfigResponse, error) {
var thisReply structs.ServiceConfigResponse
thisReply.MeshGateway.Mode = structs.MeshGatewayModeDefault
// TODO(freddy) Refactor this into smaller set of state store functions
// Pass the WatchSet to both the service and proxy config lookups. If either is updated during the
// blocking query, this function will be rerun and these state store lookups will both be current.
// We use the default enterprise meta to look up the global proxy defaults because they are not namespaced.
var proxyConfGlobalProtocol string
proxyConf := entries.GetProxyDefaults(args.PartitionOrDefault())
if proxyConf != nil {
// Apply the proxy defaults to the sidecar's proxy config
mapCopy, err := copystructure.Copy(proxyConf.Config)
if err != nil {
return nil, fmt.Errorf("failed to copy global proxy-defaults: %v", err)
}
thisReply.ProxyConfig = mapCopy.(map[string]interface{})
thisReply.Mode = proxyConf.Mode
thisReply.TransparentProxy = proxyConf.TransparentProxy
thisReply.MeshGateway = proxyConf.MeshGateway
thisReply.Expose = proxyConf.Expose
// Extract the global protocol from proxyConf for upstream configs.
rawProtocol := proxyConf.Config["protocol"]
if rawProtocol != nil {
var ok bool
proxyConfGlobalProtocol, ok = rawProtocol.(string)
if !ok {
return nil, fmt.Errorf("invalid protocol type %T", rawProtocol)
}
}
}
serviceConf := entries.GetServiceDefaults(
structs.NewServiceID(args.Name, &args.EnterpriseMeta),
)
if serviceConf != nil {
if serviceConf.Expose.Checks {
thisReply.Expose.Checks = true
}
if len(serviceConf.Expose.Paths) >= 1 {
thisReply.Expose.Paths = serviceConf.Expose.Paths
}
if serviceConf.MeshGateway.Mode != structs.MeshGatewayModeDefault {
thisReply.MeshGateway.Mode = serviceConf.MeshGateway.Mode
}
if serviceConf.Protocol != "" {
if thisReply.ProxyConfig == nil {
thisReply.ProxyConfig = make(map[string]interface{})
}
thisReply.ProxyConfig["protocol"] = serviceConf.Protocol
}
if serviceConf.TransparentProxy.OutboundListenerPort != 0 {
thisReply.TransparentProxy.OutboundListenerPort = serviceConf.TransparentProxy.OutboundListenerPort
}
if serviceConf.TransparentProxy.DialedDirectly {
thisReply.TransparentProxy.DialedDirectly = serviceConf.TransparentProxy.DialedDirectly
}
if serviceConf.Mode != structs.ProxyModeDefault {
thisReply.Mode = serviceConf.Mode
}
thisReply.Meta = serviceConf.Meta
}
// First collect all upstreams into a set of seen upstreams.
// Upstreams can come from:
// - Explicitly from proxy registrations, and therefore as an argument to this RPC endpoint
// - Implicitly from centralized upstream config in service-defaults
seenUpstreams := map[structs.ServiceID]struct{}{}
var (
noUpstreamArgs = len(upstreamIDs) == 0 && len(args.Upstreams) == 0
// Check the args and the resolved value. If it was exclusively set via a config entry, then args.Mode
// will never be transparent because the service config request does not use the resolved value.
tproxy = args.Mode == structs.ProxyModeTransparent || thisReply.Mode == structs.ProxyModeTransparent
)
// The upstreams passed as arguments to this endpoint are the upstreams explicitly defined in a proxy registration.
// If no upstreams were passed, then we should only return the resolved config if the proxy is in transparent mode.
// Otherwise we would return a resolved upstream config to a proxy with no configured upstreams.
if noUpstreamArgs && !tproxy {
return &thisReply, nil
}
// First store all upstreams that were provided in the request
for _, sid := range upstreamIDs {
if _, ok := seenUpstreams[sid]; !ok {
seenUpstreams[sid] = struct{}{}
}
}
// Then store upstreams inferred from service-defaults and mapify the overrides.
var (
upstreamConfigs = make(map[structs.ServiceID]*structs.UpstreamConfig)
upstreamDefaults *structs.UpstreamConfig
// usConfigs stores the opaque config map for each upstream and is keyed on the upstream's ID.
usConfigs = make(map[structs.ServiceID]map[string]interface{})
)
if serviceConf != nil && serviceConf.UpstreamConfig != nil {
for i, override := range serviceConf.UpstreamConfig.Overrides {
if override.Name == "" {
c.logger.Warn(
"Skipping UpstreamConfig.Overrides entry without a required name field",
"entryIndex", i,
"kind", serviceConf.GetKind(),
"name", serviceConf.GetName(),
"namespace", serviceConf.GetEnterpriseMeta().NamespaceOrEmpty(),
)
continue // skip this impossible condition
}
seenUpstreams[override.ServiceID()] = struct{}{}
upstreamConfigs[override.ServiceID()] = override
}
if serviceConf.UpstreamConfig.Defaults != nil {
upstreamDefaults = serviceConf.UpstreamConfig.Defaults
// Store the upstream defaults under a wildcard key so that they can be applied to
// upstreams that are inferred from intentions and do not have explicit upstream configuration.
cfgMap := make(map[string]interface{})
upstreamDefaults.MergeInto(cfgMap)
wildcard := structs.NewServiceID(structs.WildcardSpecifier, args.WithWildcardNamespace())
usConfigs[wildcard] = cfgMap
}
}
for upstream := range seenUpstreams {
resolvedCfg := make(map[string]interface{})
// The protocol of an upstream is resolved in this order:
// 1. Default protocol from proxy-defaults (how all services should be addressed)
// 2. Protocol for upstream service defined in its service-defaults (how the upstream wants to be addressed)
// 3. Protocol defined for the upstream in the service-defaults.(upstream_config.defaults|upstream_config.overrides) of the downstream
// (how the downstream wants to address it)
protocol := proxyConfGlobalProtocol
upstreamSvcDefaults := entries.GetServiceDefaults(
structs.NewServiceID(upstream.ID, &upstream.EnterpriseMeta),
)
if upstreamSvcDefaults != nil {
if upstreamSvcDefaults.Protocol != "" {
protocol = upstreamSvcDefaults.Protocol
}
}
if protocol != "" {
resolvedCfg["protocol"] = protocol
}
// Merge centralized defaults for all upstreams before configuration for specific upstreams
if upstreamDefaults != nil {
upstreamDefaults.MergeInto(resolvedCfg)
}
// The MeshGateway value from the proxy registration overrides the one from upstream_defaults
// because it is specific to the proxy instance.
//
// The goal is to flatten the mesh gateway mode in this order:
// 0. Value from centralized upstream_defaults
// 1. Value from local proxy registration
// 2. Value from centralized upstream_config
// 3. Value from local upstream definition. This last step is done in the client's service manager.
if !args.MeshGateway.IsZero() {
resolvedCfg["mesh_gateway"] = args.MeshGateway
}
if upstreamConfigs[upstream] != nil {
upstreamConfigs[upstream].MergeInto(resolvedCfg)
}
if len(resolvedCfg) > 0 {
usConfigs[upstream] = resolvedCfg
}
}
// don't allocate the slices just to not fill them
if len(usConfigs) == 0 {
return &thisReply, nil
}
if legacyUpstreams {
// For legacy upstreams we return a map that is only keyed on the string ID, since they precede namespaces
thisReply.UpstreamConfigs = make(map[string]map[string]interface{})
for us, conf := range usConfigs {
thisReply.UpstreamConfigs[us.ID] = conf
}
} else {
thisReply.UpstreamIDConfigs = make(structs.OpaqueUpstreamConfigs, 0, len(usConfigs))
for us, conf := range usConfigs {
thisReply.UpstreamIDConfigs = append(thisReply.UpstreamIDConfigs,
structs.OpaqueUpstreamConfig{Upstream: us, Config: conf})
}
}
return &thisReply, nil
}
func gateWriteToSecondary(targetDC, localDC, primaryDC, kind string) error {
// ExportedServices entries are gated from interactions from secondary DCs
// because non-default partitions cannot be created in secondaries

View File

@ -1,7 +1,6 @@
package consul
import (
"context"
"fmt"
"os"
"sort"
@ -9,10 +8,11 @@ import (
"time"
msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc"
hashstructure_v2 "github.com/mitchellh/hashstructure/v2"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testrpc"
@ -309,64 +309,54 @@ func TestConfigEntry_Get_BlockOnNonExistent(t *testing.T) {
t.Skip("too slow for testing.Short")
}
_, s1 := testServerWithConfig(t)
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.DevMode = true // keep it in ram to make it 10x faster on macos
})
codec := rpcClient(t, s1)
store := s1.fsm.State()
entry := &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "alpha",
}
require.NoError(t, store.EnsureConfigEntry(1, entry))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var count int
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
args := structs.ConfigEntryQuery{
Kind: structs.ServiceDefaults,
Name: "does-not-exist",
}
args.QueryOptions.MaxQueryTime = time.Second
for ctx.Err() == nil {
var out structs.ConfigEntryResponse
err := msgpackrpc.CallWithCodec(codec, "ConfigEntry.Get", &args, &out)
if err != nil {
return err
}
t.Log("blocking query index", out.QueryMeta.Index, out.Entry)
count++
args.QueryOptions.MinQueryIndex = out.QueryMeta.Index
}
return nil
})
g.Go(func() error {
for i := uint64(0); i < 200; i++ {
time.Sleep(5 * time.Millisecond)
entry := &structs.ServiceConfigEntry{
{ // create one relevant entry
var out bool
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: fmt.Sprintf("other%d", i),
}
if err := store.EnsureConfigEntry(i+2, entry); err != nil {
return err
}
}
cancel()
return nil
})
require.NoError(t, g.Wait())
// The test is a bit racy because of the timing of the two goroutines, so
// we relax the check for the count to be within a small range.
if count < 2 || count > 3 {
t.Fatalf("expected count to be 2 or 3, got %d", count)
Name: "alpha",
},
}, &out))
require.True(t, out)
}
runStep(t, "test the errNotFound path", func(t *testing.T) {
rpcBlockingQueryTestHarness(t,
func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error) {
args := structs.ConfigEntryQuery{
Kind: structs.ServiceDefaults,
Name: "does-not-exist",
}
args.QueryOptions.MinQueryIndex = minQueryIndex
var out structs.ConfigEntryResponse
errCh := channelCallRPC(s1, "ConfigEntry.Get", &args, &out, nil)
return &out.QueryMeta, errCh
},
func(i int) <-chan error {
var out bool
return channelCallRPC(s1, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: fmt.Sprintf("other%d", i),
},
}, &out, func() error {
if !out {
return fmt.Errorf("[%d] unexpectedly returned false", i)
}
return nil
})
},
)
})
}
func TestConfigEntry_Get_ACLDeny(t *testing.T) {
@ -472,6 +462,79 @@ func TestConfigEntry_List(t *testing.T) {
require.Equal(t, expected, out)
}
func TestConfigEntry_List_BlockOnNoChange(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.DevMode = true // keep it in ram to make it 10x faster on macos
})
codec := rpcClient(t, s1)
run := func(t *testing.T, dataPrefix string) {
rpcBlockingQueryTestHarness(t,
func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error) {
args := structs.ConfigEntryQuery{
Kind: structs.ServiceDefaults,
Datacenter: "dc1",
}
args.QueryOptions.MinQueryIndex = minQueryIndex
var out structs.IndexedConfigEntries
errCh := channelCallRPC(s1, "ConfigEntry.List", &args, &out, nil)
return &out.QueryMeta, errCh
},
func(i int) <-chan error {
var out bool
return channelCallRPC(s1, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: fmt.Sprintf(dataPrefix+"%d", i),
ConnectTimeout: 33 * time.Second,
},
}, &out, func() error {
if !out {
return fmt.Errorf("[%d] unexpectedly returned false", i)
}
return nil
})
},
)
}
runStep(t, "test the errNotFound path", func(t *testing.T) {
run(t, "other")
})
{ // Create some dummy services in the state store to look up.
for _, entry := range []structs.ConfigEntry{
&structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "bar",
},
&structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "foo",
},
} {
var out bool
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: entry,
}, &out))
require.True(t, out)
}
}
runStep(t, "test the errNotChanged path", func(t *testing.T) {
run(t, "completely-different-other")
})
}
func TestConfigEntry_ListAll(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
@ -970,6 +1033,7 @@ func TestConfigEntry_ResolveServiceConfig(t *testing.T) {
Kind: structs.ServiceDefaults,
Name: "foo",
Protocol: "http",
Meta: map[string]string{"foo": "bar"},
}))
require.NoError(t, state.EnsureConfigEntry(2, &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
@ -995,6 +1059,7 @@ func TestConfigEntry_ResolveServiceConfig(t *testing.T) {
"protocol": "grpc",
},
},
Meta: map[string]string{"foo": "bar"},
// Don't know what this is deterministically
QueryMeta: out.QueryMeta,
}
@ -2025,6 +2090,119 @@ func TestConfigEntry_ResolveServiceConfig_ProxyDefaultsProtocol_UsedForAllUpstre
require.Equal(t, expected, out)
}
func BenchmarkConfigEntry_ResolveServiceConfig_Hash(b *testing.B) {
res := &configentry.ResolvedServiceConfigSet{}
res.AddServiceDefaults(&structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "web",
Protocol: "http",
})
res.AddServiceDefaults(&structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "up1",
Protocol: "http",
})
res.AddServiceDefaults(&structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "up2",
Protocol: "http",
})
res.AddProxyDefaults(&structs.ProxyConfigEntry{
Kind: structs.ProxyDefaults,
Name: structs.ProxyConfigGlobal,
Config: map[string]interface{}{
"protocol": "grpc",
},
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := hashstructure_v2.Hash(res, hashstructure_v2.FormatV2, nil)
if err != nil {
b.Fatalf("error: %v", err)
}
}
}
func TestConfigEntry_ResolveServiceConfig_BlockOnNoChange(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.DevMode = true // keep it in ram to make it 10x faster on macos
})
codec := rpcClient(t, s1)
run := func(t *testing.T, dataPrefix string) {
rpcBlockingQueryTestHarness(t,
func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error) {
args := structs.ServiceConfigRequest{
Name: "foo",
UpstreamIDs: []structs.ServiceID{
structs.NewServiceID("bar", nil),
},
}
args.QueryOptions.MinQueryIndex = minQueryIndex
var out structs.ServiceConfigResponse
errCh := channelCallRPC(s1, "ConfigEntry.ResolveServiceConfig", &args, &out, nil)
return &out.QueryMeta, errCh
},
func(i int) <-chan error {
var out bool
return channelCallRPC(s1, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: fmt.Sprintf(dataPrefix+"%d", i),
},
}, &out, func() error {
if !out {
return fmt.Errorf("[%d] unexpectedly returned false", i)
}
return nil
})
},
)
}
{ // create one unrelated entry
var out bool
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "unrelated",
},
}, &out))
require.True(t, out)
}
runStep(t, "test the errNotFound path", func(t *testing.T) {
run(t, "other")
})
{ // create one relevant entry
var out bool
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "bar",
Protocol: "grpc",
},
}, &out))
require.True(t, out)
}
runStep(t, "test the errNotChanged path", func(t *testing.T) {
run(t, "completely-different-other")
})
}
func TestConfigEntry_ResolveServiceConfigNoConfig(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")

View File

@ -8,6 +8,7 @@ import (
"github.com/armon/go-metrics"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/consul/agent/structs"
)
@ -91,6 +92,7 @@ func (s *Server) reconcileLocalConfig(ctx context.Context, configs []structs.Con
ticker := time.NewTicker(time.Second / time.Duration(s.config.ConfigReplicationApplyLimit))
defer ticker.Stop()
var merr error
for i, entry := range configs {
// Exported services only apply to the primary datacenter.
if entry.GetKind() == structs.ExportedServices {
@ -104,7 +106,7 @@ func (s *Server) reconcileLocalConfig(ctx context.Context, configs []structs.Con
_, err := s.raftApply(structs.ConfigEntryRequestType, &req)
if err != nil {
return false, fmt.Errorf("Failed to apply config %s: %v", op, err)
merr = multierror.Append(merr, fmt.Errorf("Failed to apply config entry %s: %w", op, err))
}
if i < len(configs)-1 {
@ -117,7 +119,7 @@ func (s *Server) reconcileLocalConfig(ctx context.Context, configs []structs.Con
}
}
return false, nil
return false, merr
}
func (s *Server) fetchConfigEntries(lastRemoteIndex uint64) (*structs.IndexedGenericConfigEntries, error) {
@ -204,6 +206,7 @@ func (s *Server) replicateConfig(ctx context.Context, lastRemoteIndex uint64, lo
"updates", len(updates),
)
var merr error
if len(deletions) > 0 {
logger.Debug("Deleting local config entries",
"deletions", len(deletions),
@ -214,9 +217,10 @@ func (s *Server) replicateConfig(ctx context.Context, lastRemoteIndex uint64, lo
return 0, true, nil
}
if err != nil {
return 0, false, fmt.Errorf("failed to delete local config entries: %v", err)
merr = multierror.Append(merr, err)
} else {
logger.Debug("Config Entry replication - finished deletions")
}
logger.Debug("Config Entry replication - finished deletions")
}
if len(updates) > 0 {
@ -228,9 +232,14 @@ func (s *Server) replicateConfig(ctx context.Context, lastRemoteIndex uint64, lo
return 0, true, nil
}
if err != nil {
return 0, false, fmt.Errorf("failed to update local config entries: %v", err)
merr = multierror.Append(merr, err)
} else {
logger.Debug("Config Entry replication - finished updates")
}
logger.Debug("Config Entry replication - finished updates")
}
if merr != nil {
return 0, false, merr
}
// Return the index we got back from the remote side, since we've synced

View File

@ -247,3 +247,100 @@ func TestReplication_ConfigEntries(t *testing.T) {
checkSame(r)
})
}
func TestReplication_ConfigEntries_GraphValidationErrorDuringReplication(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.PrimaryDatacenter = "dc1"
})
testrpc.WaitForLeader(t, s1.RPC, "dc1")
_, s2 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.PrimaryDatacenter = "dc1"
c.ConfigReplicationRate = 100
c.ConfigReplicationBurst = 100
c.ConfigReplicationApplyLimit = 1000000
})
testrpc.WaitForLeader(t, s2.RPC, "dc2")
// Create two entries that will replicate in the wrong order and not work.
entries := []structs.ConfigEntry{
&structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "foo",
Protocol: "http",
},
&structs.IngressGatewayConfigEntry{
Kind: structs.IngressGateway,
Name: "foo",
Listeners: []structs.IngressListener{
{
Port: 9191,
Protocol: "http",
Services: []structs.IngressService{
{
Name: "foo",
},
},
},
},
},
}
for _, entry := range entries {
arg := structs.ConfigEntryRequest{
Datacenter: "dc1",
Op: structs.ConfigEntryUpsert,
Entry: entry,
}
out := false
require.NoError(t, s1.RPC("ConfigEntry.Apply", &arg, &out))
}
// Try to join which should kick off replication.
joinWAN(t, s2, s1)
testrpc.WaitForLeader(t, s1.RPC, "dc1")
testrpc.WaitForLeader(t, s1.RPC, "dc2")
checkSame := func(t require.TestingT) error {
_, remote, err := s1.fsm.State().ConfigEntries(nil, structs.ReplicationEnterpriseMeta())
require.NoError(t, err)
_, local, err := s2.fsm.State().ConfigEntries(nil, structs.ReplicationEnterpriseMeta())
require.NoError(t, err)
require.Len(t, local, len(remote))
for i, entry := range remote {
require.Equal(t, entry.GetKind(), local[i].GetKind())
require.Equal(t, entry.GetName(), local[i].GetName())
// more validations
switch entry.GetKind() {
case structs.IngressGateway:
localGw, ok := local[i].(*structs.IngressGatewayConfigEntry)
require.True(t, ok)
remoteGw, ok := entry.(*structs.IngressGatewayConfigEntry)
require.True(t, ok)
require.Len(t, remoteGw.Listeners, 1)
require.Len(t, localGw.Listeners, 1)
require.Equal(t, remoteGw.Listeners[0].Protocol, localGw.Listeners[0].Protocol)
case structs.ServiceDefaults:
localSvc, ok := local[i].(*structs.ServiceConfigEntry)
require.True(t, ok)
remoteSvc, ok := entry.(*structs.ServiceConfigEntry)
require.True(t, ok)
require.Equal(t, remoteSvc.Protocol, localSvc.Protocol)
}
}
return nil
}
// Wait for the replica to converge.
retry.Run(t, func(r *retry.R) {
checkSame(r)
})
}

View File

@ -65,8 +65,8 @@ func (s *ConnectCA) ConfigurationGet(
if err != nil {
return err
}
if authz.OperatorWrite(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorWriteAllowed(nil); err != nil {
return err
}
state := s.srv.fsm.State()
@ -97,8 +97,8 @@ func (s *ConnectCA) ConfigurationSet(
if err != nil {
return err
}
if authz.OperatorWrite(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorWriteAllowed(nil); err != nil {
return err
}
return s.srv.caManager.UpdateConfiguration(args)
@ -175,8 +175,8 @@ func (s *ConnectCA) Sign(
if isService {
entMeta.Merge(serviceID.GetEnterpriseMeta())
entMeta.FillAuthzContext(&authzContext)
if authz.ServiceWrite(serviceID.Service, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(serviceID.Service, &authzContext); err != nil {
return err
}
// Verify that the DC in the service URI matches us. We might relax this
@ -187,8 +187,8 @@ func (s *ConnectCA) Sign(
}
} else if isAgent {
agentID.GetEnterpriseMeta().FillAuthzContext(&authzContext)
if authz.NodeWrite(agentID.Agent, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(agentID.Agent, &authzContext); err != nil {
return err
}
}
@ -223,8 +223,8 @@ func (s *ConnectCA) SignIntermediate(
if err != nil {
return err
}
if authz.OperatorWrite(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorWriteAllowed(nil); err != nil {
return err
}
provider, _ := s.srv.caManager.getCAProvider()

View File

@ -152,8 +152,8 @@ func (c *Coordinate) Update(args *structs.CoordinateUpdateRequest, reply *struct
return err
}
if authz.NodeWrite(args.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(args.Node, &authzContext); err != nil {
return err
}
// Add the coordinate to the map of pending updates.
@ -245,8 +245,8 @@ func (c *Coordinate) Node(args *structs.NodeSpecificRequest, reply *structs.Inde
return err
}
if authz.NodeRead(args.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().NodeReadAllowed(args.Node, &authzContext); err != nil {
return err
}
return c.srv.blockingQuery(&args.QueryOptions,

View File

@ -6,6 +6,7 @@ import (
metrics "github.com/armon/go-metrics"
memdb "github.com/hashicorp/go-memdb"
hashstructure_v2 "github.com/mitchellh/hashstructure/v2"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/discoverychain"
@ -35,8 +36,8 @@ func (c *DiscoveryChain) Get(args *structs.DiscoveryChainRequest, reply *structs
if err != nil {
return err
}
if authz.ServiceRead(args.Name, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(args.Name, &authzContext); err != nil {
return err
}
if args.Name == "" {
@ -48,6 +49,10 @@ func (c *DiscoveryChain) Get(args *structs.DiscoveryChainRequest, reply *structs
evalDC = c.srv.config.Datacenter
}
var (
priorHash uint64
ranOnce bool
)
return c.srv.blockingQuery(
&args.QueryOptions,
&reply.QueryMeta,
@ -61,14 +66,37 @@ func (c *DiscoveryChain) Get(args *structs.DiscoveryChainRequest, reply *structs
OverrideProtocol: args.OverrideProtocol,
OverrideConnectTimeout: args.OverrideConnectTimeout,
}
index, chain, err := state.ServiceDiscoveryChain(ws, args.Name, entMeta, req)
index, chain, entries, err := state.ServiceDiscoveryChain(ws, args.Name, entMeta, req)
if err != nil {
return err
}
// Generate a hash of the config entry content driving this
// response. Use it to determine if the response is identical to a
// prior wakeup.
newHash, err := hashstructure_v2.Hash(chain, hashstructure_v2.FormatV2, nil)
if err != nil {
return fmt.Errorf("error hashing reply for spurious wakeup suppression: %w", err)
}
if ranOnce && priorHash == newHash {
priorHash = newHash
reply.Index = index
// NOTE: the prior response is still alive inside of *reply, which
// is desirable
return errNotChanged
} else {
priorHash = newHash
ranOnce = true
}
reply.Index = index
reply.Chain = chain
if entries.IsEmpty() {
return errNotFound
}
return nil
})
}

View File

@ -242,3 +242,88 @@ func TestDiscoveryChainEndpoint_Get(t *testing.T) {
require.Equal(t, expect, resp)
}
}
func TestDiscoveryChainEndpoint_Get_BlockOnNoChange(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.DevMode = true // keep it in ram to make it 10x faster on macos
c.PrimaryDatacenter = "dc1"
})
codec := rpcClient(t, s1)
waitForLeaderEstablishment(t, s1)
testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
{ // create one unrelated entry
var out bool
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Datacenter: "dc1",
Entry: &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "unrelated",
ConnectTimeout: 33 * time.Second,
},
}, &out))
require.True(t, out)
}
run := func(t *testing.T, dataPrefix string) {
rpcBlockingQueryTestHarness(t,
func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error) {
args := &structs.DiscoveryChainRequest{
Name: "web",
EvaluateInDatacenter: "dc1",
EvaluateInNamespace: "default",
EvaluateInPartition: "default",
Datacenter: "dc1",
}
args.QueryOptions.MinQueryIndex = minQueryIndex
var out structs.DiscoveryChainResponse
errCh := channelCallRPC(s1, "DiscoveryChain.Get", &args, &out, func() error {
if !out.Chain.IsDefault() {
return fmt.Errorf("expected default chain")
}
return nil
})
return &out.QueryMeta, errCh
},
func(i int) <-chan error {
var out bool
return channelCallRPC(s1, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Datacenter: "dc1",
Entry: &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: fmt.Sprintf(dataPrefix+"%d", i),
},
}, &out, nil)
},
)
}
runStep(t, "test the errNotFound path", func(t *testing.T) {
run(t, "other")
})
{ // create one relevant entry
var out bool
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "web",
Protocol: "grpc",
},
}, &out))
require.True(t, out)
}
runStep(t, "test the errNotChanged path", func(t *testing.T) {
run(t, "completely-different-other")
})
}

View File

@ -9,7 +9,6 @@ import (
"github.com/armon/go-metrics/prometheus"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
)
@ -63,8 +62,8 @@ func (c *FederationState) Apply(args *structs.FederationStateRequest, reply *boo
if err != nil {
return err
}
if authz.OperatorWrite(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorWriteAllowed(nil); err != nil {
return err
}
if args.State == nil || args.State.Datacenter == "" {
@ -109,8 +108,8 @@ func (c *FederationState) Get(args *structs.FederationStateQuery, reply *structs
if err != nil {
return err
}
if authz.OperatorRead(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorReadAllowed(nil); err != nil {
return err
}
return c.srv.blockingQuery(
@ -148,8 +147,8 @@ func (c *FederationState) List(args *structs.DCSpecificRequest, reply *structs.I
if err != nil {
return err
}
if authz.OperatorRead(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorReadAllowed(nil); err != nil {
return err
}
return c.srv.blockingQuery(

View File

@ -220,6 +220,7 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
// If we're doing a connect or ingress query, we need read access to the service
// we're trying to find proxies for, so check that.
if args.Connect || args.Ingress {
// TODO(acl-error-enhancements) Look for ways to percolate this information up to give any feedback to the user.
if authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
// Just return nil, which will return an empty response (tested)
return nil

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
hashstructure_v2 "github.com/mitchellh/hashstructure/v2"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state"
@ -609,14 +610,20 @@ func (s *Intention) Match(args *structs.IntentionQueryRequest, reply *structs.In
// matching, if you have it on the dest then perform a dest type match.
for _, entry := range args.Match.Entries {
entry.FillAuthzContext(&authzContext)
if prefix := entry.Name; prefix != "" && authz.IntentionRead(prefix, &authzContext) != acl.Allow {
accessorID := authz.AccessorID()
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Operation on intention prefix denied due to ACLs", "prefix", prefix, "accessorID", accessorID)
return acl.ErrPermissionDenied
if prefix := entry.Name; prefix != "" {
if err := authz.ToAllowAuthorizer().IntentionReadAllowed(prefix, &authzContext); err != nil {
accessorID := authz.AccessorID()
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("Operation on intention prefix denied due to ACLs", "prefix", prefix, "accessorID", accessorID)
return err
}
}
}
var (
priorHash uint64
ranOnce bool
)
return s.srv.blockingQuery(
&args.QueryOptions,
&reply.QueryMeta,
@ -628,6 +635,35 @@ func (s *Intention) Match(args *structs.IntentionQueryRequest, reply *structs.In
reply.Index = index
reply.Matches = matches
// Generate a hash of the intentions content driving this response.
// Use it to determine if the response is identical to a prior
// wakeup.
newHash, err := hashstructure_v2.Hash(matches, hashstructure_v2.FormatV2, nil)
if err != nil {
return fmt.Errorf("error hashing reply for spurious wakeup suppression: %w", err)
}
if ranOnce && priorHash == newHash {
priorHash = newHash
return errNotChanged
} else {
priorHash = newHash
ranOnce = true
}
hasData := false
for _, match := range matches {
if len(match) > 0 {
hasData = true
break
}
}
if !hasData {
return errNotFound
}
return nil
},
)
@ -699,11 +735,11 @@ func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.In
if prefix, ok := query.GetACLPrefix(); ok {
var authzContext acl.AuthorizerContext
query.FillAuthzContext(&authzContext)
if authz.ServiceRead(prefix, &authzContext) != acl.Allow {
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(prefix, &authzContext); err != nil {
accessorID := authz.AccessorID()
// todo(kit) Migrate intention access denial logging over to audit logging when we implement it
s.logger.Warn("test on intention denied due to ACLs", "prefix", prefix, "accessorID", accessorID)
return acl.ErrPermissionDenied
return err
}
}

View File

@ -1742,6 +1742,98 @@ func TestIntentionMatch_good(t *testing.T) {
require.Equal(t, expected, actual)
}
func TestIntentionMatch_BlockOnNoChange(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.DevMode = true // keep it in ram to make it 10x faster on macos
})
codec := rpcClient(t, s1)
waitForLeaderEstablishment(t, s1)
run := func(t *testing.T, dataPrefix string, expectMatches int) {
rpcBlockingQueryTestHarness(t,
func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error) {
args := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Match: &structs.IntentionQueryMatch{
Type: structs.IntentionMatchDestination,
Entries: []structs.IntentionMatchEntry{
{Name: "bar"},
},
},
}
args.QueryOptions.MinQueryIndex = minQueryIndex
var out structs.IndexedIntentionMatches
errCh := channelCallRPC(s1, "Intention.Match", args, &out, func() error {
if len(out.Matches) != 1 {
return fmt.Errorf("expected 1 match got %d", len(out.Matches))
}
if len(out.Matches[0]) != expectMatches {
return fmt.Errorf("expected %d inner matches got %d", expectMatches, len(out.Matches[0]))
}
return nil
})
return &out.QueryMeta, errCh
},
func(i int) <-chan error {
var out string
return channelCallRPC(s1, "Intention.Apply", &structs.IntentionRequest{
Datacenter: "dc1",
Op: structs.IntentionOpCreate,
Intention: &structs.Intention{
// {"default", "*", "default", "baz"}, // shouldn't match
SourceNS: "default",
SourceName: "*",
DestinationNS: "default",
DestinationName: fmt.Sprintf(dataPrefix+"%d", i),
Action: structs.IntentionActionAllow,
},
}, &out, nil)
},
)
}
runStep(t, "test the errNotFound path", func(t *testing.T) {
run(t, "other", 0)
})
// Create some records
{
insert := [][]string{
{"default", "*", "default", "*"},
{"default", "*", "default", "bar"},
{"default", "*", "default", "baz"}, // shouldn't match
}
for _, v := range insert {
var out string
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &structs.IntentionRequest{
Datacenter: "dc1",
Op: structs.IntentionOpCreate,
Intention: &structs.Intention{
SourceNS: v[0],
SourceName: v[1],
DestinationNS: v[2],
DestinationName: v[3],
Action: structs.IntentionActionAllow,
},
}, &out))
}
}
runStep(t, "test the errNotChanged path", func(t *testing.T) {
run(t, "completely-different-other", 2)
})
}
// Test matching with ACLs
func TestIntentionMatch_acl(t *testing.T) {
if testing.Short() {

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/serf/serf"
hashstructure_v2 "github.com/mitchellh/hashstructure/v2"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state"
@ -162,8 +163,8 @@ func (m *Internal) ServiceTopology(args *structs.ServiceSpecificRequest, reply *
if err := m.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil {
return err
}
if authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(args.ServiceName, &authzContext); err != nil {
return err
}
return m.srv.blockingQuery(
@ -210,6 +211,10 @@ func (m *Internal) IntentionUpstreams(args *structs.ServiceSpecificRequest, repl
return err
}
var (
priorHash uint64
ranOnce bool
)
return m.srv.blockingQuery(
&args.QueryOptions,
&reply.QueryMeta,
@ -224,6 +229,23 @@ func (m *Internal) IntentionUpstreams(args *structs.ServiceSpecificRequest, repl
reply.Index, reply.Services = index, services
m.srv.filterACLWithAuthorizer(authz, reply)
// Generate a hash of the intentions content driving this response.
// Use it to determine if the response is identical to a prior
// wakeup.
newHash, err := hashstructure_v2.Hash(services, hashstructure_v2.FormatV2, nil)
if err != nil {
return fmt.Errorf("error hashing reply for spurious wakeup suppression: %w", err)
}
if ranOnce && priorHash == newHash {
priorHash = newHash
return errNotChanged
} else {
priorHash = newHash
ranOnce = true
}
return nil
})
}
@ -250,8 +272,8 @@ func (m *Internal) GatewayServiceDump(args *structs.ServiceSpecificRequest, repl
}
// We need read access to the gateway we're trying to find services for, so check that first.
if authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(args.ServiceName, &authzContext); err != nil {
return err
}
err = m.srv.blockingQuery(
@ -334,8 +356,8 @@ func (m *Internal) GatewayIntentions(args *structs.IntentionQueryRequest, reply
}
// We need read access to the gateway we're trying to find intentions for, so check that first.
if authz.ServiceRead(args.Match.Entries[0].Name, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(args.Match.Entries[0].Name, &authzContext); err != nil {
return err
}
return m.srv.blockingQuery(
@ -406,10 +428,10 @@ func (m *Internal) EventFire(args *structs.EventFireRequest,
return err
}
if authz.EventWrite(args.Name, nil) != acl.Allow {
if err := authz.ToAllowAuthorizer().EventWriteAllowed(args.Name, nil); err != nil {
accessorID := authz.AccessorID()
m.logger.Warn("user event blocked by ACLs", "event", args.Name, "accessorID", accessorID)
return acl.ErrPermissionDenied
return err
}
// Set the query meta data
@ -442,16 +464,16 @@ func (m *Internal) KeyringOperation(
}
switch args.Operation {
case structs.KeyringList:
if authz.KeyringRead(nil) != acl.Allow {
return fmt.Errorf("Reading keyring denied by ACLs")
if err := authz.ToAllowAuthorizer().KeyringReadAllowed(nil); err != nil {
return err
}
case structs.KeyringInstall:
fallthrough
case structs.KeyringUse:
fallthrough
case structs.KeyringRemove:
if authz.KeyringWrite(nil) != acl.Allow {
return fmt.Errorf("Modifying keyring denied due to ACLs")
if err := authz.ToAllowAuthorizer().KeyringWriteAllowed(nil); err != nil {
return err
}
default:
panic("Invalid keyring operation")

View File

@ -2,6 +2,7 @@ package consul
import (
"encoding/base64"
"fmt"
"os"
"strings"
"testing"
@ -2317,6 +2318,89 @@ func TestInternal_IntentionUpstreams(t *testing.T) {
})
}
func TestInternal_IntentionUpstreams_BlockOnNoChange(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.DevMode = true // keep it in ram to make it 10x faster on macos
})
codec := rpcClient(t, s1)
waitForLeaderEstablishment(t, s1)
{ // ensure it's default deny to start
var out bool
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &structs.ConfigEntryRequest{
Entry: &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: "*",
Sources: []*structs.SourceIntention{
{
Name: "*",
Action: structs.IntentionActionDeny,
},
},
},
}, &out))
require.True(t, out)
}
run := func(t *testing.T, dataPrefix string, expectServices int) {
rpcBlockingQueryTestHarness(t,
func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error) {
args := &structs.ServiceSpecificRequest{
Datacenter: "dc1",
ServiceName: "web",
}
args.QueryOptions.MinQueryIndex = minQueryIndex
var out structs.IndexedServiceList
errCh := channelCallRPC(s1, "Internal.IntentionUpstreams", args, &out, func() error {
if len(out.Services) != expectServices {
return fmt.Errorf("expected %d services got %d", expectServices, len(out.Services))
}
return nil
})
return &out.QueryMeta, errCh
},
func(i int) <-chan error {
var out string
return channelCallRPC(s1, "Intention.Apply", &structs.IntentionRequest{
Datacenter: "dc1",
Op: structs.IntentionOpCreate,
Intention: &structs.Intention{
SourceName: fmt.Sprintf(dataPrefix+"-src-%d", i),
DestinationName: fmt.Sprintf(dataPrefix+"-dst-%d", i),
Action: structs.IntentionActionAllow,
},
}, &out, nil)
},
)
}
runStep(t, "test the errNotFound path", func(t *testing.T) {
run(t, "other", 0)
})
// Services:
// api and api-proxy on node foo
// web and web-proxy on node foo
//
// Intentions
// * -> * (deny) intention
// web -> api (allow)
registerIntentionUpstreamEntries(t, codec, "")
runStep(t, "test the errNotChanged path", func(t *testing.T) {
run(t, "completely-different-other", 1)
})
}
func TestInternal_IntentionUpstreams_ACL(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")

View File

@ -44,8 +44,8 @@ func kvsPreApply(logger hclog.Logger, srv *Server, authz acl.Authorizer, op api.
var authzContext acl.AuthorizerContext
dirEnt.FillAuthzContext(&authzContext)
if authz.KeyWritePrefix(dirEnt.Key, &authzContext) != acl.Allow {
return false, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().KeyWritePrefixAllowed(dirEnt.Key, &authzContext); err != nil {
return false, err
}
case api.KVGet, api.KVGetTree:
@ -58,16 +58,16 @@ func kvsPreApply(logger hclog.Logger, srv *Server, authz acl.Authorizer, op api.
var authzContext acl.AuthorizerContext
dirEnt.FillAuthzContext(&authzContext)
if authz.KeyRead(dirEnt.Key, &authzContext) != acl.Allow {
return false, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().KeyReadAllowed(dirEnt.Key, &authzContext); err != nil {
return false, err
}
default:
var authzContext acl.AuthorizerContext
dirEnt.FillAuthzContext(&authzContext)
if authz.KeyWrite(dirEnt.Key, &authzContext) != acl.Allow {
return false, acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().KeyWriteAllowed(dirEnt.Key, &authzContext); err != nil {
return false, err
}
}
@ -155,8 +155,8 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
if err != nil {
return err
}
if authz.KeyRead(args.Key, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().KeyReadAllowed(args.Key, &authzContext); err != nil {
return err
}
if ent == nil {
@ -187,8 +187,10 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
return err
}
if k.srv.config.ACLEnableKeyListPolicy && authz.KeyList(args.Key, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if k.srv.config.ACLEnableKeyListPolicy {
if err := authz.ToAllowAuthorizer().KeyListAllowed(args.Key, &authzContext); err != nil {
return err
}
}
return k.srv.blockingQuery(
@ -240,8 +242,10 @@ func (k *KVS) ListKeys(args *structs.KeyListRequest, reply *structs.IndexedKeyLi
return err
}
if k.srv.config.ACLEnableKeyListPolicy && authz.KeyList(args.Prefix, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if k.srv.config.ACLEnableKeyListPolicy {
if err := authz.ToAllowAuthorizer().KeyListAllowed(args.Prefix, &authzContext); err != nil {
return err
}
}
return k.srv.blockingQuery(

View File

@ -6,6 +6,7 @@ import (
"net"
"reflect"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@ -899,7 +900,7 @@ func (s *Server) reconcileReaped(known map[string]struct{}, nodeEntMeta *structs
}
// Check if this node is "known" by serf
if _, ok := known[check.Node]; ok {
if _, ok := known[strings.ToLower(check.Node)]; ok {
continue
}
@ -1204,7 +1205,7 @@ func (s *Server) handleDeregisterMember(reason string, member serf.Member, nodeE
// deregister us later.
//
// TODO(partitions): check partitions here too? server names should be unique in general though
if member.Name == s.config.NodeName {
if strings.EqualFold(member.Name, s.config.NodeName) {
s.logger.Warn("deregistering self should be done by follower",
"name", s.config.NodeName,
"partition", getSerfMemberEnterpriseMeta(member).PartitionOrDefault(),

View File

@ -693,7 +693,7 @@ func (c *CAManager) persistNewRootAndConfig(provider ca.Provider, newActiveRoot
return fmt.Errorf("local CA not initialized yet")
}
// Exit early if the change is a no-op.
if newActiveRoot == nil && config != nil && config.Provider == storedConfig.Provider && reflect.DeepEqual(config.Config, storedConfig.Config) {
if !shouldPersistNewRootAndConfig(newActiveRoot, storedConfig, config) {
return nil
}
@ -758,6 +758,17 @@ func (c *CAManager) persistNewRootAndConfig(provider ca.Provider, newActiveRoot
return nil
}
func shouldPersistNewRootAndConfig(newActiveRoot *structs.CARoot, oldConfig, newConfig *structs.CAConfiguration) bool {
if newActiveRoot != nil {
return true
}
if newConfig == nil {
return false
}
return newConfig.Provider == oldConfig.Provider && reflect.DeepEqual(newConfig.Config, oldConfig.Config)
}
func (c *CAManager) UpdateConfiguration(args *structs.CARequest) (reterr error) {
// Attempt to update the state first.
oldState, err := c.setState(caStateReconfig, true)

View File

@ -693,6 +693,62 @@ func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) {
})
}
func TestCAManager_Verify_Vault_NoChangeToSecondaryConfig(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
ca.SkipIfVaultNotPresent(t)
vault := ca.NewTestVaultServer(t)
_, sDC1 := testServerWithConfig(t, func(c *Config) {
c.CAConfig = &structs.CAConfiguration{
Provider: "vault",
Config: map[string]interface{}{
"Address": vault.Addr,
"Token": vault.RootToken,
"RootPKIPath": "pki-root/",
"IntermediatePKIPath": "pki-intermediate/",
},
}
})
defer sDC1.Shutdown()
testrpc.WaitForActiveCARoot(t, sDC1.RPC, "dc1", nil)
_, sDC2 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.PrimaryDatacenter = "dc1"
c.CAConfig = &structs.CAConfiguration{
Provider: "vault",
Config: map[string]interface{}{
"Address": vault.Addr,
"Token": vault.RootToken,
"RootPKIPath": "pki-root/",
"IntermediatePKIPath": "pki-intermediate-2/",
},
}
})
defer sDC2.Shutdown()
joinWAN(t, sDC2, sDC1)
testrpc.WaitForActiveCARoot(t, sDC2.RPC, "dc2", nil)
codec := rpcClient(t, sDC2)
var configBefore structs.CAConfiguration
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", &structs.DCSpecificRequest{}, &configBefore)
require.NoError(t, err)
renewLeafSigningCert(t, sDC1.caManager, sDC1.caManager.primaryRenewIntermediate)
// Give the secondary some time to notice the update
time.Sleep(100 * time.Millisecond)
var configAfter structs.CAConfiguration
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", &structs.DCSpecificRequest{}, &configAfter)
require.NoError(t, err)
require.EqualValues(t, configBefore.ModifyIndex, configAfter.ModifyIndex)
}
func getLeafCert(t *testing.T, codec rpc.ClientCodec, trustDomain string, dc string) string {
pk, _, err := connect.GeneratePrivateKey()
require.NoError(t, err)

View File

@ -1490,8 +1490,6 @@ func TestCAManager_Initialize_Vault_BadCAConfigDoesNotPreventLeaderEstablishment
}
func TestCAManager_Initialize_BadCAConfigDoesNotPreventLeaderEstablishment(t *testing.T) {
ca.SkipIfVaultNotPresent(t)
_, s1 := testServerWithConfig(t, func(c *Config) {
c.Build = "1.9.1"
c.PrimaryDatacenter = "dc1"

View File

@ -156,6 +156,9 @@ func TestLeader_FailedMember(t *testing.T) {
if err != nil {
r.Fatalf("err: %v", err)
}
if len(checks) != 1 {
r.Fatalf("client missing check")
}
if got, want := checks[0].Status, api.HealthCritical; got != want {
r.Fatalf("got status %q want %q", got, want)
}
@ -189,12 +192,8 @@ func TestLeader_LeftMember(t *testing.T) {
// Should be registered
retry.Run(t, func(r *retry.R) {
_, node, err := state.GetNode(c1.config.NodeName, nil)
if err != nil {
r.Fatalf("err: %v", err)
}
if node == nil {
r.Fatal("client not registered")
}
require.NoError(r, err)
require.NotNil(r, node, "client not registered")
})
// Node should leave
@ -204,14 +203,11 @@ func TestLeader_LeftMember(t *testing.T) {
// Should be deregistered
retry.Run(t, func(r *retry.R) {
_, node, err := state.GetNode(c1.config.NodeName, nil)
if err != nil {
r.Fatalf("err: %v", err)
}
if node != nil {
r.Fatal("client still registered")
}
require.NoError(r, err)
require.Nil(r, node, "client still registered")
})
}
func TestLeader_ReapMember(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
@ -239,12 +235,8 @@ func TestLeader_ReapMember(t *testing.T) {
// Should be registered
retry.Run(t, func(r *retry.R) {
_, node, err := state.GetNode(c1.config.NodeName, nil)
if err != nil {
r.Fatalf("err: %v", err)
}
if node == nil {
r.Fatal("client not registered")
}
require.NoError(r, err)
require.NotNil(r, node, "client not registered")
})
// Simulate a node reaping
@ -264,9 +256,7 @@ func TestLeader_ReapMember(t *testing.T) {
reaped := false
for start := time.Now(); time.Since(start) < 5*time.Second; {
_, node, err := state.GetNode(c1.config.NodeName, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, err)
if node == nil {
reaped = true
break
@ -277,6 +267,88 @@ func TestLeader_ReapMember(t *testing.T) {
}
}
func TestLeader_ReapOrLeftMember_IgnoreSelf(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
run := func(t *testing.T, status serf.MemberStatus, nameFn func(string) string) {
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.PrimaryDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLInitialManagementToken = "root"
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
nodeName := s1.config.NodeName
if nameFn != nil {
nodeName = nameFn(nodeName)
}
state := s1.fsm.State()
// Should be registered
retry.Run(t, func(r *retry.R) {
_, node, err := state.GetNode(nodeName, nil)
require.NoError(r, err)
require.NotNil(r, node, "server not registered")
})
// Simulate THIS node reaping or leaving
mems := s1.LANMembersInAgentPartition()
var s1mem serf.Member
for _, m := range mems {
if strings.EqualFold(m.Name, nodeName) {
s1mem = m
s1mem.Status = status
s1mem.Name = nodeName
break
}
}
s1.reconcileCh <- s1mem
// Should NOT be deregistered; we have to poll quickly here because
// anti-entropy will put it back if it did get deleted.
reaped := false
for start := time.Now(); time.Since(start) < 5*time.Second; {
_, node, err := state.GetNode(nodeName, nil)
require.NoError(t, err)
if node == nil {
reaped = true
break
}
}
if reaped {
t.Fatalf("server should still be registered")
}
}
t.Run("original name", func(t *testing.T) {
t.Parallel()
t.Run("left", func(t *testing.T) {
run(t, serf.StatusLeft, nil)
})
t.Run("reap", func(t *testing.T) {
run(t, StatusReap, nil)
})
})
t.Run("uppercased name", func(t *testing.T) {
t.Parallel()
t.Run("left", func(t *testing.T) {
run(t, serf.StatusLeft, strings.ToUpper)
})
t.Run("reap", func(t *testing.T) {
run(t, StatusReap, strings.ToUpper)
})
})
}
func TestLeader_CheckServersMeta(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
@ -637,6 +709,9 @@ func TestLeader_Reconcile_Races(t *testing.T) {
if err != nil {
r.Fatalf("err: %v", err)
}
if len(checks) != 1 {
r.Fatalf("client missing check")
}
if got, want := checks[0].Status, api.HealthCritical; got != want {
r.Fatalf("got state %q want %q", got, want)
}

View File

@ -2,6 +2,7 @@ package consul
import (
"fmt"
"strings"
"sync"
"github.com/hashicorp/go-version"
@ -38,7 +39,7 @@ func (md *lanMergeDelegate) NotifyMerge(members []*serf.Member) error {
nodeID := types.NodeID(rawID)
// See if there's another node that conflicts with us.
if (nodeID == md.nodeID) && (m.Name != md.nodeName) {
if (nodeID == md.nodeID) && !strings.EqualFold(m.Name, md.nodeName) {
return fmt.Errorf("Member '%s' has conflicting node ID '%s' with this agent's ID",
m.Name, nodeID)
}

View File

@ -58,6 +58,30 @@ func TestMerge_LAN(t *testing.T) {
},
expect: "wrong datacenter",
},
"node ID conflict with delegate's ID but same node name with same casing": {
members: []*serf.Member{
makeTestNode(t, testMember{
dc: "dc1",
name: "node0",
id: thisNodeID,
server: true,
build: "0.7.5",
}),
},
expect: "",
},
"node ID conflict with delegate's ID but same node name with different casing": {
members: []*serf.Member{
makeTestNode(t, testMember{
dc: "dc1",
name: "NoDe0",
id: thisNodeID,
server: true,
build: "0.7.5",
}),
},
expect: "",
},
"node ID conflict with delegate's ID": {
members: []*serf.Member{
makeTestNode(t, testMember{

View File

@ -2,11 +2,9 @@ package consul
import (
"fmt"
autopilot "github.com/hashicorp/raft-autopilot"
"github.com/hashicorp/serf/serf"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
)
@ -24,8 +22,9 @@ func (op *Operator) AutopilotGetConfiguration(args *structs.DCSpecificRequest, r
if err := op.srv.validateEnterpriseToken(authz.Identity()); err != nil {
return err
}
if authz.OperatorRead(nil) != acl.Allow {
return acl.PermissionDeniedByACLUnnamed(authz, nil, acl.ResourceOperator, acl.AccessRead)
if err := authz.ToAllowAuthorizer().OperatorReadAllowed(nil); err != nil {
return err
}
state := op.srv.fsm.State()
@ -56,8 +55,9 @@ func (op *Operator) AutopilotSetConfiguration(args *structs.AutopilotSetConfigRe
if err := op.srv.validateEnterpriseToken(authz.Identity()); err != nil {
return err
}
if authz.OperatorWrite(nil) != acl.Allow {
return acl.PermissionDeniedByACLUnnamed(authz, nil, acl.ResourceOperator, acl.AccessWrite)
if err := authz.ToAllowAuthorizer().OperatorWriteAllowed(nil); err != nil {
return err
}
// Apply the update
@ -91,8 +91,9 @@ func (op *Operator) ServerHealth(args *structs.DCSpecificRequest, reply *structs
if err := op.srv.validateEnterpriseToken(authz.Identity()); err != nil {
return err
}
if authz.OperatorRead(nil) != acl.Allow {
return acl.PermissionDeniedByACLUnnamed(authz, nil, acl.ResourceOperator, acl.AccessRead)
if err := authz.ToAllowAuthorizer().OperatorReadAllowed(nil); err != nil {
return err
}
state := op.srv.autopilot.GetState()
@ -158,8 +159,9 @@ func (op *Operator) AutopilotState(args *structs.DCSpecificRequest, reply *autop
if err := op.srv.validateEnterpriseToken(authz.Identity()); err != nil {
return err
}
if authz.OperatorRead(nil) != acl.Allow {
return acl.PermissionDeniedByACLUnnamed(authz, nil, acl.ResourceOperator, acl.AccessRead)
if err := authz.ToAllowAuthorizer().OperatorReadAllowed(nil); err != nil {
return err
}
state := op.srv.autopilot.GetState()

View File

@ -7,7 +7,6 @@ import (
"github.com/hashicorp/raft"
"github.com/hashicorp/serf/serf"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/metadata"
"github.com/hashicorp/consul/agent/structs"
)
@ -23,8 +22,8 @@ func (op *Operator) RaftGetConfiguration(args *structs.DCSpecificRequest, reply
if err != nil {
return err
}
if authz.OperatorRead(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorReadAllowed(nil); err != nil {
return err
}
// We can't fetch the leader and the configuration atomically with
@ -88,8 +87,8 @@ func (op *Operator) RaftRemovePeerByAddress(args *structs.RaftRemovePeerRequest,
if err := op.srv.validateEnterpriseToken(authz.Identity()); err != nil {
return err
}
if authz.OperatorWrite(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorWriteAllowed(nil); err != nil {
return err
}
// Since this is an operation designed for humans to use, we will return
@ -141,8 +140,8 @@ func (op *Operator) RaftRemovePeerByID(args *structs.RaftRemovePeerRequest, repl
if err := op.srv.validateEnterpriseToken(authz.Identity()); err != nil {
return err
}
if authz.OperatorWrite(nil) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().OperatorWriteAllowed(nil); err != nil {
return err
}
// Since this is an operation designed for humans to use, we will return

View File

@ -86,9 +86,9 @@ func (p *PreparedQuery) Apply(args *structs.PreparedQueryRequest, reply *string)
// need to make sure they have write access for whatever they are
// proposing.
if prefix, ok := args.Query.GetACLPrefix(); ok {
if authz.PreparedQueryWrite(prefix, nil) != acl.Allow {
if err := authz.ToAllowAuthorizer().PreparedQueryWriteAllowed(prefix, nil); err != nil {
p.logger.Warn("Operation on prepared query denied due to ACLs", "query", args.Query.ID)
return acl.ErrPermissionDenied
return err
}
}
@ -106,9 +106,9 @@ func (p *PreparedQuery) Apply(args *structs.PreparedQueryRequest, reply *string)
}
if prefix, ok := query.GetACLPrefix(); ok {
if authz.PreparedQueryWrite(prefix, nil) != acl.Allow {
if err := authz.ToAllowAuthorizer().PreparedQueryWriteAllowed(prefix, nil); err != nil {
p.logger.Warn("Operation on prepared query denied due to ACLs", "query", args.Query.ID)
return acl.ErrPermissionDenied
return err
}
}
}
@ -439,7 +439,7 @@ func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest,
// position 0, provided the results are from the same datacenter.
if qs.Node != "" && reply.Datacenter == qs.Datacenter {
for i, node := range reply.Nodes {
if node.Node.Node == qs.Node {
if strings.EqualFold(node.Node.Node, qs.Node) {
reply.Nodes[0], reply.Nodes[i] = reply.Nodes[i], reply.Nodes[0]
break
}

View File

@ -954,6 +954,19 @@ type blockingQueryResponseMeta interface {
// a previous result. errNotFound will never be returned to the caller, it is
// converted to nil before returning.
//
// The query function can return errNotChanged, which is a sentinel error. This
// can only be returned on calls AFTER the first call, as it would not be
// possible to detect the absence of a change on the first call. Returning
// errNotChanged indicates that the query results are identical to the prior
// results which allows blockingQuery to keep blocking until the query returns
// a real changed result.
//
// The query function must take care to ensure the actual result of the query
// is either left unmodified or explicitly left in a good state before
// returning, otherwise when blockingQuery times out it may return an
// incomplete or unexpected result. errNotChanged will never be returned to the
// caller, it is converted to nil before returning.
//
// If query function returns any other error, the error is returned to the caller
// immediately.
//
@ -993,7 +1006,7 @@ func (s *Server) blockingQuery(
var ws memdb.WatchSet
err := query(ws, s.fsm.State())
s.setQueryMeta(responseMeta, opts.GetToken())
if errors.Is(err, errNotFound) {
if errors.Is(err, errNotFound) || errors.Is(err, errNotChanged) {
return nil
}
return err
@ -1008,7 +1021,10 @@ func (s *Server) blockingQuery(
// decrement the count when the function returns.
defer atomic.AddUint64(&s.queriesBlocking, ^uint64(0))
var notFound bool
var (
notFound bool
ranOnce bool
)
for {
if opts.GetRequireConsistent() {
@ -1029,17 +1045,23 @@ func (s *Server) blockingQuery(
err := query(ws, state)
s.setQueryMeta(responseMeta, opts.GetToken())
switch {
case errors.Is(err, errNotFound):
if notFound {
// query result has not changed
minQueryIndex = responseMeta.GetIndex()
}
notFound = true
case errors.Is(err, errNotChanged):
if ranOnce {
// query result has not changed
minQueryIndex = responseMeta.GetIndex()
}
case err != nil:
return err
}
ranOnce = true
if responseMeta.GetIndex() > minQueryIndex {
return nil
@ -1060,7 +1082,10 @@ func (s *Server) blockingQuery(
}
}
var errNotFound = fmt.Errorf("no data found for query")
var (
errNotFound = fmt.Errorf("no data found for query")
errNotChanged = fmt.Errorf("data did not change for query")
)
// setQueryMeta is used to populate the QueryMeta data for an RPC call
//

View File

@ -1681,3 +1681,104 @@ func getFirstSubscribeEventOrError(conn *grpc.ClientConn, req *pbsubscribe.Subsc
}
return event, nil
}
// channelCallRPC lets you execute an RPC async. Helpful in some
// tests.
func channelCallRPC(
srv *Server,
method string,
args interface{},
resp interface{},
responseInterceptor func() error,
) <-chan error {
errCh := make(chan error, 1)
go func() {
codec, err := rpcClientNoClose(srv)
if err != nil {
errCh <- err
return
}
defer codec.Close()
err = msgpackrpc.CallWithCodec(codec, method, args, resp)
if err == nil && responseInterceptor != nil {
err = responseInterceptor()
}
errCh <- err
}()
return errCh
}
// rpcBlockingQueryTestHarness is specifically meant to test the
// errNotFound and errNotChanged mechanisms in blockingQuery()
func rpcBlockingQueryTestHarness(
t *testing.T,
readQueryFn func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error),
noisyWriteFn func(i int) <-chan error,
) {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
launchWriters := func() {
defer cancel()
for i := 0; i < 200; i++ {
time.Sleep(5 * time.Millisecond)
errCh := noisyWriteFn(i)
select {
case <-ctx.Done():
return
case err := <-errCh:
if err != nil {
t.Errorf("[%d] unexpected error: %w", i, err)
return
}
}
}
}
var (
count int
minQueryIndex uint64
)
for ctx.Err() == nil {
// The first iteration is an orientation iteration, as we don't pass an
// index value so there is no actual blocking that will happen.
//
// Since the data is not changing, we don't expect the second iteration
// to return soon, so we wait a bit after kicking it off before
// launching the write-storm.
var timerCh <-chan time.Time
if count == 1 {
timerCh = time.After(50 * time.Millisecond)
}
qm, errCh := readQueryFn(minQueryIndex)
RESUME:
select {
case err := <-errCh:
if err != nil {
require.NoError(t, err)
}
t.Log("blocking query index", qm.Index)
count++
minQueryIndex = qm.Index
case <-timerCh:
timerCh = nil
go launchWriters()
goto RESUME
case <-ctx.Done():
break
}
}
require.Equal(t, 1, count, "if this fails, then the timer likely needs to be increased above")
}

View File

@ -822,7 +822,7 @@ func (s *Server) setupRaft() error {
// If we are in bootstrap or dev mode and the state is clean then we can
// bootstrap now.
if s.config.Bootstrap || s.config.DevMode {
if (s.config.Bootstrap || s.config.DevMode) && !s.config.ReadReplica {
hasState, err := raft.HasExistingState(log, stable, snap)
if err != nil {
return err

View File

@ -5,6 +5,7 @@ package consul
import (
"fmt"
"strings"
"time"
"github.com/armon/go-metrics"
@ -138,10 +139,11 @@ func (s *Server) reconcile() (err error) {
members := s.serfLAN.Members()
knownMembers := make(map[string]struct{})
for _, member := range members {
memberName := strings.ToLower(member.Name)
if err := s.reconcileMember(member); err != nil {
return err
}
knownMembers[member.Name] = struct{}{}
knownMembers[memberName] = struct{}{}
}
// Reconcile any members that have been reaped while we were not the

View File

@ -386,6 +386,11 @@ func (s *Server) maybeBootstrap() {
return
}
if s.config.ReadReplica {
s.logger.Info("Read replicas cannot bootstrap raft")
return
}
// Scan for all the known servers.
members := s.serfLAN.Members()
var servers []metadata.Server

View File

@ -82,13 +82,13 @@ func (s *Session) Apply(args *structs.SessionRequest, reply *string) error {
if existing == nil {
return nil
}
if authz.SessionWrite(existing.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().SessionWriteAllowed(existing.Node, &authzContext); err != nil {
return err
}
case structs.SessionCreate:
if authz.SessionWrite(args.Session.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().SessionWriteAllowed(args.Session.Node, &authzContext); err != nil {
return err
}
default:
@ -303,8 +303,8 @@ func (s *Session) Renew(args *structs.SessionSpecificRequest,
return nil
}
if authz.SessionWrite(session.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().SessionWriteAllowed(session.Node, &authzContext); err != nil {
return err
}
// Reset the session TTL timer.

View File

@ -18,7 +18,6 @@ import (
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/pool"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/snapshot"
@ -62,8 +61,8 @@ func (s *Server) dispatchSnapshotRequest(args *structs.SnapshotRequest, in io.Re
// all the ACLs and you could escalate from there.
if authz, err := s.ResolveToken(args.Token); err != nil {
return nil, err
} else if authz.Snapshot(nil) != acl.Allow {
return nil, acl.ErrPermissionDenied
} else if err := authz.ToAllowAuthorizer().SnapshotAllowed(nil); err != nil {
return nil, err
}
// Dispatch the operation.

View File

@ -136,7 +136,7 @@ func (s *Store) ensureCheckIfNodeMatches(
nodePartition string,
check *structs.HealthCheck,
) error {
if check.Node != node || !structs.EqualPartitions(nodePartition, check.PartitionOrDefault()) {
if !strings.EqualFold(check.Node, node) || !structs.EqualPartitions(nodePartition, check.PartitionOrDefault()) {
return fmt.Errorf("check node %q does not match node %q",
printNodeName(check.Node, check.PartitionOrDefault()),
printNodeName(node, nodePartition),
@ -330,7 +330,7 @@ func (s *Store) ensureNodeTxn(tx WriteTxn, idx uint64, preserveIndexes bool, nod
}
if existing != nil {
n = existing
if n.Node != node.Node {
if !strings.EqualFold(n.Node, node.Node) {
// Lets first get all nodes and check whether name do match, we do not allow clash on nodes without ID
dupNameError := ensureNoNodeWithSimilarNameTxn(tx, node, false)
if dupNameError != nil {

View File

@ -105,7 +105,7 @@ type nodeServiceTuple struct {
func newNodeServiceTupleFromServiceNode(sn *structs.ServiceNode) nodeServiceTuple {
return nodeServiceTuple{
Node: sn.Node,
Node: strings.ToLower(sn.Node),
ServiceID: sn.ServiceID,
EntMeta: sn.EnterpriseMeta,
}
@ -113,7 +113,7 @@ func newNodeServiceTupleFromServiceNode(sn *structs.ServiceNode) nodeServiceTupl
func newNodeServiceTupleFromServiceHealthCheck(hc *structs.HealthCheck) nodeServiceTuple {
return nodeServiceTuple{
Node: hc.Node,
Node: strings.ToLower(hc.Node),
ServiceID: hc.ServiceID,
EntMeta: hc.EnterpriseMeta,
}

View File

@ -3,22 +3,29 @@
package state
import "github.com/hashicorp/consul/agent/structs"
import (
"strings"
"github.com/hashicorp/consul/agent/structs"
)
func (nst nodeServiceTuple) nodeTuple() nodeTuple {
return nodeTuple{Node: nst.Node, Partition: ""}
return nodeTuple{
Node: strings.ToLower(nst.Node),
Partition: "",
}
}
func newNodeTupleFromNode(node *structs.Node) nodeTuple {
return nodeTuple{
Node: node.Node,
Node: strings.ToLower(node.Node),
Partition: "",
}
}
func newNodeTupleFromHealthCheck(hc *structs.HealthCheck) nodeTuple {
return nodeTuple{
Node: hc.Node,
Node: strings.ToLower(hc.Node),
Partition: "",
}
}

View File

@ -471,8 +471,11 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) {
}
// Add in a top-level check.
//
// Verify that node name references in checks are case-insensitive during
// restore.
req.Check = &structs.HealthCheck{
Node: nodeName,
Node: strings.ToUpper(nodeName),
CheckID: "check1",
Name: "check",
RaftIndex: structs.RaftIndex{
@ -499,7 +502,7 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) {
t.Fatalf("bad: %#v", out)
}
c := out[0]
if c.Node != nodeName || c.CheckID != "check1" || c.Name != "check" ||
if c.Node != strings.ToUpper(nodeName) || c.CheckID != "check1" || c.Name != "check" ||
c.CreateIndex != 3 || c.ModifyIndex != 3 {
t.Fatalf("bad check returned: %#v", c)
}
@ -545,7 +548,7 @@ func TestStateStore_EnsureRegistration_Restore(t *testing.T) {
t.Fatalf("bad: %#v", out)
}
c1 := out[0]
if c1.Node != nodeName || c1.CheckID != "check1" || c1.Name != "check" ||
if c1.Node != strings.ToUpper(nodeName) || c1.CheckID != "check1" || c1.Name != "check" ||
c1.CreateIndex != 3 || c1.ModifyIndex != 3 {
t.Fatalf("bad check returned, should not be modified: %#v", c1)
}

View File

@ -431,7 +431,7 @@ func (s *Store) discoveryChainTargetsTxn(tx ReadTxn, ws memdb.WatchSet, dc, serv
EvaluateInPartition: source.PartitionOrDefault(),
EvaluateInDatacenter: dc,
}
idx, chain, err := s.serviceDiscoveryChainTxn(tx, ws, source.Name, entMeta, req)
idx, chain, _, err := s.serviceDiscoveryChainTxn(tx, ws, source.Name, entMeta, req)
if err != nil {
return 0, nil, fmt.Errorf("failed to fetch discovery chain for %q: %v", source.String(), err)
}
@ -488,7 +488,7 @@ func (s *Store) discoveryChainSourcesTxn(tx ReadTxn, ws memdb.WatchSet, dc strin
EvaluateInPartition: sn.PartitionOrDefault(),
EvaluateInDatacenter: dc,
}
idx, chain, err := s.serviceDiscoveryChainTxn(tx, ws, sn.Name, &sn.EnterpriseMeta, req)
idx, chain, _, err := s.serviceDiscoveryChainTxn(tx, ws, sn.Name, &sn.EnterpriseMeta, req)
if err != nil {
return 0, nil, fmt.Errorf("failed to fetch discovery chain for %q: %v", sn.String(), err)
}
@ -772,7 +772,7 @@ func (s *Store) ServiceDiscoveryChain(
serviceName string,
entMeta *structs.EnterpriseMeta,
req discoverychain.CompileRequest,
) (uint64, *structs.CompiledDiscoveryChain, error) {
) (uint64, *structs.CompiledDiscoveryChain, *configentry.DiscoveryChainSet, error) {
tx := s.db.ReadTxn()
defer tx.Abort()
@ -785,19 +785,19 @@ func (s *Store) serviceDiscoveryChainTxn(
serviceName string,
entMeta *structs.EnterpriseMeta,
req discoverychain.CompileRequest,
) (uint64, *structs.CompiledDiscoveryChain, error) {
) (uint64, *structs.CompiledDiscoveryChain, *configentry.DiscoveryChainSet, error) {
index, entries, err := readDiscoveryChainConfigEntriesTxn(tx, ws, serviceName, nil, entMeta)
if err != nil {
return 0, nil, err
return 0, nil, nil, err
}
req.Entries = entries
_, config, err := s.CAConfig(ws)
if err != nil {
return 0, nil, err
return 0, nil, nil, err
} else if config == nil {
return 0, nil, errors.New("no cluster ca config setup")
return 0, nil, nil, errors.New("no cluster ca config setup")
}
// Build TrustDomain based on the ClusterID stored.
@ -805,17 +805,131 @@ func (s *Store) serviceDiscoveryChainTxn(
if signingID == nil {
// If CA is bootstrapped at all then this should never happen but be
// defensive.
return 0, nil, errors.New("no cluster trust domain setup")
return 0, nil, nil, errors.New("no cluster trust domain setup")
}
req.EvaluateInTrustDomain = signingID.Host()
// Then we compile it into something useful.
chain, err := discoverychain.Compile(req)
if err != nil {
return 0, nil, fmt.Errorf("failed to compile discovery chain: %v", err)
return 0, nil, nil, fmt.Errorf("failed to compile discovery chain: %v", err)
}
return index, chain, nil
return index, chain, entries, nil
}
func (s *Store) ReadResolvedServiceConfigEntries(
ws memdb.WatchSet,
serviceName string,
entMeta *structs.EnterpriseMeta,
upstreamIDs []structs.ServiceID,
proxyMode structs.ProxyMode,
) (uint64, *configentry.ResolvedServiceConfigSet, error) {
tx := s.db.Txn(false)
defer tx.Abort()
var res configentry.ResolvedServiceConfigSet
// The caller will likely calculate this again, but we need to do it here
// to determine if we are going to traverse into implicit upstream
// definitions.
var inferredProxyMode structs.ProxyMode
index, proxyEntry, err := configEntryTxn(tx, ws, structs.ProxyDefaults, structs.ProxyConfigGlobal, entMeta)
if err != nil {
return 0, nil, err
}
maxIndex := index
if proxyEntry != nil {
var ok bool
proxyConf, ok := proxyEntry.(*structs.ProxyConfigEntry)
if !ok {
return 0, nil, fmt.Errorf("invalid proxy config type %T", proxyEntry)
}
res.AddProxyDefaults(proxyConf)
inferredProxyMode = proxyConf.Mode
}
index, serviceEntry, err := configEntryTxn(tx, ws, structs.ServiceDefaults, serviceName, entMeta)
if err != nil {
return 0, nil, err
}
if index > maxIndex {
maxIndex = index
}
var serviceConf *structs.ServiceConfigEntry
if serviceEntry != nil {
var ok bool
serviceConf, ok = serviceEntry.(*structs.ServiceConfigEntry)
if !ok {
return 0, nil, fmt.Errorf("invalid service config type %T", serviceEntry)
}
res.AddServiceDefaults(serviceConf)
if serviceConf.Mode != structs.ProxyModeDefault {
inferredProxyMode = serviceConf.Mode
}
}
var (
noUpstreamArgs = len(upstreamIDs) == 0
// Check the args and the resolved value. If it was exclusively set via a config entry, then proxyMode
// will never be transparent because the service config request does not use the resolved value.
tproxy = proxyMode == structs.ProxyModeTransparent || inferredProxyMode == structs.ProxyModeTransparent
)
// The upstreams passed as arguments to this endpoint are the upstreams explicitly defined in a proxy registration.
// If no upstreams were passed, then we should only return the resolved config if the proxy is in transparent mode.
// Otherwise we would return a resolved upstream config to a proxy with no configured upstreams.
if noUpstreamArgs && !tproxy {
return maxIndex, &res, nil
}
// First collect all upstreams into a set of seen upstreams.
// Upstreams can come from:
// - Explicitly from proxy registrations, and therefore as an argument to this RPC endpoint
// - Implicitly from centralized upstream config in service-defaults
seenUpstreams := map[structs.ServiceID]struct{}{}
for _, sid := range upstreamIDs {
if _, ok := seenUpstreams[sid]; !ok {
seenUpstreams[sid] = struct{}{}
}
}
if serviceConf != nil && serviceConf.UpstreamConfig != nil {
for _, override := range serviceConf.UpstreamConfig.Overrides {
if override.Name == "" {
continue // skip this impossible condition
}
seenUpstreams[override.ServiceID()] = struct{}{}
}
}
for upstream := range seenUpstreams {
index, rawEntry, err := configEntryTxn(tx, ws, structs.ServiceDefaults, upstream.ID, &upstream.EnterpriseMeta)
if err != nil {
return 0, nil, err
}
if index > maxIndex {
maxIndex = index
}
if rawEntry != nil {
entry, ok := rawEntry.(*structs.ServiceConfigEntry)
if !ok {
return 0, nil, fmt.Errorf("invalid service config type %T", rawEntry)
}
res.AddServiceDefaults(entry)
}
}
return maxIndex, &res, nil
}
// ReadDiscoveryChainConfigEntries will query for the full discovery chain for

View File

@ -17,17 +17,25 @@ import (
)
func rpcClient(t *testing.T, s *Server) rpc.ClientCodec {
codec, err := rpcClientNoClose(s)
if err != nil {
t.Fatalf("err: %v", err)
}
t.Cleanup(func() { codec.Close() })
return codec
}
func rpcClientNoClose(s *Server) (rpc.ClientCodec, error) {
addr := s.config.RPCAdvertise
conn, err := net.DialTimeout("tcp", addr.String(), time.Second)
if err != nil {
t.Fatalf("err: %v", err)
return nil, err
}
// Write the Consul RPC byte to set the mode
conn.Write([]byte{byte(pool.RPCConsul)})
codec := msgpackrpc.NewCodecFromHandle(true, true, conn, structs.MsgpackHandle)
t.Cleanup(func() { codec.Close() })
return codec
return codec, nil
}
func insecureRPCClient(s *Server, c tlsutil.Config) (rpc.ClientCodec, error) {

View File

@ -113,8 +113,8 @@ func vetNodeTxnOp(op *structs.TxnNodeOp, authz acl.Authorizer) error {
var authzContext acl.AuthorizerContext
op.FillAuthzContext(&authzContext)
if authz.NodeWrite(op.Node.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(op.Node.Node, &authzContext); err != nil {
return err
}
return nil
}
@ -126,13 +126,13 @@ func vetCheckTxnOp(op *structs.TxnCheckOp, authz acl.Authorizer) error {
if op.Check.ServiceID == "" {
// Node-level check.
if authz.NodeWrite(op.Check.Node, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().NodeWriteAllowed(op.Check.Node, &authzContext); err != nil {
return err
}
} else {
// Service-level check.
if authz.ServiceWrite(op.Check.ServiceName, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(op.Check.ServiceName, &authzContext); err != nil {
return err
}
}
return nil

View File

@ -554,57 +554,64 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
}
// Verify the transaction's return value.
var expected structs.TxnResponse
var outPos int
for i, op := range arg.Ops {
err := out.Errors[outPos]
switch {
case op.KV != nil:
switch op.KV.Verb {
case api.KVGet, api.KVGetTree:
// These get filtered but won't result in an error.
case api.KVSet, api.KVDelete, api.KVDeleteCAS, api.KVDeleteTree, api.KVCAS, api.KVLock, api.KVUnlock, api.KVCheckNotExists:
require.Equal(t, err.OpIndex, i)
acl.RequirePermissionDeniedMessage(t, err.What, nil, nil, acl.ResourceKey, acl.AccessWrite, "nope")
outPos++
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
require.Equal(t, err.OpIndex, i)
acl.RequirePermissionDeniedMessage(t, err.What, nil, nil, acl.ResourceKey, acl.AccessRead, "nope")
outPos++
}
case op.Node != nil:
switch op.Node.Verb {
case api.NodeGet:
// These get filtered but won't result in an error.
case api.NodeSet, api.NodeDelete, api.NodeDeleteCAS, api.NodeCAS:
require.Equal(t, err.OpIndex, i)
acl.RequirePermissionDeniedMessage(t, err.What, nil, nil, acl.ResourceNode, acl.AccessWrite, "nope")
outPos++
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
require.Equal(t, err.OpIndex, i)
acl.RequirePermissionDeniedMessage(t, err.What, nil, nil, acl.ResourceNode, acl.AccessRead, "nope")
outPos++
}
case op.Service != nil:
switch op.Service.Verb {
case api.ServiceGet:
// These get filtered but won't result in an error.
case api.ServiceSet, api.ServiceCAS, api.ServiceDelete, api.ServiceDeleteCAS:
require.Equal(t, err.OpIndex, i)
acl.RequirePermissionDeniedMessage(t, err.What, nil, nil, acl.ResourceService, acl.AccessWrite, "nope")
outPos++
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
require.Equal(t, err.OpIndex, i)
acl.RequirePermissionDeniedMessage(t, err.What, nil, nil, acl.ResourceService, acl.AccessRead, "nope")
outPos++
}
case op.Check != nil:
switch op.Check.Verb {
case api.CheckGet:
// These get filtered but won't result in an error.
case api.CheckSet, api.CheckCAS, api.CheckDelete, api.CheckDeleteCAS:
require.Equal(t, err.OpIndex, i)
acl.RequirePermissionDeniedMessage(t, err.What, nil, nil, acl.ResourceNode, acl.AccessWrite, "nope")
outPos++
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
require.Equal(t, err.OpIndex, i)
acl.RequirePermissionDeniedMessage(t, err.What, nil, nil, acl.ResourceNode, acl.AccessRead, "nope")
outPos++
}
}
}
require.Equal(t, expected, out)
}
func TestTxn_Apply_LockDelay(t *testing.T) {
@ -927,10 +934,9 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
var out structs.TxnReadResponse
err := msgpackrpc.CallWithCodec(codec, "Txn.Read", &arg, &out)
require.NoError(t, err)
require.Equal(t, structs.TxnErrors{
{OpIndex: 0, What: acl.ErrPermissionDenied.Error()},
{OpIndex: 1, What: acl.ErrPermissionDenied.Error()},
}, out.Errors)
acl.RequirePermissionDeniedMessage(t, out.Errors[0].What, nil, nil, acl.ResourceKey, acl.AccessRead, "nope")
acl.RequirePermissionDeniedMessage(t, out.Errors[1].What, nil, nil, acl.ResourceKey, acl.AccessRead, "nope")
require.Empty(t, out.Results)
})
}

View File

@ -3,6 +3,7 @@ package consul
import (
"runtime"
"strconv"
"strings"
"github.com/hashicorp/go-version"
"github.com/hashicorp/serf/serf"
@ -161,7 +162,7 @@ func (c *Client) CheckServers(datacenter string, fn func(*metadata.Server) bool)
func isSerfMember(s *serf.Serf, nodeName string) bool {
for _, m := range s.Members() {
if m.Name == nodeName {
if strings.EqualFold(m.Name, nodeName) {
return true
}
}

View File

@ -273,6 +273,7 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
// If the token provided does not have the necessary permissions,
// write a forbidden response
// TODO(partitions): should this be possible in a partition?
// TODO(acl-error-enhancements): We should return error details somehow here.
if authz.OperatorRead(nil) != acl.Allow {
resp.WriteHeader(http.StatusForbidden)
return

View File

@ -233,9 +233,6 @@ func decodeStringKey(key string) ([]byte, error) {
func (a *Agent) keyringProcess(args *structs.KeyringRequest) (*structs.KeyringResponses, error) {
var reply structs.KeyringResponses
if _, ok := a.delegate.(*consul.Server); !ok {
return nil, fmt.Errorf("keyring operations must run against a server node")
}
if err := a.RPC("Internal.KeyringOperation", args, &reply); err != nil {
return &reply, err
}

View File

@ -152,10 +152,26 @@ func newState(ns *structs.NodeService, token string, config stateConfig) (*state
return nil, err
}
handler, err := newKindHandler(config, s, ch)
if err != nil {
return nil, err
}
return &state{
logger: config.logger.With("proxy", s.proxyID, "kind", s.kind),
serviceInstance: s,
handler: handler,
ch: ch,
snapCh: make(chan ConfigSnapshot, 1),
reqCh: make(chan chan *ConfigSnapshot, 1),
}, nil
}
func newKindHandler(config stateConfig, s serviceInstance, ch chan cache.UpdateEvent) (kindHandler, error) {
var handler kindHandler
h := handlerState{stateConfig: config, serviceInstance: s, ch: ch}
switch ns.Kind {
switch s.kind {
case structs.ServiceKindConnectProxy:
handler = &handlerConnectProxy{handlerState: h}
case structs.ServiceKindTerminatingGateway:
@ -170,14 +186,7 @@ func newState(ns *structs.NodeService, token string, config stateConfig) (*state
return nil, errors.New("not a connect-proxy, terminating-gateway, mesh-gateway, or ingress-gateway")
}
return &state{
logger: config.logger.With("proxy", s.proxyID, "kind", s.kind),
serviceInstance: s,
handler: handler,
ch: ch,
snapCh: make(chan ConfigSnapshot, 1),
reqCh: make(chan chan *ConfigSnapshot, 1),
}, nil
return handler, nil
}
func newServiceInstanceFromNodeService(ns *structs.NodeService, token string) (serviceInstance, error) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,271 @@
package proxycfg
import (
"github.com/mitchellh/go-testing-interface"
"github.com/stretchr/testify/assert"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/consul/discoverychain"
"github.com/hashicorp/consul/agent/structs"
)
// TestConfigSnapshot returns a fully populated snapshot
func TestConfigSnapshot(t testing.T, nsFn func(ns *structs.NodeService), extraUpdates []cache.UpdateEvent) *ConfigSnapshot {
roots, leaf := TestCerts(t)
// no entries implies we'll get a default chain
dbChain := discoverychain.TestCompileConfigEntries(t, "db", "default", "default", "dc1", connect.TestClusterID+".consul", nil)
assert.True(t, dbChain.IsDefault())
var (
upstreams = structs.TestUpstreams(t)
dbUpstream = upstreams[0]
geoUpstream = upstreams[1]
dbUID = NewUpstreamID(&dbUpstream)
geoUID = NewUpstreamID(&geoUpstream)
webSN = structs.ServiceIDString("web", nil)
)
baseEvents := []cache.UpdateEvent{
{
CorrelationID: rootsWatchID,
Result: roots,
},
{
CorrelationID: leafWatchID,
Result: leaf,
},
{
CorrelationID: intentionsWatchID,
Result: &structs.IndexedIntentionMatches{
Matches: []structs.Intentions{
nil, // no intentions defined
},
},
},
{
CorrelationID: svcChecksWatchIDPrefix + webSN,
Result: []structs.CheckType{},
},
{
CorrelationID: "upstream:" + geoUID.String(),
Result: &structs.PreparedQueryExecuteResponse{
Nodes: TestPreparedQueryNodes(t, "geo-cache"),
},
},
{
CorrelationID: "discovery-chain:" + dbUID.String(),
Result: &structs.DiscoveryChainResponse{
Chain: dbChain,
},
},
{
CorrelationID: "upstream-target:" + dbChain.ID() + ":" + dbUID.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: TestUpstreamNodes(t, "db"),
},
},
}
return testConfigSnapshotFixture(t, &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
Service: "web-sidecar-proxy",
Port: 9999,
Proxy: structs.ConnectProxyConfig{
DestinationServiceID: "web",
DestinationServiceName: "web",
LocalServiceAddress: "127.0.0.1",
LocalServicePort: 8080,
Config: map[string]interface{}{
"foo": "bar",
},
Upstreams: upstreams,
},
Meta: nil,
TaggedAddresses: nil,
}, nsFn, nil, testSpliceEvents(baseEvents, extraUpdates))
}
// TestConfigSnapshotDiscoveryChain returns a fully populated snapshot using a discovery chain
func TestConfigSnapshotDiscoveryChain(
t testing.T,
variation string,
nsFn func(ns *structs.NodeService),
extraUpdates []cache.UpdateEvent,
additionalEntries ...structs.ConfigEntry,
) *ConfigSnapshot {
roots, leaf := TestCerts(t)
var (
upstreams = structs.TestUpstreams(t)
geoUpstream = upstreams[1]
geoUID = NewUpstreamID(&geoUpstream)
webSN = structs.ServiceIDString("web", nil)
)
baseEvents := testSpliceEvents([]cache.UpdateEvent{
{
CorrelationID: rootsWatchID,
Result: roots,
},
{
CorrelationID: leafWatchID,
Result: leaf,
},
{
CorrelationID: intentionsWatchID,
Result: &structs.IndexedIntentionMatches{
Matches: []structs.Intentions{
nil, // no intentions defined
},
},
},
{
CorrelationID: svcChecksWatchIDPrefix + webSN,
Result: []structs.CheckType{},
},
{
CorrelationID: "upstream:" + geoUID.String(),
Result: &structs.PreparedQueryExecuteResponse{
Nodes: TestPreparedQueryNodes(t, "geo-cache"),
},
},
}, setupTestVariationConfigEntriesAndSnapshot(
t, variation, upstreams, additionalEntries...,
))
return testConfigSnapshotFixture(t, &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
Service: "web-sidecar-proxy",
Port: 9999,
Proxy: structs.ConnectProxyConfig{
DestinationServiceID: "web",
DestinationServiceName: "web",
LocalServiceAddress: "127.0.0.1",
LocalServicePort: 8080,
Config: map[string]interface{}{
"foo": "bar",
},
Upstreams: upstreams,
},
Meta: nil,
TaggedAddresses: nil,
}, nsFn, nil, testSpliceEvents(baseEvents, extraUpdates))
}
func TestConfigSnapshotExposeConfig(t testing.T, nsFn func(ns *structs.NodeService)) *ConfigSnapshot {
roots, leaf := TestCerts(t)
var (
webSN = structs.ServiceIDString("web", nil)
)
baseEvents := []cache.UpdateEvent{
{
CorrelationID: rootsWatchID,
Result: roots,
},
{
CorrelationID: leafWatchID, Result: leaf,
},
{
CorrelationID: intentionsWatchID,
Result: &structs.IndexedIntentionMatches{
Matches: []structs.Intentions{
nil, // no intentions defined
},
},
},
{
CorrelationID: svcChecksWatchIDPrefix + webSN,
Result: []structs.CheckType{},
},
}
return testConfigSnapshotFixture(t, &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
Service: "web-sidecar-proxy",
Address: "1.2.3.4",
Port: 8080,
Proxy: structs.ConnectProxyConfig{
DestinationServiceID: "web",
DestinationServiceName: "web",
LocalServicePort: 8080,
Expose: structs.ExposeConfig{
Checks: false,
Paths: []structs.ExposePath{
{
LocalPathPort: 8080,
Path: "/health1",
ListenerPort: 21500,
},
{
LocalPathPort: 8080,
Path: "/health2",
ListenerPort: 21501,
},
},
},
},
Meta: nil,
TaggedAddresses: nil,
}, nsFn, nil, baseEvents)
}
func TestConfigSnapshotGRPCExposeHTTP1(t testing.T) *ConfigSnapshot {
roots, leaf := TestCerts(t)
return testConfigSnapshotFixture(t, &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
Service: "grpc-proxy",
Address: "1.2.3.4",
Port: 8080,
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "grpc",
DestinationServiceID: "grpc",
LocalServicePort: 8080,
Config: map[string]interface{}{
"protocol": "grpc",
},
Expose: structs.ExposeConfig{
Checks: false,
Paths: []structs.ExposePath{
{
LocalPathPort: 8090,
Path: "/healthz",
ListenerPort: 21500,
Protocol: "http",
},
},
},
},
Meta: nil,
TaggedAddresses: nil,
}, nil, nil, []cache.UpdateEvent{
{
CorrelationID: rootsWatchID,
Result: roots,
},
{
CorrelationID: leafWatchID,
Result: leaf,
},
{
CorrelationID: intentionsWatchID,
Result: &structs.IndexedIntentionMatches{
Matches: []structs.Intentions{
nil, // no intentions defined
},
},
},
{
CorrelationID: svcChecksWatchIDPrefix + structs.ServiceIDString("grpc", nil),
Result: []structs.CheckType{},
},
})
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,442 @@
package proxycfg
import (
"math"
"time"
"github.com/mitchellh/go-testing-interface"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/structs"
)
func TestConfigSnapshotMeshGateway(t testing.T, variant string, nsFn func(ns *structs.NodeService), extraUpdates []cache.UpdateEvent) *ConfigSnapshot {
roots, _ := TestCerts(t)
var (
populateServices = true
useFederationStates = false
deleteCrossDCEntry = false
)
switch variant {
case "default":
case "federation-states":
populateServices = true
useFederationStates = true
deleteCrossDCEntry = true
case "newer-info-in-federation-states":
populateServices = true
useFederationStates = true
deleteCrossDCEntry = false
case "older-info-in-federation-states":
populateServices = true
useFederationStates = true
deleteCrossDCEntry = false
case "no-services":
populateServices = false
useFederationStates = false
deleteCrossDCEntry = false
case "service-subsets":
extraUpdates = append(extraUpdates, cache.UpdateEvent{
CorrelationID: serviceResolversWatchID,
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "bar",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
},
},
},
})
case "service-subsets2": // TODO(rb): make this merge with 'service-subsets'
extraUpdates = append(extraUpdates, cache.UpdateEvent{
CorrelationID: serviceResolversWatchID,
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "bar",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.version == 1",
},
"v2": {
Filter: "Service.Meta.version == 2",
OnlyPassing: true,
},
},
},
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "foo",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.version == 1",
},
"v2": {
Filter: "Service.Meta.version == 2",
OnlyPassing: true,
},
},
},
},
},
})
case "default-service-subsets2": // TODO(rb): rename to strip the 2 when the prior is merged with 'service-subsets'
extraUpdates = append(extraUpdates, cache.UpdateEvent{
CorrelationID: serviceResolversWatchID,
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "bar",
DefaultSubset: "v2",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.version == 1",
},
"v2": {
Filter: "Service.Meta.version == 2",
OnlyPassing: true,
},
},
},
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "foo",
DefaultSubset: "v2",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.version == 1",
},
"v2": {
Filter: "Service.Meta.version == 2",
OnlyPassing: true,
},
},
},
},
},
})
case "ignore-extra-resolvers":
extraUpdates = append(extraUpdates, cache.UpdateEvent{
CorrelationID: serviceResolversWatchID,
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "bar",
DefaultSubset: "v2",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
},
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "notfound",
DefaultSubset: "v2",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
},
},
},
})
case "service-timeouts":
extraUpdates = append(extraUpdates, cache.UpdateEvent{
CorrelationID: serviceResolversWatchID,
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "bar",
ConnectTimeout: 10 * time.Second,
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
},
},
},
})
case "non-hash-lb-injected":
extraUpdates = append(extraUpdates, cache.UpdateEvent{
CorrelationID: "service-resolvers", // serviceResolversWatchID
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "bar",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
LoadBalancer: &structs.LoadBalancer{
Policy: "least_request",
LeastRequestConfig: &structs.LeastRequestConfig{
ChoiceCount: 5,
},
},
},
},
},
})
case "hash-lb-ignored":
extraUpdates = append(extraUpdates, cache.UpdateEvent{
CorrelationID: "service-resolvers", // serviceResolversWatchID
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "bar",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
LoadBalancer: &structs.LoadBalancer{
Policy: "ring_hash",
RingHashConfig: &structs.RingHashConfig{
MinimumRingSize: 20,
MaximumRingSize: 50,
},
},
},
},
},
})
default:
t.Fatalf("unknown variant: %s", variant)
return nil
}
baseEvents := []cache.UpdateEvent{
{
CorrelationID: rootsWatchID,
Result: roots,
},
{
CorrelationID: serviceListWatchID,
Result: &structs.IndexedServiceList{
Services: nil,
},
},
{
CorrelationID: serviceResolversWatchID,
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: nil,
},
},
{
CorrelationID: datacentersWatchID,
Result: &[]string{"dc1"},
},
}
if populateServices || useFederationStates {
baseEvents = testSpliceEvents(baseEvents, []cache.UpdateEvent{
{
CorrelationID: datacentersWatchID,
Result: &[]string{"dc1", "dc2", "dc4", "dc6"},
},
})
}
if populateServices {
var (
foo = structs.NewServiceName("foo", nil)
bar = structs.NewServiceName("bar", nil)
)
baseEvents = testSpliceEvents(baseEvents, []cache.UpdateEvent{
{
CorrelationID: "mesh-gateway:dc2",
Result: &structs.IndexedNodesWithGateways{
Nodes: TestGatewayNodesDC2(t),
},
},
{
CorrelationID: "mesh-gateway:dc4",
Result: &structs.IndexedNodesWithGateways{
Nodes: TestGatewayNodesDC4Hostname(t),
},
},
{
CorrelationID: "mesh-gateway:dc6",
Result: &structs.IndexedNodesWithGateways{
Nodes: TestGatewayNodesDC6Hostname(t),
},
},
{
CorrelationID: serviceListWatchID,
Result: &structs.IndexedServiceList{
Services: []structs.ServiceName{
foo,
bar,
},
},
},
{
CorrelationID: "connect-service:" + foo.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: TestGatewayServiceGroupFooDC1(t),
},
},
{
CorrelationID: "connect-service:" + bar.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: TestGatewayServiceGroupBarDC1(t),
},
},
{
CorrelationID: serviceResolversWatchID,
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
//
},
},
},
})
}
if useFederationStates {
nsFn = testSpliceNodeServiceFunc(nsFn, func(ns *structs.NodeService) {
ns.Meta[structs.MetaWANFederationKey] = "1"
})
if deleteCrossDCEntry {
baseEvents = testSpliceEvents(baseEvents, []cache.UpdateEvent{
{
// Have the cross-dc query mechanism not work for dc2 so
// fedstates will infill.
CorrelationID: "mesh-gateway:dc2",
Result: &structs.IndexedNodesWithGateways{
Nodes: nil,
},
},
})
}
dc2Nodes := TestGatewayNodesDC2(t)
switch variant {
case "newer-info-in-federation-states":
// Create a duplicate entry in FedStateGateways, with a high ModifyIndex, to
// verify that fresh data in the federation state is preferred over stale data
// in GatewayGroups.
svc := structs.TestNodeServiceMeshGatewayWithAddrs(t,
"10.0.1.3", 8443,
structs.ServiceAddress{Address: "10.0.1.3", Port: 8443},
structs.ServiceAddress{Address: "198.18.1.3", Port: 443},
)
svc.RaftIndex.ModifyIndex = math.MaxUint64
dc2Nodes = structs.CheckServiceNodes{
{
Node: dc2Nodes[0].Node,
Service: svc,
},
}
case "older-info-in-federation-states":
// Create a duplicate entry in FedStateGateways, with a low ModifyIndex, to
// verify that stale data in the federation state is ignored in favor of the
// fresher data in GatewayGroups.
svc := structs.TestNodeServiceMeshGatewayWithAddrs(t,
"10.0.1.3", 8443,
structs.ServiceAddress{Address: "10.0.1.3", Port: 8443},
structs.ServiceAddress{Address: "198.18.1.3", Port: 443},
)
svc.RaftIndex.ModifyIndex = 0
dc2Nodes = structs.CheckServiceNodes{
{
Node: dc2Nodes[0].Node,
Service: svc,
},
}
}
baseEvents = testSpliceEvents(baseEvents, []cache.UpdateEvent{
{
CorrelationID: federationStateListGatewaysWatchID,
Result: &structs.DatacenterIndexedCheckServiceNodes{
DatacenterNodes: map[string]structs.CheckServiceNodes{
"dc2": dc2Nodes,
"dc4": TestGatewayNodesDC4Hostname(t),
"dc6": TestGatewayNodesDC6Hostname(t),
},
},
},
{
CorrelationID: consulServerListWatchID,
Result: &structs.IndexedCheckServiceNodes{
Nodes: nil, // TODO
},
},
})
}
return testConfigSnapshotFixture(t, &structs.NodeService{
Kind: structs.ServiceKindMeshGateway,
Service: "mesh-gateway",
Address: "1.2.3.4",
Port: 8443,
Proxy: structs.ConnectProxyConfig{
Config: map[string]interface{}{},
},
Meta: make(map[string]string),
TaggedAddresses: map[string]structs.ServiceAddress{
structs.TaggedAddressLAN: {
Address: "1.2.3.4",
Port: 8443,
},
structs.TaggedAddressWAN: {
Address: "198.18.0.1",
Port: 443,
},
},
}, nsFn, nil, testSpliceEvents(baseEvents, extraUpdates))
}

View File

@ -0,0 +1,655 @@
package proxycfg
import (
"github.com/mitchellh/go-testing-interface"
"github.com/hashicorp/consul/agent/cache"
agentcache "github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/structs"
)
func TestConfigSnapshotTerminatingGateway(
t testing.T,
populateServices bool,
nsFn func(ns *structs.NodeService),
extraUpdates []agentcache.UpdateEvent,
) *ConfigSnapshot {
roots, _ := TestCerts(t)
var (
web = structs.NewServiceName("web", nil)
api = structs.NewServiceName("api", nil)
db = structs.NewServiceName("db", nil)
cache = structs.NewServiceName("cache", nil)
)
baseEvents := []agentcache.UpdateEvent{
{
CorrelationID: rootsWatchID,
Result: roots,
},
{
CorrelationID: gatewayServicesWatchID,
Result: &structs.IndexedGatewayServices{
Services: nil,
},
},
}
if populateServices {
webNodes := TestUpstreamNodes(t, web.Name)
webNodes[0].Service.Meta = map[string]string{"version": "1"}
webNodes[1].Service.Meta = map[string]string{"version": "2"}
apiNodes := structs.CheckServiceNodes{
structs.CheckServiceNode{
Node: &structs.Node{
ID: "api",
Node: "test1",
Address: "10.10.1.1",
Datacenter: "dc1",
},
Service: &structs.NodeService{
Service: "api",
Address: "api.mydomain",
Port: 8081,
},
Checks: structs.HealthChecks{
{Status: "critical"},
},
},
structs.CheckServiceNode{
Node: &structs.Node{
ID: "test2",
Node: "test2",
Address: "10.10.1.2",
Datacenter: "dc1",
},
Service: &structs.NodeService{
Service: "api",
Address: "api.altdomain",
Port: 8081,
Meta: map[string]string{
"domain": "alt",
},
},
},
structs.CheckServiceNode{
Node: &structs.Node{
ID: "test3",
Node: "test3",
Address: "10.10.1.3",
Datacenter: "dc1",
},
Service: &structs.NodeService{
Service: "api",
Address: "10.10.1.3",
Port: 8081,
},
},
structs.CheckServiceNode{
Node: &structs.Node{
ID: "test4",
Node: "test4",
Address: "10.10.1.4",
Datacenter: "dc1",
},
Service: &structs.NodeService{
Service: "api",
Address: "api.thirddomain",
Port: 8081,
},
},
}
// Has failing instance
dbNodes := structs.CheckServiceNodes{
structs.CheckServiceNode{
Node: &structs.Node{
ID: "db",
Node: "test4",
Address: "10.10.1.4",
Datacenter: "dc1",
},
Service: &structs.NodeService{
Service: "db",
Address: "db.mydomain",
Port: 8081,
},
Checks: structs.HealthChecks{
{Status: "critical"},
},
},
}
// Has passing instance but failing subset
cacheNodes := structs.CheckServiceNodes{
{
Node: &structs.Node{
ID: "cache",
Node: "test5",
Address: "10.10.1.5",
Datacenter: "dc1",
},
Service: &structs.NodeService{
Service: "cache",
Address: "cache.mydomain",
Port: 8081,
},
},
{
Node: &structs.Node{
ID: "cache",
Node: "test5",
Address: "10.10.1.5",
Datacenter: "dc1",
},
Service: &structs.NodeService{
Service: "cache",
Address: "cache.mydomain",
Port: 8081,
Meta: map[string]string{
"Env": "prod",
},
},
Checks: structs.HealthChecks{
{Status: "critical"},
},
},
}
baseEvents = testSpliceEvents(baseEvents, []agentcache.UpdateEvent{
{
CorrelationID: gatewayServicesWatchID,
Result: &structs.IndexedGatewayServices{
Services: []*structs.GatewayService{
{
Service: web,
CAFile: "ca.cert.pem",
},
{
Service: api,
CAFile: "ca.cert.pem",
CertFile: "api.cert.pem",
KeyFile: "api.key.pem",
},
{
Service: db,
},
{
Service: cache,
},
},
},
},
{
CorrelationID: externalServiceIDPrefix + web.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: webNodes,
},
},
{
CorrelationID: externalServiceIDPrefix + api.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: apiNodes,
},
},
{
CorrelationID: externalServiceIDPrefix + db.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: dbNodes,
},
},
{
CorrelationID: externalServiceIDPrefix + cache.String(),
Result: &structs.IndexedCheckServiceNodes{
Nodes: cacheNodes,
},
},
// ========
// no intentions defined for these services
{
CorrelationID: serviceIntentionsIDPrefix + web.String(),
Result: &structs.IndexedIntentionMatches{
Matches: []structs.Intentions{
nil,
},
},
},
{
CorrelationID: serviceIntentionsIDPrefix + api.String(),
Result: &structs.IndexedIntentionMatches{
Matches: []structs.Intentions{
nil,
},
},
},
{
CorrelationID: serviceIntentionsIDPrefix + db.String(),
Result: &structs.IndexedIntentionMatches{
Matches: []structs.Intentions{
nil,
},
},
},
{
CorrelationID: serviceIntentionsIDPrefix + cache.String(),
Result: &structs.IndexedIntentionMatches{
Matches: []structs.Intentions{
nil,
},
},
},
// ========
{
CorrelationID: serviceLeafIDPrefix + web.String(),
Result: &structs.IssuedCert{
CertPEM: golden(t, "test-leaf-cert"),
PrivateKeyPEM: golden(t, "test-leaf-key"),
},
},
{
CorrelationID: serviceLeafIDPrefix + api.String(),
Result: &structs.IssuedCert{
CertPEM: golden(t, "alt-test-leaf-cert"),
PrivateKeyPEM: golden(t, "alt-test-leaf-key"),
},
},
{
CorrelationID: serviceLeafIDPrefix + db.String(),
Result: &structs.IssuedCert{
CertPEM: golden(t, "db-test-leaf-cert"),
PrivateKeyPEM: golden(t, "db-test-leaf-key"),
},
},
{
CorrelationID: serviceLeafIDPrefix + cache.String(),
Result: &structs.IssuedCert{
CertPEM: golden(t, "cache-test-leaf-cert"),
PrivateKeyPEM: golden(t, "cache-test-leaf-key"),
},
},
// ========
{
CorrelationID: serviceConfigIDPrefix + web.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "tcp"},
},
},
{
CorrelationID: serviceConfigIDPrefix + api.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "tcp"},
},
},
{
CorrelationID: serviceConfigIDPrefix + db.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "tcp"},
},
},
{
CorrelationID: serviceConfigIDPrefix + cache.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "tcp"},
},
},
// ========
{
CorrelationID: serviceResolverIDPrefix + web.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: nil,
},
},
{
CorrelationID: serviceResolverIDPrefix + api.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: nil,
},
},
{
CorrelationID: serviceResolverIDPrefix + db.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: nil,
},
},
{
CorrelationID: serviceResolverIDPrefix + cache.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: nil,
},
},
})
}
return testConfigSnapshotFixture(t, &structs.NodeService{
Kind: structs.ServiceKindTerminatingGateway,
Service: "terminating-gateway",
Address: "1.2.3.4",
Port: 8443,
TaggedAddresses: map[string]structs.ServiceAddress{
structs.TaggedAddressWAN: {
Address: "198.18.0.1",
Port: 443,
},
},
}, nsFn, nil, testSpliceEvents(baseEvents, extraUpdates))
}
func TestConfigSnapshotTerminatingGatewayServiceSubsets(t testing.T) *ConfigSnapshot {
return testConfigSnapshotTerminatingGatewayServiceSubsets(t, false)
}
func TestConfigSnapshotTerminatingGatewayServiceSubsetsWebAndCache(t testing.T) *ConfigSnapshot {
return testConfigSnapshotTerminatingGatewayServiceSubsets(t, true)
}
func testConfigSnapshotTerminatingGatewayServiceSubsets(t testing.T, alsoAdjustCache bool) *ConfigSnapshot {
var (
web = structs.NewServiceName("web", nil)
cache = structs.NewServiceName("cache", nil)
)
events := []agentcache.UpdateEvent{
{
CorrelationID: serviceResolverIDPrefix + web.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "web",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.version == 1",
},
"v2": {
Filter: "Service.Meta.version == 2",
OnlyPassing: true,
},
},
},
},
},
},
{
CorrelationID: serviceConfigIDPrefix + web.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "http"},
},
},
}
if alsoAdjustCache {
events = testSpliceEvents(events, []agentcache.UpdateEvent{
{
CorrelationID: serviceResolverIDPrefix + cache.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "cache",
Subsets: map[string]structs.ServiceResolverSubset{
"prod": {
Filter: "Service.Meta.Env == prod",
},
},
},
},
},
},
{
CorrelationID: serviceConfigIDPrefix + web.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "http"},
},
},
})
}
return TestConfigSnapshotTerminatingGateway(t, true, nil, events)
}
func TestConfigSnapshotTerminatingGatewayDefaultServiceSubset(t testing.T) *ConfigSnapshot {
web := structs.NewServiceName("web", nil)
return TestConfigSnapshotTerminatingGateway(t, true, nil, []agentcache.UpdateEvent{
{
CorrelationID: serviceResolverIDPrefix + web.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "web",
DefaultSubset: "v2",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.version == 1",
},
"v2": {
Filter: "Service.Meta.version == 2",
OnlyPassing: true,
},
},
},
},
},
},
// {
// CorrelationID: serviceConfigIDPrefix + web.String(),
// Result: &structs.ServiceConfigResponse{
// ProxyConfig: map[string]interface{}{"protocol": "http"},
// },
// },
})
}
func TestConfigSnapshotTerminatingGatewayLBConfig(t testing.T) *ConfigSnapshot {
return testConfigSnapshotTerminatingGatewayLBConfig(t, "default")
}
func TestConfigSnapshotTerminatingGatewayLBConfigNoHashPolicies(t testing.T) *ConfigSnapshot {
return testConfigSnapshotTerminatingGatewayLBConfig(t, "no-hash-policies")
}
func testConfigSnapshotTerminatingGatewayLBConfig(t testing.T, variant string) *ConfigSnapshot {
web := structs.NewServiceName("web", nil)
entry := &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "web",
DefaultSubset: "v2",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
LoadBalancer: &structs.LoadBalancer{
Policy: "ring_hash",
RingHashConfig: &structs.RingHashConfig{
MinimumRingSize: 20,
MaximumRingSize: 50,
},
HashPolicies: []structs.HashPolicy{
{
Field: structs.HashPolicyCookie,
FieldValue: "chocolate-chip",
Terminal: true,
},
{
Field: structs.HashPolicyHeader,
FieldValue: "x-user-id",
},
{
SourceIP: true,
Terminal: true,
},
},
},
}
switch variant {
case "default":
case "no-hash-policies":
entry.LoadBalancer.HashPolicies = nil
default:
t.Fatalf("unknown variant %q", variant)
return nil
}
return TestConfigSnapshotTerminatingGateway(t, true, nil, []cache.UpdateEvent{
{
CorrelationID: serviceConfigIDPrefix + web.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "http"},
},
},
{
CorrelationID: serviceResolverIDPrefix + web.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{entry},
},
},
{
CorrelationID: serviceConfigIDPrefix + web.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "http"},
},
},
})
}
func TestConfigSnapshotTerminatingGatewayHostnameSubsets(t testing.T) *ConfigSnapshot {
var (
api = structs.NewServiceName("api", nil)
cache = structs.NewServiceName("cache", nil)
)
return TestConfigSnapshotTerminatingGateway(t, true, nil, []agentcache.UpdateEvent{
{
CorrelationID: serviceResolverIDPrefix + api.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "api",
Subsets: map[string]structs.ServiceResolverSubset{
"alt": {
Filter: "Service.Meta.domain == alt",
},
},
},
},
},
},
{
CorrelationID: serviceResolverIDPrefix + cache.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "cache",
Subsets: map[string]structs.ServiceResolverSubset{
"prod": {
Filter: "Service.Meta.Env == prod",
},
},
},
},
},
},
{
CorrelationID: serviceConfigIDPrefix + api.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "http"},
},
},
{
CorrelationID: serviceConfigIDPrefix + cache.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "http"},
},
},
})
}
func TestConfigSnapshotTerminatingGatewayIgnoreExtraResolvers(t testing.T) *ConfigSnapshot {
var (
web = structs.NewServiceName("web", nil)
notfound = structs.NewServiceName("notfound", nil)
)
return TestConfigSnapshotTerminatingGateway(t, true, nil, []agentcache.UpdateEvent{
{
CorrelationID: serviceResolverIDPrefix + web.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "web",
DefaultSubset: "v2",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
},
},
},
},
{
CorrelationID: serviceResolverIDPrefix + notfound.String(),
Result: &structs.IndexedConfigEntries{
Kind: structs.ServiceResolver,
Entries: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "notfound",
DefaultSubset: "v2",
Subsets: map[string]structs.ServiceResolverSubset{
"v1": {
Filter: "Service.Meta.Version == 1",
},
"v2": {
Filter: "Service.Meta.Version == 2",
OnlyPassing: true,
},
},
},
},
},
},
{
CorrelationID: serviceConfigIDPrefix + web.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "http"},
},
},
})
}
func TestConfigSnapshotTerminatingGatewayWithServiceDefaultsMeta(t testing.T) *ConfigSnapshot {
web := structs.NewServiceName("web", nil)
return TestConfigSnapshotTerminatingGateway(t, true, nil, []agentcache.UpdateEvent{
{
CorrelationID: serviceConfigIDPrefix + web.String(),
Result: &structs.ServiceConfigResponse{
Meta: map[string]string{"a": "b"},
},
},
})
}

Some files were not shown because too many files have changed in this diff Show More