mirror of https://github.com/status-im/consul.git
proxycfg: server-local intention upstreams data source
This is the OSS portion of enterprise PR 2157. It builds on the local blocking query work in #13438 to implement the proxycfg.IntentionUpstreams interface using server-local data. Also moves the ACL filtering logic from agent/consul into the acl/filter package so that it can be reused here.
This commit is contained in:
parent
37ccbd2826
commit
45886848b4
|
@ -4231,16 +4231,18 @@ func (a *Agent) proxyDataSources() proxycfg.DataSources {
|
||||||
ExportedPeeredServices: proxycfgglue.CacheExportedPeeredServices(a.cache),
|
ExportedPeeredServices: proxycfgglue.CacheExportedPeeredServices(a.cache),
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.config.ServerMode {
|
if server, ok := a.delegate.(*consul.Server); ok {
|
||||||
deps := proxycfgglue.ServerDataSourceDeps{
|
deps := proxycfgglue.ServerDataSourceDeps{
|
||||||
EventPublisher: a.baseDeps.EventPublisher,
|
EventPublisher: a.baseDeps.EventPublisher,
|
||||||
ViewStore: a.baseDeps.ViewStore,
|
ViewStore: a.baseDeps.ViewStore,
|
||||||
Logger: a.logger.Named("proxycfg.server-data-sources"),
|
Logger: a.logger.Named("proxycfg.server-data-sources"),
|
||||||
ACLResolver: a.delegate,
|
ACLResolver: a.delegate,
|
||||||
|
GetStore: func() proxycfgglue.Store { return server.FSM().State() },
|
||||||
}
|
}
|
||||||
sources.ConfigEntry = proxycfgglue.ServerConfigEntry(deps)
|
sources.ConfigEntry = proxycfgglue.ServerConfigEntry(deps)
|
||||||
sources.ConfigEntryList = proxycfgglue.ServerConfigEntryList(deps)
|
sources.ConfigEntryList = proxycfgglue.ServerConfigEntryList(deps)
|
||||||
sources.Intentions = proxycfgglue.ServerIntentions(deps)
|
sources.Intentions = proxycfgglue.ServerIntentions(deps)
|
||||||
|
sources.IntentionUpstreams = proxycfgglue.ServerIntentionUpstreams(deps)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.fillEnterpriseProxyDataSources(&sources)
|
a.fillEnterpriseProxyDataSources(&sources)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/acl/resolver"
|
"github.com/hashicorp/consul/acl/resolver"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
"github.com/hashicorp/consul/agent/token"
|
"github.com/hashicorp/consul/agent/token"
|
||||||
"github.com/hashicorp/consul/logging"
|
"github.com/hashicorp/consul/logging"
|
||||||
)
|
)
|
||||||
|
@ -43,10 +44,6 @@ const (
|
||||||
// provided.
|
// provided.
|
||||||
anonymousToken = "anonymous"
|
anonymousToken = "anonymous"
|
||||||
|
|
||||||
// redactedToken is shown in structures with embedded tokens when they
|
|
||||||
// are not allowed to be displayed.
|
|
||||||
redactedToken = "<hidden>"
|
|
||||||
|
|
||||||
// aclTokenReapingRateLimit is the number of batch token reaping requests per second allowed.
|
// aclTokenReapingRateLimit is the number of batch token reaping requests per second allowed.
|
||||||
aclTokenReapingRateLimit rate.Limit = 1.0
|
aclTokenReapingRateLimit rate.Limit = 1.0
|
||||||
|
|
||||||
|
@ -1114,816 +1111,8 @@ func (r *ACLResolver) ResolveTokenAndDefaultMetaWithPeerName(
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// aclFilter is used to filter results from our state store based on ACL rules
|
|
||||||
// configured for the provided token.
|
|
||||||
type aclFilter struct {
|
|
||||||
authorizer acl.Authorizer
|
|
||||||
logger hclog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// newACLFilter constructs a new aclFilter.
|
|
||||||
func newACLFilter(authorizer acl.Authorizer, logger hclog.Logger) *aclFilter {
|
|
||||||
if logger == nil {
|
|
||||||
logger = hclog.New(&hclog.LoggerOptions{})
|
|
||||||
}
|
|
||||||
return &aclFilter{
|
|
||||||
authorizer: authorizer,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// allowNode is used to determine if a node is accessible for an ACL.
|
|
||||||
func (f *aclFilter) allowNode(node string, ent *acl.AuthorizerContext) bool {
|
|
||||||
return f.authorizer.NodeRead(node, ent) == acl.Allow
|
|
||||||
}
|
|
||||||
|
|
||||||
// allowNode is used to determine if the gateway and service are accessible for an ACL
|
|
||||||
func (f *aclFilter) allowGateway(gs *structs.GatewayService) bool {
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
|
|
||||||
// Need read on service and gateway. Gateway may have different EnterpriseMeta so we fill authzContext twice
|
|
||||||
gs.Gateway.FillAuthzContext(&authzContext)
|
|
||||||
if !f.allowService(gs.Gateway.Name, &authzContext) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.Service.FillAuthzContext(&authzContext)
|
|
||||||
if !f.allowService(gs.Service.Name, &authzContext) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// allowService is used to determine if a service is accessible for an ACL.
|
|
||||||
func (f *aclFilter) allowService(service string, ent *acl.AuthorizerContext) bool {
|
|
||||||
if service == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.authorizer.ServiceRead(service, ent) == acl.Allow
|
|
||||||
}
|
|
||||||
|
|
||||||
// allowSession is used to determine if a session for a node is accessible for
|
|
||||||
// an ACL.
|
|
||||||
func (f *aclFilter) allowSession(node string, ent *acl.AuthorizerContext) bool {
|
|
||||||
return f.authorizer.SessionRead(node, ent) == acl.Allow
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterHealthChecks is used to filter a set of health checks down based on
|
|
||||||
// the configured ACL rules for a token. Returns true if any elements were
|
|
||||||
// removed.
|
|
||||||
func (f *aclFilter) filterHealthChecks(checks *structs.HealthChecks) bool {
|
|
||||||
hc := *checks
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
var removed bool
|
|
||||||
|
|
||||||
for i := 0; i < len(hc); i++ {
|
|
||||||
check := hc[i]
|
|
||||||
check.FillAuthzContext(&authzContext)
|
|
||||||
if f.allowNode(check.Node, &authzContext) && f.allowService(check.ServiceName, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
f.logger.Debug("dropping check from result due to ACLs", "check", check.CheckID)
|
|
||||||
removed = true
|
|
||||||
hc = append(hc[:i], hc[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
*checks = hc
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterServices is used to filter a set of services based on ACLs. Returns
|
|
||||||
// true if any elements were removed.
|
|
||||||
func (f *aclFilter) filterServices(services structs.Services, entMeta *acl.EnterpriseMeta) bool {
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
entMeta.FillAuthzContext(&authzContext)
|
|
||||||
|
|
||||||
var removed bool
|
|
||||||
|
|
||||||
for svc := range services {
|
|
||||||
if f.allowService(svc, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.logger.Debug("dropping service from result due to ACLs", "service", svc)
|
|
||||||
removed = true
|
|
||||||
delete(services, svc)
|
|
||||||
}
|
|
||||||
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterServiceNodes is used to filter a set of nodes for a given service
|
|
||||||
// based on the configured ACL rules. Returns true if any elements were removed.
|
|
||||||
func (f *aclFilter) filterServiceNodes(nodes *structs.ServiceNodes) bool {
|
|
||||||
sn := *nodes
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
var removed bool
|
|
||||||
|
|
||||||
for i := 0; i < len(sn); i++ {
|
|
||||||
node := sn[i]
|
|
||||||
|
|
||||||
node.FillAuthzContext(&authzContext)
|
|
||||||
if f.allowNode(node.Node, &authzContext) && f.allowService(node.ServiceName, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
removed = true
|
|
||||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node.Node, &node.EnterpriseMeta))
|
|
||||||
sn = append(sn[:i], sn[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
*nodes = sn
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterNodeServices is used to filter services on a given node base on ACLs.
|
|
||||||
// Returns true if any elements were removed
|
|
||||||
func (f *aclFilter) filterNodeServices(services **structs.NodeServices) bool {
|
|
||||||
if *services == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
(*services).Node.FillAuthzContext(&authzContext)
|
|
||||||
if !f.allowNode((*services).Node.Node, &authzContext) {
|
|
||||||
*services = nil
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var removed bool
|
|
||||||
for svcName, svc := range (*services).Services {
|
|
||||||
svc.FillAuthzContext(&authzContext)
|
|
||||||
|
|
||||||
if f.allowNode((*services).Node.Node, &authzContext) && f.allowService(svcName, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.logger.Debug("dropping service from result due to ACLs", "service", svc.CompoundServiceID())
|
|
||||||
removed = true
|
|
||||||
delete((*services).Services, svcName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterNodeServices is used to filter services on a given node base on ACLs.
|
|
||||||
// Returns true if any elements were removed.
|
|
||||||
func (f *aclFilter) filterNodeServiceList(services *structs.NodeServiceList) bool {
|
|
||||||
if services.Node == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
services.Node.FillAuthzContext(&authzContext)
|
|
||||||
if !f.allowNode(services.Node.Node, &authzContext) {
|
|
||||||
*services = structs.NodeServiceList{}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var removed bool
|
|
||||||
svcs := services.Services
|
|
||||||
for i := 0; i < len(svcs); i++ {
|
|
||||||
svc := svcs[i]
|
|
||||||
svc.FillAuthzContext(&authzContext)
|
|
||||||
|
|
||||||
if f.allowService(svc.Service, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
f.logger.Debug("dropping service from result due to ACLs", "service", svc.CompoundServiceID())
|
|
||||||
svcs = append(svcs[:i], svcs[i+1:]...)
|
|
||||||
i--
|
|
||||||
removed = true
|
|
||||||
}
|
|
||||||
services.Services = svcs
|
|
||||||
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterCheckServiceNodes is used to filter nodes based on ACL rules. Returns
|
|
||||||
// true if any elements were removed.
|
|
||||||
func (f *aclFilter) filterCheckServiceNodes(nodes *structs.CheckServiceNodes) bool {
|
|
||||||
csn := *nodes
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
var removed bool
|
|
||||||
|
|
||||||
for i := 0; i < len(csn); i++ {
|
|
||||||
node := csn[i]
|
|
||||||
node.Service.FillAuthzContext(&authzContext)
|
|
||||||
if f.allowNode(node.Node.Node, &authzContext) && f.allowService(node.Service.Service, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node.Node.Node, node.Node.GetEnterpriseMeta()))
|
|
||||||
removed = true
|
|
||||||
csn = append(csn[:i], csn[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
*nodes = csn
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterServiceTopology is used to filter upstreams/downstreams based on ACL rules.
|
|
||||||
// this filter is unlike others in that it also returns whether the result was filtered by ACLs
|
|
||||||
func (f *aclFilter) filterServiceTopology(topology *structs.ServiceTopology) bool {
|
|
||||||
filteredUpstreams := f.filterCheckServiceNodes(&topology.Upstreams)
|
|
||||||
filteredDownstreams := f.filterCheckServiceNodes(&topology.Downstreams)
|
|
||||||
return filteredUpstreams || filteredDownstreams
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterDatacenterCheckServiceNodes is used to filter nodes based on ACL rules.
|
|
||||||
// Returns true if any elements are removed.
|
|
||||||
func (f *aclFilter) filterDatacenterCheckServiceNodes(datacenterNodes *map[string]structs.CheckServiceNodes) bool {
|
|
||||||
dn := *datacenterNodes
|
|
||||||
out := make(map[string]structs.CheckServiceNodes)
|
|
||||||
var removed bool
|
|
||||||
for dc := range dn {
|
|
||||||
nodes := dn[dc]
|
|
||||||
if f.filterCheckServiceNodes(&nodes) {
|
|
||||||
removed = true
|
|
||||||
}
|
|
||||||
if len(nodes) > 0 {
|
|
||||||
out[dc] = nodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*datacenterNodes = out
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterSessions is used to filter a set of sessions based on ACLs. Returns
|
|
||||||
// true if any elements were removed.
|
|
||||||
func (f *aclFilter) filterSessions(sessions *structs.Sessions) bool {
|
|
||||||
s := *sessions
|
|
||||||
|
|
||||||
var removed bool
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
session := s[i]
|
|
||||||
|
|
||||||
var entCtx acl.AuthorizerContext
|
|
||||||
session.FillAuthzContext(&entCtx)
|
|
||||||
|
|
||||||
if f.allowSession(session.Node, &entCtx) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
removed = true
|
|
||||||
f.logger.Debug("dropping session from result due to ACLs", "session", session.ID)
|
|
||||||
s = append(s[:i], s[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
*sessions = s
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterCoordinates is used to filter nodes in a coordinate dump based on ACL
|
|
||||||
// rules. Returns true if any elements were removed.
|
|
||||||
func (f *aclFilter) filterCoordinates(coords *structs.Coordinates) bool {
|
|
||||||
c := *coords
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
var removed bool
|
|
||||||
|
|
||||||
for i := 0; i < len(c); i++ {
|
|
||||||
c[i].FillAuthzContext(&authzContext)
|
|
||||||
node := c[i].Node
|
|
||||||
if f.allowNode(node, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, c[i].GetEnterpriseMeta()))
|
|
||||||
removed = true
|
|
||||||
c = append(c[:i], c[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
*coords = c
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterIntentions is used to filter intentions based on ACL rules.
|
|
||||||
// We prune entries the user doesn't have access to, and we redact any tokens
|
|
||||||
// if the user doesn't have a management token. Returns true if any elements
|
|
||||||
// were removed.
|
|
||||||
func (f *aclFilter) filterIntentions(ixns *structs.Intentions) bool {
|
|
||||||
ret := make(structs.Intentions, 0, len(*ixns))
|
|
||||||
var removed bool
|
|
||||||
for _, ixn := range *ixns {
|
|
||||||
if !ixn.CanRead(f.authorizer) {
|
|
||||||
removed = true
|
|
||||||
f.logger.Debug("dropping intention from result due to ACLs", "intention", ixn.ID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, ixn)
|
|
||||||
}
|
|
||||||
|
|
||||||
*ixns = ret
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterNodeDump is used to filter through all parts of a node dump and
|
|
||||||
// remove elements the provided ACL token cannot access. Returns true if
|
|
||||||
// any elements were removed.
|
|
||||||
func (f *aclFilter) filterNodeDump(dump *structs.NodeDump) bool {
|
|
||||||
nd := *dump
|
|
||||||
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
var removed bool
|
|
||||||
for i := 0; i < len(nd); i++ {
|
|
||||||
info := nd[i]
|
|
||||||
|
|
||||||
// Filter nodes
|
|
||||||
info.FillAuthzContext(&authzContext)
|
|
||||||
if node := info.Node; !f.allowNode(node, &authzContext) {
|
|
||||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, info.GetEnterpriseMeta()))
|
|
||||||
removed = true
|
|
||||||
nd = append(nd[:i], nd[i+1:]...)
|
|
||||||
i--
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter services
|
|
||||||
for j := 0; j < len(info.Services); j++ {
|
|
||||||
svc := info.Services[j].Service
|
|
||||||
info.Services[j].FillAuthzContext(&authzContext)
|
|
||||||
if f.allowNode(info.Node, &authzContext) && f.allowService(svc, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.logger.Debug("dropping service from result due to ACLs", "service", svc)
|
|
||||||
removed = true
|
|
||||||
info.Services = append(info.Services[:j], info.Services[j+1:]...)
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter checks
|
|
||||||
for j := 0; j < len(info.Checks); j++ {
|
|
||||||
chk := info.Checks[j]
|
|
||||||
chk.FillAuthzContext(&authzContext)
|
|
||||||
if f.allowNode(info.Node, &authzContext) && f.allowService(chk.ServiceName, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.logger.Debug("dropping check from result due to ACLs", "check", chk.CheckID)
|
|
||||||
removed = true
|
|
||||||
info.Checks = append(info.Checks[:j], info.Checks[j+1:]...)
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*dump = nd
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterServiceDump is used to filter nodes based on ACL rules. Returns true
|
|
||||||
// if any elements were removed.
|
|
||||||
func (f *aclFilter) filterServiceDump(services *structs.ServiceDump) bool {
|
|
||||||
svcs := *services
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
var removed bool
|
|
||||||
|
|
||||||
for i := 0; i < len(svcs); i++ {
|
|
||||||
service := svcs[i]
|
|
||||||
|
|
||||||
if f.allowGateway(service.GatewayService) {
|
|
||||||
// ServiceDump might only have gateway config and no node information
|
|
||||||
if service.Node == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
service.Service.FillAuthzContext(&authzContext)
|
|
||||||
if f.allowNode(service.Node.Node, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.logger.Debug("dropping service from result due to ACLs", "service", service.GatewayService.Service)
|
|
||||||
removed = true
|
|
||||||
svcs = append(svcs[:i], svcs[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
*services = svcs
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterNodes is used to filter through all parts of a node list and remove
|
|
||||||
// elements the provided ACL token cannot access. Returns true if any elements
|
|
||||||
// were removed.
|
|
||||||
func (f *aclFilter) filterNodes(nodes *structs.Nodes) bool {
|
|
||||||
n := *nodes
|
|
||||||
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
var removed bool
|
|
||||||
|
|
||||||
for i := 0; i < len(n); i++ {
|
|
||||||
n[i].FillAuthzContext(&authzContext)
|
|
||||||
node := n[i].Node
|
|
||||||
if f.allowNode(node, &authzContext) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, n[i].GetEnterpriseMeta()))
|
|
||||||
removed = true
|
|
||||||
n = append(n[:i], n[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
*nodes = n
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// redactPreparedQueryTokens will redact any tokens unless the client has a
|
|
||||||
// management token. This eases the transition to delegated authority over
|
|
||||||
// prepared queries, since it was easy to capture management tokens in Consul
|
|
||||||
// 0.6.3 and earlier, and we don't want to willy-nilly show those. This does
|
|
||||||
// have the limitation of preventing delegated non-management users from seeing
|
|
||||||
// captured tokens, but they can at least see whether or not a token is set.
|
|
||||||
func (f *aclFilter) redactPreparedQueryTokens(query **structs.PreparedQuery) {
|
|
||||||
// Management tokens can see everything with no filtering.
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
structs.DefaultEnterpriseMetaInDefaultPartition().FillAuthzContext(&authzContext)
|
|
||||||
if f.authorizer.ACLWrite(&authzContext) == acl.Allow {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let the user see if there's a blank token, otherwise we need
|
|
||||||
// to redact it, since we know they don't have a management
|
|
||||||
// token.
|
|
||||||
if (*query).Token != "" {
|
|
||||||
// Redact the token, using a copy of the query structure
|
|
||||||
// since we could be pointed at a live instance from the
|
|
||||||
// state store so it's not safe to modify it. Note that
|
|
||||||
// this clone will still point to things like underlying
|
|
||||||
// arrays in the original, but for modifying just the
|
|
||||||
// token it will be safe to use.
|
|
||||||
clone := *(*query)
|
|
||||||
clone.Token = redactedToken
|
|
||||||
*query = &clone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterPreparedQueries is used to filter prepared queries based on ACL rules.
|
|
||||||
// We prune entries the user doesn't have access to, and we redact any tokens
|
|
||||||
// if the user doesn't have a management token. Returns true if any (named)
|
|
||||||
// queries were removed - un-named queries are meant to be ephemeral and can
|
|
||||||
// only be enumerated by a management token
|
|
||||||
func (f *aclFilter) filterPreparedQueries(queries *structs.PreparedQueries) bool {
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
structs.DefaultEnterpriseMetaInDefaultPartition().FillAuthzContext(&authzContext)
|
|
||||||
// Management tokens can see everything with no filtering.
|
|
||||||
// TODO is this check even necessary - this looks like a search replace from
|
|
||||||
// the 1.4 ACL rewrite. The global-management token will provide unrestricted query privileges
|
|
||||||
// so asking for ACLWrite should be unnecessary.
|
|
||||||
if f.authorizer.ACLWrite(&authzContext) == acl.Allow {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, we need to see what the token has access to.
|
|
||||||
var namedQueriesRemoved bool
|
|
||||||
ret := make(structs.PreparedQueries, 0, len(*queries))
|
|
||||||
for _, query := range *queries {
|
|
||||||
// If no prefix ACL applies to this query then filter it, since
|
|
||||||
// we know at this point the user doesn't have a management
|
|
||||||
// token, otherwise see what the policy says.
|
|
||||||
prefix, hasName := query.GetACLPrefix()
|
|
||||||
switch {
|
|
||||||
case hasName && f.authorizer.PreparedQueryRead(prefix, &authzContext) != acl.Allow:
|
|
||||||
namedQueriesRemoved = true
|
|
||||||
fallthrough
|
|
||||||
case !hasName:
|
|
||||||
f.logger.Debug("dropping prepared query from result due to ACLs", "query", query.ID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redact any tokens if necessary. We make a copy of just the
|
|
||||||
// pointer so we don't mess with the caller's slice.
|
|
||||||
final := query
|
|
||||||
f.redactPreparedQueryTokens(&final)
|
|
||||||
ret = append(ret, final)
|
|
||||||
}
|
|
||||||
*queries = ret
|
|
||||||
return namedQueriesRemoved
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterToken(token **structs.ACLToken) {
|
|
||||||
var entCtx acl.AuthorizerContext
|
|
||||||
if token == nil || *token == nil || f == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
(*token).FillAuthzContext(&entCtx)
|
|
||||||
|
|
||||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
|
||||||
// no permissions to read
|
|
||||||
*token = nil
|
|
||||||
} else if f.authorizer.ACLWrite(&entCtx) != acl.Allow {
|
|
||||||
// no write permissions - redact secret
|
|
||||||
clone := *(*token)
|
|
||||||
clone.SecretID = redactedToken
|
|
||||||
*token = &clone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterTokens(tokens *structs.ACLTokens) {
|
|
||||||
ret := make(structs.ACLTokens, 0, len(*tokens))
|
|
||||||
for _, token := range *tokens {
|
|
||||||
final := token
|
|
||||||
f.filterToken(&final)
|
|
||||||
if final != nil {
|
|
||||||
ret = append(ret, final)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*tokens = ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterTokenStub(token **structs.ACLTokenListStub) {
|
|
||||||
var entCtx acl.AuthorizerContext
|
|
||||||
if token == nil || *token == nil || f == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
(*token).FillAuthzContext(&entCtx)
|
|
||||||
|
|
||||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
|
||||||
*token = nil
|
|
||||||
} else if f.authorizer.ACLWrite(&entCtx) != acl.Allow {
|
|
||||||
// no write permissions - redact secret
|
|
||||||
clone := *(*token)
|
|
||||||
clone.SecretID = redactedToken
|
|
||||||
*token = &clone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterTokenStubs(tokens *[]*structs.ACLTokenListStub) {
|
|
||||||
ret := make(structs.ACLTokenListStubs, 0, len(*tokens))
|
|
||||||
for _, token := range *tokens {
|
|
||||||
final := token
|
|
||||||
f.filterTokenStub(&final)
|
|
||||||
if final != nil {
|
|
||||||
ret = append(ret, final)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*tokens = ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterPolicy(policy **structs.ACLPolicy) {
|
|
||||||
var entCtx acl.AuthorizerContext
|
|
||||||
if policy == nil || *policy == nil || f == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
(*policy).FillAuthzContext(&entCtx)
|
|
||||||
|
|
||||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
|
||||||
// no permissions to read
|
|
||||||
*policy = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterPolicies(policies *structs.ACLPolicies) {
|
|
||||||
ret := make(structs.ACLPolicies, 0, len(*policies))
|
|
||||||
for _, policy := range *policies {
|
|
||||||
final := policy
|
|
||||||
f.filterPolicy(&final)
|
|
||||||
if final != nil {
|
|
||||||
ret = append(ret, final)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*policies = ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterRole(role **structs.ACLRole) {
|
|
||||||
var entCtx acl.AuthorizerContext
|
|
||||||
if role == nil || *role == nil || f == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
(*role).FillAuthzContext(&entCtx)
|
|
||||||
|
|
||||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
|
||||||
// no permissions to read
|
|
||||||
*role = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterRoles(roles *structs.ACLRoles) {
|
|
||||||
ret := make(structs.ACLRoles, 0, len(*roles))
|
|
||||||
for _, role := range *roles {
|
|
||||||
final := role
|
|
||||||
f.filterRole(&final)
|
|
||||||
if final != nil {
|
|
||||||
ret = append(ret, final)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*roles = ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterBindingRule(rule **structs.ACLBindingRule) {
|
|
||||||
var entCtx acl.AuthorizerContext
|
|
||||||
if rule == nil || *rule == nil || f == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
(*rule).FillAuthzContext(&entCtx)
|
|
||||||
|
|
||||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
|
||||||
// no permissions to read
|
|
||||||
*rule = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterBindingRules(rules *structs.ACLBindingRules) {
|
|
||||||
ret := make(structs.ACLBindingRules, 0, len(*rules))
|
|
||||||
for _, rule := range *rules {
|
|
||||||
final := rule
|
|
||||||
f.filterBindingRule(&final)
|
|
||||||
if final != nil {
|
|
||||||
ret = append(ret, final)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*rules = ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterAuthMethod(method **structs.ACLAuthMethod) {
|
|
||||||
var entCtx acl.AuthorizerContext
|
|
||||||
if method == nil || *method == nil || f == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
(*method).FillAuthzContext(&entCtx)
|
|
||||||
|
|
||||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
|
||||||
// no permissions to read
|
|
||||||
*method = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterAuthMethods(methods *structs.ACLAuthMethods) {
|
|
||||||
ret := make(structs.ACLAuthMethods, 0, len(*methods))
|
|
||||||
for _, method := range *methods {
|
|
||||||
final := method
|
|
||||||
f.filterAuthMethod(&final)
|
|
||||||
if final != nil {
|
|
||||||
ret = append(ret, final)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*methods = ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *aclFilter) filterServiceList(services *structs.ServiceList) bool {
|
|
||||||
ret := make(structs.ServiceList, 0, len(*services))
|
|
||||||
var removed bool
|
|
||||||
for _, svc := range *services {
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
|
|
||||||
svc.FillAuthzContext(&authzContext)
|
|
||||||
|
|
||||||
if f.authorizer.ServiceRead(svc.Name, &authzContext) != acl.Allow {
|
|
||||||
removed = true
|
|
||||||
sid := structs.NewServiceID(svc.Name, &svc.EnterpriseMeta)
|
|
||||||
f.logger.Debug("dropping service from result due to ACLs", "service", sid.String())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, svc)
|
|
||||||
}
|
|
||||||
|
|
||||||
*services = ret
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterGatewayServices is used to filter gateway to service mappings based on ACL rules.
|
|
||||||
// Returns true if any elements were removed.
|
|
||||||
func (f *aclFilter) filterGatewayServices(mappings *structs.GatewayServices) bool {
|
|
||||||
ret := make(structs.GatewayServices, 0, len(*mappings))
|
|
||||||
var removed bool
|
|
||||||
for _, s := range *mappings {
|
|
||||||
// This filter only checks ServiceRead on the linked service.
|
|
||||||
// ServiceRead on the gateway is checked in the GatewayServices endpoint before filtering.
|
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
s.Service.FillAuthzContext(&authzContext)
|
|
||||||
|
|
||||||
if f.authorizer.ServiceRead(s.Service.Name, &authzContext) != acl.Allow {
|
|
||||||
f.logger.Debug("dropping service from result due to ACLs", "service", s.Service.String())
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ret = append(ret, s)
|
|
||||||
}
|
|
||||||
*mappings = ret
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterACLWithAuthorizer(logger hclog.Logger, authorizer acl.Authorizer, subj interface{}) {
|
func filterACLWithAuthorizer(logger hclog.Logger, authorizer acl.Authorizer, subj interface{}) {
|
||||||
if authorizer == nil {
|
aclfilter.New(authorizer, logger).Filter(subj)
|
||||||
return
|
|
||||||
}
|
|
||||||
filt := newACLFilter(authorizer, logger)
|
|
||||||
|
|
||||||
switch v := subj.(type) {
|
|
||||||
case *structs.CheckServiceNodes:
|
|
||||||
filt.filterCheckServiceNodes(v)
|
|
||||||
|
|
||||||
case *structs.IndexedCheckServiceNodes:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterCheckServiceNodes(&v.Nodes)
|
|
||||||
|
|
||||||
case *structs.PreparedQueryExecuteResponse:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterCheckServiceNodes(&v.Nodes)
|
|
||||||
|
|
||||||
case *structs.IndexedServiceTopology:
|
|
||||||
filtered := filt.filterServiceTopology(v.ServiceTopology)
|
|
||||||
if filtered {
|
|
||||||
v.FilteredByACLs = true
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = true
|
|
||||||
}
|
|
||||||
|
|
||||||
case *structs.DatacenterIndexedCheckServiceNodes:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterDatacenterCheckServiceNodes(&v.DatacenterNodes)
|
|
||||||
|
|
||||||
case *structs.IndexedCoordinates:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterCoordinates(&v.Coordinates)
|
|
||||||
|
|
||||||
case *structs.IndexedHealthChecks:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterHealthChecks(&v.HealthChecks)
|
|
||||||
|
|
||||||
case *structs.IndexedIntentions:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterIntentions(&v.Intentions)
|
|
||||||
|
|
||||||
case *structs.IndexedNodeDump:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterNodeDump(&v.Dump)
|
|
||||||
|
|
||||||
case *structs.IndexedServiceDump:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServiceDump(&v.Dump)
|
|
||||||
|
|
||||||
case *structs.IndexedNodes:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterNodes(&v.Nodes)
|
|
||||||
|
|
||||||
case *structs.IndexedNodeServices:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterNodeServices(&v.NodeServices)
|
|
||||||
|
|
||||||
case *structs.IndexedNodeServiceList:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterNodeServiceList(&v.NodeServices)
|
|
||||||
|
|
||||||
case *structs.IndexedServiceNodes:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServiceNodes(&v.ServiceNodes)
|
|
||||||
|
|
||||||
case *structs.IndexedServices:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServices(v.Services, &v.EnterpriseMeta)
|
|
||||||
|
|
||||||
case *structs.IndexedSessions:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterSessions(&v.Sessions)
|
|
||||||
|
|
||||||
case *structs.IndexedPreparedQueries:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterPreparedQueries(&v.Queries)
|
|
||||||
|
|
||||||
case **structs.PreparedQuery:
|
|
||||||
filt.redactPreparedQueryTokens(v)
|
|
||||||
|
|
||||||
case *structs.ACLTokens:
|
|
||||||
filt.filterTokens(v)
|
|
||||||
case **structs.ACLToken:
|
|
||||||
filt.filterToken(v)
|
|
||||||
case *[]*structs.ACLTokenListStub:
|
|
||||||
filt.filterTokenStubs(v)
|
|
||||||
case **structs.ACLTokenListStub:
|
|
||||||
filt.filterTokenStub(v)
|
|
||||||
|
|
||||||
case *structs.ACLPolicies:
|
|
||||||
filt.filterPolicies(v)
|
|
||||||
case **structs.ACLPolicy:
|
|
||||||
filt.filterPolicy(v)
|
|
||||||
|
|
||||||
case *structs.ACLRoles:
|
|
||||||
filt.filterRoles(v)
|
|
||||||
case **structs.ACLRole:
|
|
||||||
filt.filterRole(v)
|
|
||||||
|
|
||||||
case *structs.ACLBindingRules:
|
|
||||||
filt.filterBindingRules(v)
|
|
||||||
case **structs.ACLBindingRule:
|
|
||||||
filt.filterBindingRule(v)
|
|
||||||
|
|
||||||
case *structs.ACLAuthMethods:
|
|
||||||
filt.filterAuthMethods(v)
|
|
||||||
case **structs.ACLAuthMethod:
|
|
||||||
filt.filterAuthMethod(v)
|
|
||||||
|
|
||||||
case *structs.IndexedServiceList:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServiceList(&v.Services)
|
|
||||||
|
|
||||||
case *structs.IndexedExportedServiceList:
|
|
||||||
for peer, peerServices := range v.Services {
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServiceList(&peerServices)
|
|
||||||
if len(peerServices) == 0 {
|
|
||||||
delete(v.Services, peer)
|
|
||||||
} else {
|
|
||||||
v.Services[peer] = peerServices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case *structs.IndexedGatewayServices:
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterGatewayServices(&v.Services)
|
|
||||||
|
|
||||||
case *structs.IndexedNodesWithGateways:
|
|
||||||
if filt.filterCheckServiceNodes(&v.Nodes) {
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = true
|
|
||||||
}
|
|
||||||
if filt.filterGatewayServices(&v.Gateways) {
|
|
||||||
v.QueryMeta.ResultsFilteredByACLs = true
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("Unhandled type passed to ACL filter: %T %#v", subj, subj))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterACL uses the ACLResolver to resolve the token in an acl.Authorizer,
|
// filterACL uses the ACLResolver to resolve the token in an acl.Authorizer,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/hashicorp/consul/agent/consul/authmethod"
|
"github.com/hashicorp/consul/agent/consul/authmethod"
|
||||||
"github.com/hashicorp/consul/agent/consul/state"
|
"github.com/hashicorp/consul/agent/consul/state"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
"github.com/hashicorp/consul/lib"
|
"github.com/hashicorp/consul/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -291,7 +292,7 @@ func (a *ACL) TokenRead(args *structs.ACLTokenGetRequest, reply *structs.ACLToke
|
||||||
a.srv.filterACLWithAuthorizer(authz, &token)
|
a.srv.filterACLWithAuthorizer(authz, &token)
|
||||||
|
|
||||||
// token secret was redacted
|
// token secret was redacted
|
||||||
if token.SecretID == redactedToken {
|
if token.SecretID == aclfilter.RedactedToken {
|
||||||
reply.Redacted = true
|
reply.Redacted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -719,7 +720,7 @@ func (a *ACL) TokenBatchRead(args *structs.ACLTokenBatchGetRequest, reply *struc
|
||||||
a.srv.filterACLWithAuthorizer(authz, &final)
|
a.srv.filterACLWithAuthorizer(authz, &final)
|
||||||
if final != nil {
|
if final != nil {
|
||||||
ret = append(ret, final)
|
ret = append(ret, final)
|
||||||
if final.SecretID == redactedToken {
|
if final.SecretID == aclfilter.RedactedToken {
|
||||||
reply.Redacted = true
|
reply.Redacted = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"github.com/hashicorp/consul/agent/consul/authmethod/kubeauth"
|
"github.com/hashicorp/consul/agent/consul/authmethod/kubeauth"
|
||||||
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
|
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
|
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
|
||||||
"github.com/hashicorp/consul/sdk/testutil"
|
"github.com/hashicorp/consul/sdk/testutil"
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
|
@ -1854,7 +1855,7 @@ func TestACLEndpoint_TokenList(t *testing.T) {
|
||||||
}
|
}
|
||||||
require.ElementsMatch(t, gatherIDs(t, resp.Tokens), tokens)
|
require.ElementsMatch(t, gatherIDs(t, resp.Tokens), tokens)
|
||||||
for _, token := range resp.Tokens {
|
for _, token := range resp.Tokens {
|
||||||
require.Equal(t, redactedToken, token.SecretID)
|
require.Equal(t, aclfilter.RedactedToken, token.SecretID)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
|
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
tokenStore "github.com/hashicorp/consul/agent/token"
|
tokenStore "github.com/hashicorp/consul/agent/token"
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
"github.com/hashicorp/consul/testrpc"
|
"github.com/hashicorp/consul/testrpc"
|
||||||
|
@ -752,9 +753,9 @@ func TestACLReplication_TokensRedacted(t *testing.T) {
|
||||||
var tokenResp structs.ACLTokenResponse
|
var tokenResp structs.ACLTokenResponse
|
||||||
req := structs.ACLTokenGetRequest{
|
req := structs.ACLTokenGetRequest{
|
||||||
Datacenter: "dc2",
|
Datacenter: "dc2",
|
||||||
TokenID: redactedToken,
|
TokenID: aclfilter.RedactedToken,
|
||||||
TokenIDType: structs.ACLTokenSecret,
|
TokenIDType: structs.ACLTokenSecret,
|
||||||
QueryOptions: structs.QueryOptions{Token: redactedToken},
|
QueryOptions: structs.QueryOptions{Token: aclfilter.RedactedToken},
|
||||||
}
|
}
|
||||||
err := s2.RPC("ACL.TokenRead", &req, &tokenResp)
|
err := s2.RPC("ACL.TokenRead", &req, &tokenResp)
|
||||||
// its not an error for the secret to not be found.
|
// its not an error for the secret to not be found.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
)
|
)
|
||||||
|
|
||||||
type aclTokenReplicator struct {
|
type aclTokenReplicator struct {
|
||||||
|
@ -99,7 +100,7 @@ func (r *aclTokenReplicator) PendingUpdateEstimatedSize(i int) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *aclTokenReplicator) PendingUpdateIsRedacted(i int) bool {
|
func (r *aclTokenReplicator) PendingUpdateIsRedacted(i int) bool {
|
||||||
return r.updated[i].SecretID == redactedToken
|
return r.updated[i].SecretID == aclfilter.RedactedToken
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *aclTokenReplicator) UpdateLocalBatch(ctx context.Context, srv *Server, start, end int) error {
|
func (r *aclTokenReplicator) UpdateLocalBatch(ctx context.Context, srv *Server, start, end int) error {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/metadata"
|
"github.com/hashicorp/consul/agent/metadata"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/lib"
|
"github.com/hashicorp/consul/lib"
|
||||||
"github.com/hashicorp/consul/logging"
|
"github.com/hashicorp/consul/logging"
|
||||||
|
@ -385,7 +386,7 @@ func (s *Server) initializeACLs(ctx context.Context) error {
|
||||||
|
|
||||||
// Remove any token affected by CVE-2019-8336
|
// Remove any token affected by CVE-2019-8336
|
||||||
if !s.InPrimaryDatacenter() {
|
if !s.InPrimaryDatacenter() {
|
||||||
_, token, err := s.fsm.State().ACLTokenGetBySecret(nil, redactedToken, nil)
|
_, token, err := s.fsm.State().ACLTokenGetBySecret(nil, aclfilter.RedactedToken, nil)
|
||||||
if err == nil && token != nil {
|
if err == nil && token != nil {
|
||||||
req := structs.ACLTokenBatchDeleteRequest{
|
req := structs.ACLTokenBatchDeleteRequest{
|
||||||
TokenIDs: []string{token.AccessorID},
|
TokenIDs: []string{token.AccessorID},
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/consul/state"
|
"github.com/hashicorp/consul/agent/consul/state"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
"github.com/hashicorp/consul/logging"
|
"github.com/hashicorp/consul/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -159,7 +160,7 @@ func parseQuery(query *structs.PreparedQuery) error {
|
||||||
// Token is checked when the query is executed, but we do make sure the
|
// Token is checked when the query is executed, but we do make sure the
|
||||||
// user hasn't accidentally pasted-in the special redacted token name,
|
// user hasn't accidentally pasted-in the special redacted token name,
|
||||||
// which if we allowed in would be super hard to debug and understand.
|
// which if we allowed in would be super hard to debug and understand.
|
||||||
if query.Token == redactedToken {
|
if query.Token == aclfilter.RedactedToken {
|
||||||
return fmt.Errorf("Bad Token '%s', it looks like a query definition with a redacted token was submitted", query.Token)
|
return fmt.Errorf("Bad Token '%s', it looks like a query definition with a redacted token was submitted", query.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
tokenStore "github.com/hashicorp/consul/agent/token"
|
tokenStore "github.com/hashicorp/consul/agent/token"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
|
@ -570,7 +571,7 @@ func TestPreparedQuery_parseQuery(t *testing.T) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
query.Token = redactedToken
|
query.Token = aclfilter.RedactedToken
|
||||||
err = parseQuery(query)
|
err = parseQuery(query)
|
||||||
if err == nil || !strings.Contains(err.Error(), "Bad Token") {
|
if err == nil || !strings.Contains(err.Error(), "Bad Token") {
|
||||||
t.Fatalf("bad: %v", err)
|
t.Fatalf("bad: %v", err)
|
||||||
|
@ -680,7 +681,7 @@ func TestPreparedQuery_ACLDeny_Catchall_Template(t *testing.T) {
|
||||||
// Capture the ID and read back the query to verify. Note that the token
|
// Capture the ID and read back the query to verify. Note that the token
|
||||||
// will be redacted since this isn't a management token.
|
// will be redacted since this isn't a management token.
|
||||||
query.Query.ID = reply
|
query.Query.ID = reply
|
||||||
query.Query.Token = redactedToken
|
query.Query.Token = aclfilter.RedactedToken
|
||||||
{
|
{
|
||||||
req := &structs.PreparedQuerySpecificRequest{
|
req := &structs.PreparedQuerySpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
|
@ -779,7 +780,7 @@ func TestPreparedQuery_ACLDeny_Catchall_Template(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user can explain and see the redacted token.
|
// The user can explain and see the redacted token.
|
||||||
query.Query.Token = redactedToken
|
query.Query.Token = aclfilter.RedactedToken
|
||||||
query.Query.Service.Service = "anything"
|
query.Query.Service.Service = "anything"
|
||||||
{
|
{
|
||||||
req := &structs.PreparedQueryExecuteRequest{
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
@ -993,7 +994,7 @@ func TestPreparedQuery_Get(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should get redacted when we read it back without a token.
|
// This should get redacted when we read it back without a token.
|
||||||
query.Query.Token = redactedToken
|
query.Query.Token = aclfilter.RedactedToken
|
||||||
{
|
{
|
||||||
req := &structs.PreparedQuerySpecificRequest{
|
req := &structs.PreparedQuerySpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
|
@ -1127,7 +1128,7 @@ func TestPreparedQuery_List(t *testing.T) {
|
||||||
// Capture the ID and read back the query to verify. We also make sure
|
// Capture the ID and read back the query to verify. We also make sure
|
||||||
// the captured token gets redacted.
|
// the captured token gets redacted.
|
||||||
query.Query.ID = reply
|
query.Query.ID = reply
|
||||||
query.Query.Token = redactedToken
|
query.Query.Token = aclfilter.RedactedToken
|
||||||
{
|
{
|
||||||
req := &structs.DCSpecificRequest{
|
req := &structs.DCSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
|
@ -1355,7 +1356,7 @@ func TestPreparedQuery_Explain(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explain via the user token, which will redact the captured token.
|
// Explain via the user token, which will redact the captured token.
|
||||||
query.Query.Token = redactedToken
|
query.Query.Token = aclfilter.RedactedToken
|
||||||
query.Query.Service.Service = "prod-redis"
|
query.Query.Service.Service = "prod-redis"
|
||||||
{
|
{
|
||||||
req := &structs.PreparedQueryExecuteRequest{
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
|
|
@ -23,6 +23,7 @@ type ServerDataSourceDeps struct {
|
||||||
EventPublisher *stream.EventPublisher
|
EventPublisher *stream.EventPublisher
|
||||||
Logger hclog.Logger
|
Logger hclog.Logger
|
||||||
ACLResolver submatview.ACLResolver
|
ACLResolver submatview.ACLResolver
|
||||||
|
GetStore func() Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfigEntry satisfies the proxycfg.ConfigEntry interface by sourcing
|
// ServerConfigEntry satisfies the proxycfg.ConfigEntry interface by sourcing
|
||||||
|
@ -46,7 +47,7 @@ func (e serverConfigEntry) Notify(ctx context.Context, req *structs.ConfigEntryQ
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return e.deps.ViewStore.NotifyCallback(ctx, cfgReq, correlationID, dispatchCacheUpdate(ctx, ch))
|
return e.deps.ViewStore.NotifyCallback(ctx, cfgReq, correlationID, dispatchCacheUpdate(ch))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfigEntryRequest(req *structs.ConfigEntryQuery, deps ServerDataSourceDeps) (*configEntryRequest, error) {
|
func newConfigEntryRequest(req *structs.ConfigEntryQuery, deps ServerDataSourceDeps) (*configEntryRequest, error) {
|
||||||
|
|
|
@ -3,14 +3,25 @@ package proxycfgglue
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-memdb"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/cache"
|
"github.com/hashicorp/consul/agent/cache"
|
||||||
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
||||||
|
"github.com/hashicorp/consul/agent/consul/watch"
|
||||||
"github.com/hashicorp/consul/agent/proxycfg"
|
"github.com/hashicorp/consul/agent/proxycfg"
|
||||||
"github.com/hashicorp/consul/agent/rpcclient/health"
|
"github.com/hashicorp/consul/agent/rpcclient/health"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/proto/pbpeering"
|
"github.com/hashicorp/consul/proto/pbpeering"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Store is the state store interface required for server-local data sources.
|
||||||
|
type Store interface {
|
||||||
|
watch.StateStore
|
||||||
|
|
||||||
|
IntentionTopology(ws memdb.WatchSet, target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision, intentionTarget structs.IntentionTargetType) (uint64, structs.ServiceList, error)
|
||||||
|
}
|
||||||
|
|
||||||
// CacheCARoots satisfies the proxycfg.CARoots interface by sourcing data from
|
// CacheCARoots satisfies the proxycfg.CARoots interface by sourcing data from
|
||||||
// the agent cache.
|
// the agent cache.
|
||||||
func CacheCARoots(c *cache.Cache) proxycfg.CARoots {
|
func CacheCARoots(c *cache.Cache) proxycfg.CARoots {
|
||||||
|
@ -134,7 +145,7 @@ func (c *cacheProxyDataSource[ReqType]) Notify(
|
||||||
correlationID string,
|
correlationID string,
|
||||||
ch chan<- proxycfg.UpdateEvent,
|
ch chan<- proxycfg.UpdateEvent,
|
||||||
) error {
|
) error {
|
||||||
return c.c.NotifyCallback(ctx, c.t, req, correlationID, dispatchCacheUpdate(ctx, ch))
|
return c.c.NotifyCallback(ctx, c.t, req, correlationID, dispatchCacheUpdate(ch))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health wraps health.Client so that the proxycfg package doesn't need to
|
// Health wraps health.Client so that the proxycfg package doesn't need to
|
||||||
|
@ -153,10 +164,10 @@ func (h *healthWrapper) Notify(
|
||||||
correlationID string,
|
correlationID string,
|
||||||
ch chan<- proxycfg.UpdateEvent,
|
ch chan<- proxycfg.UpdateEvent,
|
||||||
) error {
|
) error {
|
||||||
return h.client.Notify(ctx, *req, correlationID, dispatchCacheUpdate(ctx, ch))
|
return h.client.Notify(ctx, *req, correlationID, dispatchCacheUpdate(ch))
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchCacheUpdate(ctx context.Context, ch chan<- proxycfg.UpdateEvent) cache.Callback {
|
func dispatchCacheUpdate(ch chan<- proxycfg.UpdateEvent) cache.Callback {
|
||||||
return func(ctx context.Context, e cache.UpdateEvent) {
|
return func(ctx context.Context, e cache.UpdateEvent) {
|
||||||
u := proxycfg.UpdateEvent{
|
u := proxycfg.UpdateEvent{
|
||||||
CorrelationID: e.CorrelationID,
|
CorrelationID: e.CorrelationID,
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package proxycfgglue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-memdb"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/consul/watch"
|
||||||
|
"github.com/hashicorp/consul/agent/proxycfg"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerIntentionUpstreams satisfies the proxycfg.IntentionUpstreams interface
|
||||||
|
// by sourcing data from a blocking query against the server's state store.
|
||||||
|
func ServerIntentionUpstreams(deps ServerDataSourceDeps) proxycfg.IntentionUpstreams {
|
||||||
|
return serverIntentionUpstreams{deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverIntentionUpstreams struct {
|
||||||
|
deps ServerDataSourceDeps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s serverIntentionUpstreams) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error {
|
||||||
|
target := structs.NewServiceName(req.ServiceName, &req.EnterpriseMeta)
|
||||||
|
|
||||||
|
return watch.ServerLocalNotify(ctx, correlationID, s.deps.GetStore,
|
||||||
|
func(ws memdb.WatchSet, store Store) (uint64, *structs.IndexedServiceList, error) {
|
||||||
|
authz, err := s.deps.ACLResolver.ResolveTokenAndDefaultMeta(req.Token, &req.EnterpriseMeta, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
defaultDecision := authz.IntentionDefaultAllow(nil)
|
||||||
|
|
||||||
|
index, services, err := store.IntentionTopology(ws, target, false, defaultDecision, structs.IntentionTargetService)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &structs.IndexedServiceList{
|
||||||
|
Services: services,
|
||||||
|
QueryMeta: structs.QueryMeta{
|
||||||
|
Index: index,
|
||||||
|
Backend: structs.QueryBackendBlocking,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
aclfilter.New(authz, s.deps.Logger).Filter(result)
|
||||||
|
|
||||||
|
return index, result, nil
|
||||||
|
},
|
||||||
|
dispatchBlockingQueryUpdate[*structs.IndexedServiceList](ch),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dispatchBlockingQueryUpdate[ResultType any](ch chan<- proxycfg.UpdateEvent) func(context.Context, string, ResultType, error) {
|
||||||
|
return func(ctx context.Context, correlationID string, result ResultType, err error) {
|
||||||
|
event := proxycfg.UpdateEvent{
|
||||||
|
CorrelationID: correlationID,
|
||||||
|
Result: result,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case ch <- event:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package proxycfgglue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/consul/agent/consul/state"
|
||||||
|
"github.com/hashicorp/consul/agent/proxycfg"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerIntentionUpstreams(t *testing.T) {
|
||||||
|
const serviceName = "web"
|
||||||
|
|
||||||
|
var index uint64
|
||||||
|
getIndex := func() uint64 {
|
||||||
|
index++
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
store := state.NewStateStore(nil)
|
||||||
|
disableLegacyIntentions(t, store)
|
||||||
|
|
||||||
|
// Register api and db services.
|
||||||
|
for _, service := range []string{"api", "db"} {
|
||||||
|
err := store.EnsureRegistration(getIndex(), &structs.RegisterRequest{
|
||||||
|
Node: "node-1",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Service: service,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createIntention := func(destination string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
err := store.EnsureConfigEntry(getIndex(), &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Name: destination,
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{
|
||||||
|
Name: serviceName,
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
Type: structs.IntentionSourceConsul,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an allow intention for the api service. This should be filtered out
|
||||||
|
// because the ACL token doesn't have read access on it.
|
||||||
|
createIntention("api")
|
||||||
|
|
||||||
|
authz := policyAuthorizer(t, `service "db" { policy = "read" }`)
|
||||||
|
|
||||||
|
dataSource := ServerIntentionUpstreams(ServerDataSourceDeps{
|
||||||
|
ACLResolver: staticResolver{authz},
|
||||||
|
GetStore: func() Store { return store },
|
||||||
|
})
|
||||||
|
|
||||||
|
ch := make(chan proxycfg.UpdateEvent)
|
||||||
|
err := dataSource.Notify(ctx, &structs.ServiceSpecificRequest{ServiceName: serviceName}, "", ch)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case event := <-ch:
|
||||||
|
result, ok := event.Result.(*structs.IndexedServiceList)
|
||||||
|
require.Truef(t, ok, "expected IndexedServiceList, got: %T", event.Result)
|
||||||
|
require.Len(t, result.Services, 0)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("timeout waiting for event")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an allow intention for the db service. This should *not* be filtered
|
||||||
|
// out because the ACL token *does* have read access on it.
|
||||||
|
createIntention("db")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case event := <-ch:
|
||||||
|
result, ok := event.Result.(*structs.IndexedServiceList)
|
||||||
|
require.Truef(t, ok, "expected IndexedServiceList, got: %T", event.Result)
|
||||||
|
require.Len(t, result.Services, 1)
|
||||||
|
require.Equal(t, "db", result.Services[0].Name)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("timeout waiting for event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableLegacyIntentions(t *testing.T, store *state.Store) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
require.NoError(t, store.SystemMetadataSet(0, &structs.SystemMetadataEntry{
|
||||||
|
Key: structs.SystemMetadataIntentionFormatKey,
|
||||||
|
Value: structs.SystemMetadataIntentionFormatConfigValue,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func policyAuthorizer(t *testing.T, policyHCL string) acl.Authorizer {
|
||||||
|
policy, err := acl.NewPolicyFromSource(policyHCL, acl.SyntaxCurrent, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return authz
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ func TestServerIntentions_Enterprise(t *testing.T) {
|
||||||
go publisher.Run(ctx)
|
go publisher.Run(ctx)
|
||||||
|
|
||||||
intentions := ServerIntentions(ServerDataSourceDeps{
|
intentions := ServerIntentions(ServerDataSourceDeps{
|
||||||
ACLResolver: manageAllResolver{},
|
ACLResolver: staticResolver{acl.ManageAll()},
|
||||||
ViewStore: store,
|
ViewStore: store,
|
||||||
EventPublisher: publisher,
|
EventPublisher: publisher,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
|
|
|
@ -39,7 +39,7 @@ func TestServerIntentions(t *testing.T) {
|
||||||
go publisher.Run(ctx)
|
go publisher.Run(ctx)
|
||||||
|
|
||||||
intentions := ServerIntentions(ServerDataSourceDeps{
|
intentions := ServerIntentions(ServerDataSourceDeps{
|
||||||
ACLResolver: manageAllResolver{},
|
ACLResolver: staticResolver{acl.ManageAll()},
|
||||||
ViewStore: store,
|
ViewStore: store,
|
||||||
EventPublisher: publisher,
|
EventPublisher: publisher,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
|
@ -146,8 +146,10 @@ func TestServerIntentions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type manageAllResolver struct{}
|
type staticResolver struct {
|
||||||
|
authorizer acl.Authorizer
|
||||||
func (manageAllResolver) ResolveTokenAndDefaultMeta(token string, entMeta *acl.EnterpriseMeta, authzContext *acl.AuthorizerContext) (resolver.Result, error) {
|
}
|
||||||
return resolver.Result{Authorizer: acl.ManageAll()}, nil
|
|
||||||
|
func (r staticResolver) ResolveTokenAndDefaultMeta(token string, entMeta *acl.EnterpriseMeta, authzContext *acl.AuthorizerContext) (resolver.Result, error) {
|
||||||
|
return resolver.Result{Authorizer: r.authorizer}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,820 @@
|
||||||
|
package aclfilter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-hclog"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RedactedToken is shown in structures with embedded tokens when they
|
||||||
|
// are not allowed to be displayed.
|
||||||
|
RedactedToken = "<hidden>"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter is used to filter results based on ACL rules.
|
||||||
|
type Filter struct {
|
||||||
|
authorizer acl.Authorizer
|
||||||
|
logger hclog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New constructs a Filter with the given authorizer.
|
||||||
|
func New(authorizer acl.Authorizer, logger hclog.Logger) *Filter {
|
||||||
|
if logger == nil {
|
||||||
|
logger = hclog.NewNullLogger()
|
||||||
|
}
|
||||||
|
return &Filter{authorizer, logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the given subject in-place.
|
||||||
|
func (f *Filter) Filter(subject any) {
|
||||||
|
switch v := subject.(type) {
|
||||||
|
case *structs.CheckServiceNodes:
|
||||||
|
f.filterCheckServiceNodes(v)
|
||||||
|
|
||||||
|
case *structs.IndexedCheckServiceNodes:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterCheckServiceNodes(&v.Nodes)
|
||||||
|
|
||||||
|
case *structs.PreparedQueryExecuteResponse:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterCheckServiceNodes(&v.Nodes)
|
||||||
|
|
||||||
|
case *structs.IndexedServiceTopology:
|
||||||
|
filtered := f.filterServiceTopology(v.ServiceTopology)
|
||||||
|
if filtered {
|
||||||
|
v.FilteredByACLs = true
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case *structs.DatacenterIndexedCheckServiceNodes:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterDatacenterCheckServiceNodes(&v.DatacenterNodes)
|
||||||
|
|
||||||
|
case *structs.IndexedCoordinates:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterCoordinates(&v.Coordinates)
|
||||||
|
|
||||||
|
case *structs.IndexedHealthChecks:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterHealthChecks(&v.HealthChecks)
|
||||||
|
|
||||||
|
case *structs.IndexedIntentions:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterIntentions(&v.Intentions)
|
||||||
|
|
||||||
|
case *structs.IndexedNodeDump:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterNodeDump(&v.Dump)
|
||||||
|
|
||||||
|
case *structs.IndexedServiceDump:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterServiceDump(&v.Dump)
|
||||||
|
|
||||||
|
case *structs.IndexedNodes:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterNodes(&v.Nodes)
|
||||||
|
|
||||||
|
case *structs.IndexedNodeServices:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterNodeServices(&v.NodeServices)
|
||||||
|
|
||||||
|
case *structs.IndexedNodeServiceList:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterNodeServiceList(&v.NodeServices)
|
||||||
|
|
||||||
|
case *structs.IndexedServiceNodes:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterServiceNodes(&v.ServiceNodes)
|
||||||
|
|
||||||
|
case *structs.IndexedServices:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterServices(v.Services, &v.EnterpriseMeta)
|
||||||
|
|
||||||
|
case *structs.IndexedSessions:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterSessions(&v.Sessions)
|
||||||
|
|
||||||
|
case *structs.IndexedPreparedQueries:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterPreparedQueries(&v.Queries)
|
||||||
|
|
||||||
|
case **structs.PreparedQuery:
|
||||||
|
f.redactPreparedQueryTokens(v)
|
||||||
|
|
||||||
|
case *structs.ACLTokens:
|
||||||
|
f.filterTokens(v)
|
||||||
|
case **structs.ACLToken:
|
||||||
|
f.filterToken(v)
|
||||||
|
case *[]*structs.ACLTokenListStub:
|
||||||
|
f.filterTokenStubs(v)
|
||||||
|
case **structs.ACLTokenListStub:
|
||||||
|
f.filterTokenStub(v)
|
||||||
|
|
||||||
|
case *structs.ACLPolicies:
|
||||||
|
f.filterPolicies(v)
|
||||||
|
case **structs.ACLPolicy:
|
||||||
|
f.filterPolicy(v)
|
||||||
|
|
||||||
|
case *structs.ACLRoles:
|
||||||
|
f.filterRoles(v)
|
||||||
|
case **structs.ACLRole:
|
||||||
|
f.filterRole(v)
|
||||||
|
|
||||||
|
case *structs.ACLBindingRules:
|
||||||
|
f.filterBindingRules(v)
|
||||||
|
case **structs.ACLBindingRule:
|
||||||
|
f.filterBindingRule(v)
|
||||||
|
|
||||||
|
case *structs.ACLAuthMethods:
|
||||||
|
f.filterAuthMethods(v)
|
||||||
|
case **structs.ACLAuthMethod:
|
||||||
|
f.filterAuthMethod(v)
|
||||||
|
|
||||||
|
case *structs.IndexedServiceList:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterServiceList(&v.Services)
|
||||||
|
|
||||||
|
case *structs.IndexedExportedServiceList:
|
||||||
|
for peer, peerServices := range v.Services {
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterServiceList(&peerServices)
|
||||||
|
if len(peerServices) == 0 {
|
||||||
|
delete(v.Services, peer)
|
||||||
|
} else {
|
||||||
|
v.Services[peer] = peerServices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *structs.IndexedGatewayServices:
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = f.filterGatewayServices(&v.Services)
|
||||||
|
|
||||||
|
case *structs.IndexedNodesWithGateways:
|
||||||
|
if f.filterCheckServiceNodes(&v.Nodes) {
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = true
|
||||||
|
}
|
||||||
|
if f.filterGatewayServices(&v.Gateways) {
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("Unhandled type passed to ACL filter: %T %#v", subject, subject))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowNode is used to determine if a node is accessible for an ACL.
|
||||||
|
func (f *Filter) allowNode(node string, ent *acl.AuthorizerContext) bool {
|
||||||
|
return f.authorizer.NodeRead(node, ent) == acl.Allow
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowNode is used to determine if the gateway and service are accessible for an ACL
|
||||||
|
func (f *Filter) allowGateway(gs *structs.GatewayService) bool {
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
|
||||||
|
// Need read on service and gateway. Gateway may have different EnterpriseMeta so we fill authzContext twice
|
||||||
|
gs.Gateway.FillAuthzContext(&authzContext)
|
||||||
|
if !f.allowService(gs.Gateway.Name, &authzContext) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
gs.Service.FillAuthzContext(&authzContext)
|
||||||
|
if !f.allowService(gs.Service.Name, &authzContext) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowService is used to determine if a service is accessible for an ACL.
|
||||||
|
func (f *Filter) allowService(service string, ent *acl.AuthorizerContext) bool {
|
||||||
|
if service == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.authorizer.ServiceRead(service, ent) == acl.Allow
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowSession is used to determine if a session for a node is accessible for
|
||||||
|
// an ACL.
|
||||||
|
func (f *Filter) allowSession(node string, ent *acl.AuthorizerContext) bool {
|
||||||
|
return f.authorizer.SessionRead(node, ent) == acl.Allow
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterHealthChecks is used to filter a set of health checks down based on
|
||||||
|
// the configured ACL rules for a token. Returns true if any elements were
|
||||||
|
// removed.
|
||||||
|
func (f *Filter) filterHealthChecks(checks *structs.HealthChecks) bool {
|
||||||
|
hc := *checks
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
var removed bool
|
||||||
|
|
||||||
|
for i := 0; i < len(hc); i++ {
|
||||||
|
check := hc[i]
|
||||||
|
check.FillAuthzContext(&authzContext)
|
||||||
|
if f.allowNode(check.Node, &authzContext) && f.allowService(check.ServiceName, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f.logger.Debug("dropping check from result due to ACLs", "check", check.CheckID)
|
||||||
|
removed = true
|
||||||
|
hc = append(hc[:i], hc[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
*checks = hc
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterServices is used to filter a set of services based on ACLs. Returns
|
||||||
|
// true if any elements were removed.
|
||||||
|
func (f *Filter) filterServices(services structs.Services, entMeta *acl.EnterpriseMeta) bool {
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
entMeta.FillAuthzContext(&authzContext)
|
||||||
|
|
||||||
|
var removed bool
|
||||||
|
|
||||||
|
for svc := range services {
|
||||||
|
if f.allowService(svc, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.logger.Debug("dropping service from result due to ACLs", "service", svc)
|
||||||
|
removed = true
|
||||||
|
delete(services, svc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterServiceNodes is used to filter a set of nodes for a given service
|
||||||
|
// based on the configured ACL rules. Returns true if any elements were removed.
|
||||||
|
func (f *Filter) filterServiceNodes(nodes *structs.ServiceNodes) bool {
|
||||||
|
sn := *nodes
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
var removed bool
|
||||||
|
|
||||||
|
for i := 0; i < len(sn); i++ {
|
||||||
|
node := sn[i]
|
||||||
|
|
||||||
|
node.FillAuthzContext(&authzContext)
|
||||||
|
if f.allowNode(node.Node, &authzContext) && f.allowService(node.ServiceName, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
removed = true
|
||||||
|
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node.Node, &node.EnterpriseMeta))
|
||||||
|
sn = append(sn[:i], sn[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
*nodes = sn
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterNodeServices is used to filter services on a given node base on ACLs.
|
||||||
|
// Returns true if any elements were removed
|
||||||
|
func (f *Filter) filterNodeServices(services **structs.NodeServices) bool {
|
||||||
|
if *services == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
(*services).Node.FillAuthzContext(&authzContext)
|
||||||
|
if !f.allowNode((*services).Node.Node, &authzContext) {
|
||||||
|
*services = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed bool
|
||||||
|
for svcName, svc := range (*services).Services {
|
||||||
|
svc.FillAuthzContext(&authzContext)
|
||||||
|
|
||||||
|
if f.allowNode((*services).Node.Node, &authzContext) && f.allowService(svcName, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.logger.Debug("dropping service from result due to ACLs", "service", svc.CompoundServiceID())
|
||||||
|
removed = true
|
||||||
|
delete((*services).Services, svcName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterNodeServices is used to filter services on a given node base on ACLs.
|
||||||
|
// Returns true if any elements were removed.
|
||||||
|
func (f *Filter) filterNodeServiceList(services *structs.NodeServiceList) bool {
|
||||||
|
if services.Node == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
services.Node.FillAuthzContext(&authzContext)
|
||||||
|
if !f.allowNode(services.Node.Node, &authzContext) {
|
||||||
|
*services = structs.NodeServiceList{}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed bool
|
||||||
|
svcs := services.Services
|
||||||
|
for i := 0; i < len(svcs); i++ {
|
||||||
|
svc := svcs[i]
|
||||||
|
svc.FillAuthzContext(&authzContext)
|
||||||
|
|
||||||
|
if f.allowService(svc.Service, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f.logger.Debug("dropping service from result due to ACLs", "service", svc.CompoundServiceID())
|
||||||
|
svcs = append(svcs[:i], svcs[i+1:]...)
|
||||||
|
i--
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
services.Services = svcs
|
||||||
|
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterCheckServiceNodes is used to filter nodes based on ACL rules. Returns
|
||||||
|
// true if any elements were removed.
|
||||||
|
func (f *Filter) filterCheckServiceNodes(nodes *structs.CheckServiceNodes) bool {
|
||||||
|
csn := *nodes
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
var removed bool
|
||||||
|
|
||||||
|
for i := 0; i < len(csn); i++ {
|
||||||
|
node := csn[i]
|
||||||
|
node.Service.FillAuthzContext(&authzContext)
|
||||||
|
if f.allowNode(node.Node.Node, &authzContext) && f.allowService(node.Service.Service, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node.Node.Node, node.Node.GetEnterpriseMeta()))
|
||||||
|
removed = true
|
||||||
|
csn = append(csn[:i], csn[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
*nodes = csn
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterServiceTopology is used to filter upstreams/downstreams based on ACL rules.
|
||||||
|
// this filter is unlike others in that it also returns whether the result was filtered by ACLs
|
||||||
|
func (f *Filter) filterServiceTopology(topology *structs.ServiceTopology) bool {
|
||||||
|
filteredUpstreams := f.filterCheckServiceNodes(&topology.Upstreams)
|
||||||
|
filteredDownstreams := f.filterCheckServiceNodes(&topology.Downstreams)
|
||||||
|
return filteredUpstreams || filteredDownstreams
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterDatacenterCheckServiceNodes is used to filter nodes based on ACL rules.
|
||||||
|
// Returns true if any elements are removed.
|
||||||
|
func (f *Filter) filterDatacenterCheckServiceNodes(datacenterNodes *map[string]structs.CheckServiceNodes) bool {
|
||||||
|
dn := *datacenterNodes
|
||||||
|
out := make(map[string]structs.CheckServiceNodes)
|
||||||
|
var removed bool
|
||||||
|
for dc := range dn {
|
||||||
|
nodes := dn[dc]
|
||||||
|
if f.filterCheckServiceNodes(&nodes) {
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
if len(nodes) > 0 {
|
||||||
|
out[dc] = nodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*datacenterNodes = out
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterSessions is used to filter a set of sessions based on ACLs. Returns
|
||||||
|
// true if any elements were removed.
|
||||||
|
func (f *Filter) filterSessions(sessions *structs.Sessions) bool {
|
||||||
|
s := *sessions
|
||||||
|
|
||||||
|
var removed bool
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
session := s[i]
|
||||||
|
|
||||||
|
var entCtx acl.AuthorizerContext
|
||||||
|
session.FillAuthzContext(&entCtx)
|
||||||
|
|
||||||
|
if f.allowSession(session.Node, &entCtx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
removed = true
|
||||||
|
f.logger.Debug("dropping session from result due to ACLs", "session", session.ID)
|
||||||
|
s = append(s[:i], s[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
*sessions = s
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterCoordinates is used to filter nodes in a coordinate dump based on ACL
|
||||||
|
// rules. Returns true if any elements were removed.
|
||||||
|
func (f *Filter) filterCoordinates(coords *structs.Coordinates) bool {
|
||||||
|
c := *coords
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
var removed bool
|
||||||
|
|
||||||
|
for i := 0; i < len(c); i++ {
|
||||||
|
c[i].FillAuthzContext(&authzContext)
|
||||||
|
node := c[i].Node
|
||||||
|
if f.allowNode(node, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, c[i].GetEnterpriseMeta()))
|
||||||
|
removed = true
|
||||||
|
c = append(c[:i], c[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
*coords = c
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterIntentions is used to filter intentions based on ACL rules.
|
||||||
|
// We prune entries the user doesn't have access to, and we redact any tokens
|
||||||
|
// if the user doesn't have a management token. Returns true if any elements
|
||||||
|
// were removed.
|
||||||
|
func (f *Filter) filterIntentions(ixns *structs.Intentions) bool {
|
||||||
|
ret := make(structs.Intentions, 0, len(*ixns))
|
||||||
|
var removed bool
|
||||||
|
for _, ixn := range *ixns {
|
||||||
|
if !ixn.CanRead(f.authorizer) {
|
||||||
|
removed = true
|
||||||
|
f.logger.Debug("dropping intention from result due to ACLs", "intention", ixn.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, ixn)
|
||||||
|
}
|
||||||
|
|
||||||
|
*ixns = ret
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterNodeDump is used to filter through all parts of a node dump and
|
||||||
|
// remove elements the provided ACL token cannot access. Returns true if
|
||||||
|
// any elements were removed.
|
||||||
|
func (f *Filter) filterNodeDump(dump *structs.NodeDump) bool {
|
||||||
|
nd := *dump
|
||||||
|
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
var removed bool
|
||||||
|
for i := 0; i < len(nd); i++ {
|
||||||
|
info := nd[i]
|
||||||
|
|
||||||
|
// Filter nodes
|
||||||
|
info.FillAuthzContext(&authzContext)
|
||||||
|
if node := info.Node; !f.allowNode(node, &authzContext) {
|
||||||
|
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, info.GetEnterpriseMeta()))
|
||||||
|
removed = true
|
||||||
|
nd = append(nd[:i], nd[i+1:]...)
|
||||||
|
i--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter services
|
||||||
|
for j := 0; j < len(info.Services); j++ {
|
||||||
|
svc := info.Services[j].Service
|
||||||
|
info.Services[j].FillAuthzContext(&authzContext)
|
||||||
|
if f.allowNode(info.Node, &authzContext) && f.allowService(svc, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.logger.Debug("dropping service from result due to ACLs", "service", svc)
|
||||||
|
removed = true
|
||||||
|
info.Services = append(info.Services[:j], info.Services[j+1:]...)
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter checks
|
||||||
|
for j := 0; j < len(info.Checks); j++ {
|
||||||
|
chk := info.Checks[j]
|
||||||
|
chk.FillAuthzContext(&authzContext)
|
||||||
|
if f.allowNode(info.Node, &authzContext) && f.allowService(chk.ServiceName, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.logger.Debug("dropping check from result due to ACLs", "check", chk.CheckID)
|
||||||
|
removed = true
|
||||||
|
info.Checks = append(info.Checks[:j], info.Checks[j+1:]...)
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*dump = nd
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterServiceDump is used to filter nodes based on ACL rules. Returns true
|
||||||
|
// if any elements were removed.
|
||||||
|
func (f *Filter) filterServiceDump(services *structs.ServiceDump) bool {
|
||||||
|
svcs := *services
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
var removed bool
|
||||||
|
|
||||||
|
for i := 0; i < len(svcs); i++ {
|
||||||
|
service := svcs[i]
|
||||||
|
|
||||||
|
if f.allowGateway(service.GatewayService) {
|
||||||
|
// ServiceDump might only have gateway config and no node information
|
||||||
|
if service.Node == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Service.FillAuthzContext(&authzContext)
|
||||||
|
if f.allowNode(service.Node.Node, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.logger.Debug("dropping service from result due to ACLs", "service", service.GatewayService.Service)
|
||||||
|
removed = true
|
||||||
|
svcs = append(svcs[:i], svcs[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
*services = svcs
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterNodes is used to filter through all parts of a node list and remove
|
||||||
|
// elements the provided ACL token cannot access. Returns true if any elements
|
||||||
|
// were removed.
|
||||||
|
func (f *Filter) filterNodes(nodes *structs.Nodes) bool {
|
||||||
|
n := *nodes
|
||||||
|
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
var removed bool
|
||||||
|
|
||||||
|
for i := 0; i < len(n); i++ {
|
||||||
|
n[i].FillAuthzContext(&authzContext)
|
||||||
|
node := n[i].Node
|
||||||
|
if f.allowNode(node, &authzContext) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, n[i].GetEnterpriseMeta()))
|
||||||
|
removed = true
|
||||||
|
n = append(n[:i], n[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
*nodes = n
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// redactPreparedQueryTokens will redact any tokens unless the client has a
|
||||||
|
// management token. This eases the transition to delegated authority over
|
||||||
|
// prepared queries, since it was easy to capture management tokens in Consul
|
||||||
|
// 0.6.3 and earlier, and we don't want to willy-nilly show those. This does
|
||||||
|
// have the limitation of preventing delegated non-management users from seeing
|
||||||
|
// captured tokens, but they can at least see whether or not a token is set.
|
||||||
|
func (f *Filter) redactPreparedQueryTokens(query **structs.PreparedQuery) {
|
||||||
|
// Management tokens can see everything with no filtering.
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
structs.DefaultEnterpriseMetaInDefaultPartition().FillAuthzContext(&authzContext)
|
||||||
|
if f.authorizer.ACLWrite(&authzContext) == acl.Allow {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the user see if there's a blank token, otherwise we need
|
||||||
|
// to redact it, since we know they don't have a management
|
||||||
|
// token.
|
||||||
|
if (*query).Token != "" {
|
||||||
|
// Redact the token, using a copy of the query structure
|
||||||
|
// since we could be pointed at a live instance from the
|
||||||
|
// state store so it's not safe to modify it. Note that
|
||||||
|
// this clone will still point to things like underlying
|
||||||
|
// arrays in the original, but for modifying just the
|
||||||
|
// token it will be safe to use.
|
||||||
|
clone := *(*query)
|
||||||
|
clone.Token = RedactedToken
|
||||||
|
*query = &clone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterPreparedQueries is used to filter prepared queries based on ACL rules.
|
||||||
|
// We prune entries the user doesn't have access to, and we redact any tokens
|
||||||
|
// if the user doesn't have a management token. Returns true if any (named)
|
||||||
|
// queries were removed - un-named queries are meant to be ephemeral and can
|
||||||
|
// only be enumerated by a management token
|
||||||
|
func (f *Filter) filterPreparedQueries(queries *structs.PreparedQueries) bool {
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
structs.DefaultEnterpriseMetaInDefaultPartition().FillAuthzContext(&authzContext)
|
||||||
|
// Management tokens can see everything with no filtering.
|
||||||
|
// TODO is this check even necessary - this looks like a search replace from
|
||||||
|
// the 1.4 ACL rewrite. The global-management token will provide unrestricted query privileges
|
||||||
|
// so asking for ACLWrite should be unnecessary.
|
||||||
|
if f.authorizer.ACLWrite(&authzContext) == acl.Allow {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we need to see what the token has access to.
|
||||||
|
var namedQueriesRemoved bool
|
||||||
|
ret := make(structs.PreparedQueries, 0, len(*queries))
|
||||||
|
for _, query := range *queries {
|
||||||
|
// If no prefix ACL applies to this query then filter it, since
|
||||||
|
// we know at this point the user doesn't have a management
|
||||||
|
// token, otherwise see what the policy says.
|
||||||
|
prefix, hasName := query.GetACLPrefix()
|
||||||
|
switch {
|
||||||
|
case hasName && f.authorizer.PreparedQueryRead(prefix, &authzContext) != acl.Allow:
|
||||||
|
namedQueriesRemoved = true
|
||||||
|
fallthrough
|
||||||
|
case !hasName:
|
||||||
|
f.logger.Debug("dropping prepared query from result due to ACLs", "query", query.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redact any tokens if necessary. We make a copy of just the
|
||||||
|
// pointer so we don't mess with the caller's slice.
|
||||||
|
final := query
|
||||||
|
f.redactPreparedQueryTokens(&final)
|
||||||
|
ret = append(ret, final)
|
||||||
|
}
|
||||||
|
*queries = ret
|
||||||
|
return namedQueriesRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterToken(token **structs.ACLToken) {
|
||||||
|
var entCtx acl.AuthorizerContext
|
||||||
|
if token == nil || *token == nil || f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*token).FillAuthzContext(&entCtx)
|
||||||
|
|
||||||
|
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||||
|
// no permissions to read
|
||||||
|
*token = nil
|
||||||
|
} else if f.authorizer.ACLWrite(&entCtx) != acl.Allow {
|
||||||
|
// no write permissions - redact secret
|
||||||
|
clone := *(*token)
|
||||||
|
clone.SecretID = RedactedToken
|
||||||
|
*token = &clone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterTokens(tokens *structs.ACLTokens) {
|
||||||
|
ret := make(structs.ACLTokens, 0, len(*tokens))
|
||||||
|
for _, token := range *tokens {
|
||||||
|
final := token
|
||||||
|
f.filterToken(&final)
|
||||||
|
if final != nil {
|
||||||
|
ret = append(ret, final)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*tokens = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterTokenStub(token **structs.ACLTokenListStub) {
|
||||||
|
var entCtx acl.AuthorizerContext
|
||||||
|
if token == nil || *token == nil || f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*token).FillAuthzContext(&entCtx)
|
||||||
|
|
||||||
|
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||||
|
*token = nil
|
||||||
|
} else if f.authorizer.ACLWrite(&entCtx) != acl.Allow {
|
||||||
|
// no write permissions - redact secret
|
||||||
|
clone := *(*token)
|
||||||
|
clone.SecretID = RedactedToken
|
||||||
|
*token = &clone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterTokenStubs(tokens *[]*structs.ACLTokenListStub) {
|
||||||
|
ret := make(structs.ACLTokenListStubs, 0, len(*tokens))
|
||||||
|
for _, token := range *tokens {
|
||||||
|
final := token
|
||||||
|
f.filterTokenStub(&final)
|
||||||
|
if final != nil {
|
||||||
|
ret = append(ret, final)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*tokens = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterPolicy(policy **structs.ACLPolicy) {
|
||||||
|
var entCtx acl.AuthorizerContext
|
||||||
|
if policy == nil || *policy == nil || f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*policy).FillAuthzContext(&entCtx)
|
||||||
|
|
||||||
|
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||||
|
// no permissions to read
|
||||||
|
*policy = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterPolicies(policies *structs.ACLPolicies) {
|
||||||
|
ret := make(structs.ACLPolicies, 0, len(*policies))
|
||||||
|
for _, policy := range *policies {
|
||||||
|
final := policy
|
||||||
|
f.filterPolicy(&final)
|
||||||
|
if final != nil {
|
||||||
|
ret = append(ret, final)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*policies = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterRole(role **structs.ACLRole) {
|
||||||
|
var entCtx acl.AuthorizerContext
|
||||||
|
if role == nil || *role == nil || f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*role).FillAuthzContext(&entCtx)
|
||||||
|
|
||||||
|
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||||
|
// no permissions to read
|
||||||
|
*role = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterRoles(roles *structs.ACLRoles) {
|
||||||
|
ret := make(structs.ACLRoles, 0, len(*roles))
|
||||||
|
for _, role := range *roles {
|
||||||
|
final := role
|
||||||
|
f.filterRole(&final)
|
||||||
|
if final != nil {
|
||||||
|
ret = append(ret, final)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*roles = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterBindingRule(rule **structs.ACLBindingRule) {
|
||||||
|
var entCtx acl.AuthorizerContext
|
||||||
|
if rule == nil || *rule == nil || f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*rule).FillAuthzContext(&entCtx)
|
||||||
|
|
||||||
|
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||||
|
// no permissions to read
|
||||||
|
*rule = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterBindingRules(rules *structs.ACLBindingRules) {
|
||||||
|
ret := make(structs.ACLBindingRules, 0, len(*rules))
|
||||||
|
for _, rule := range *rules {
|
||||||
|
final := rule
|
||||||
|
f.filterBindingRule(&final)
|
||||||
|
if final != nil {
|
||||||
|
ret = append(ret, final)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*rules = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterAuthMethod(method **structs.ACLAuthMethod) {
|
||||||
|
var entCtx acl.AuthorizerContext
|
||||||
|
if method == nil || *method == nil || f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*method).FillAuthzContext(&entCtx)
|
||||||
|
|
||||||
|
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||||
|
// no permissions to read
|
||||||
|
*method = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterAuthMethods(methods *structs.ACLAuthMethods) {
|
||||||
|
ret := make(structs.ACLAuthMethods, 0, len(*methods))
|
||||||
|
for _, method := range *methods {
|
||||||
|
final := method
|
||||||
|
f.filterAuthMethod(&final)
|
||||||
|
if final != nil {
|
||||||
|
ret = append(ret, final)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*methods = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) filterServiceList(services *structs.ServiceList) bool {
|
||||||
|
ret := make(structs.ServiceList, 0, len(*services))
|
||||||
|
var removed bool
|
||||||
|
for _, svc := range *services {
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
|
||||||
|
svc.FillAuthzContext(&authzContext)
|
||||||
|
|
||||||
|
if f.authorizer.ServiceRead(svc.Name, &authzContext) != acl.Allow {
|
||||||
|
removed = true
|
||||||
|
sid := structs.NewServiceID(svc.Name, &svc.EnterpriseMeta)
|
||||||
|
f.logger.Debug("dropping service from result due to ACLs", "service", sid.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, svc)
|
||||||
|
}
|
||||||
|
|
||||||
|
*services = ret
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterGatewayServices is used to filter gateway to service mappings based on ACL rules.
|
||||||
|
// Returns true if any elements were removed.
|
||||||
|
func (f *Filter) filterGatewayServices(mappings *structs.GatewayServices) bool {
|
||||||
|
ret := make(structs.GatewayServices, 0, len(*mappings))
|
||||||
|
var removed bool
|
||||||
|
for _, s := range *mappings {
|
||||||
|
// This filter only checks ServiceRead on the linked service.
|
||||||
|
// ServiceRead on the gateway is checked in the GatewayServices endpoint before filtering.
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
s.Service.FillAuthzContext(&authzContext)
|
||||||
|
|
||||||
|
if f.authorizer.ServiceRead(s.Service.Name, &authzContext) != acl.Allow {
|
||||||
|
f.logger.Debug("dropping service from result due to ACLs", "service", s.Service.String())
|
||||||
|
removed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, s)
|
||||||
|
}
|
||||||
|
*mappings = ret
|
||||||
|
return removed
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue