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),
|
||||
}
|
||||
|
||||
if a.config.ServerMode {
|
||||
if server, ok := a.delegate.(*consul.Server); ok {
|
||||
deps := proxycfgglue.ServerDataSourceDeps{
|
||||
EventPublisher: a.baseDeps.EventPublisher,
|
||||
ViewStore: a.baseDeps.ViewStore,
|
||||
Logger: a.logger.Named("proxycfg.server-data-sources"),
|
||||
ACLResolver: a.delegate,
|
||||
GetStore: func() proxycfgglue.Store { return server.FSM().State() },
|
||||
}
|
||||
sources.ConfigEntry = proxycfgglue.ServerConfigEntry(deps)
|
||||
sources.ConfigEntryList = proxycfgglue.ServerConfigEntryList(deps)
|
||||
sources.Intentions = proxycfgglue.ServerIntentions(deps)
|
||||
sources.IntentionUpstreams = proxycfgglue.ServerIntentionUpstreams(deps)
|
||||
}
|
||||
|
||||
a.fillEnterpriseProxyDataSources(&sources)
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"github.com/hashicorp/consul/agent/token"
|
||||
"github.com/hashicorp/consul/logging"
|
||||
)
|
||||
|
@ -43,10 +44,6 @@ const (
|
|||
// provided.
|
||||
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 rate.Limit = 1.0
|
||||
|
||||
|
@ -1114,816 +1111,8 @@ func (r *ACLResolver) ResolveTokenAndDefaultMetaWithPeerName(
|
|||
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{}) {
|
||||
if authorizer == nil {
|
||||
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))
|
||||
}
|
||||
aclfilter.New(authorizer, logger).Filter(subj)
|
||||
}
|
||||
|
||||
// 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/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
)
|
||||
|
||||
|
@ -291,7 +292,7 @@ func (a *ACL) TokenRead(args *structs.ACLTokenGetRequest, reply *structs.ACLToke
|
|||
a.srv.filterACLWithAuthorizer(authz, &token)
|
||||
|
||||
// token secret was redacted
|
||||
if token.SecretID == redactedToken {
|
||||
if token.SecretID == aclfilter.RedactedToken {
|
||||
reply.Redacted = true
|
||||
}
|
||||
}
|
||||
|
@ -719,7 +720,7 @@ func (a *ACL) TokenBatchRead(args *structs.ACLTokenBatchGetRequest, reply *struc
|
|||
a.srv.filterACLWithAuthorizer(authz, &final)
|
||||
if final != nil {
|
||||
ret = append(ret, final)
|
||||
if final.SecretID == redactedToken {
|
||||
if final.SecretID == aclfilter.RedactedToken {
|
||||
reply.Redacted = true
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/hashicorp/consul/agent/consul/authmethod/kubeauth"
|
||||
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
|
||||
"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/sdk/testutil"
|
||||
"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)
|
||||
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/agent/consul/authmethod/testauth"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
tokenStore "github.com/hashicorp/consul/agent/token"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
"github.com/hashicorp/consul/testrpc"
|
||||
|
@ -752,9 +753,9 @@ func TestACLReplication_TokensRedacted(t *testing.T) {
|
|||
var tokenResp structs.ACLTokenResponse
|
||||
req := structs.ACLTokenGetRequest{
|
||||
Datacenter: "dc2",
|
||||
TokenID: redactedToken,
|
||||
TokenID: aclfilter.RedactedToken,
|
||||
TokenIDType: structs.ACLTokenSecret,
|
||||
QueryOptions: structs.QueryOptions{Token: redactedToken},
|
||||
QueryOptions: structs.QueryOptions{Token: aclfilter.RedactedToken},
|
||||
}
|
||||
err := s2.RPC("ACL.TokenRead", &req, &tokenResp)
|
||||
// its not an error for the secret to not be found.
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
)
|
||||
|
||||
type aclTokenReplicator struct {
|
||||
|
@ -99,7 +100,7 @@ func (r *aclTokenReplicator) PendingUpdateEstimatedSize(i int) int {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/metadata"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
"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
|
||||
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 {
|
||||
req := structs.ACLTokenBatchDeleteRequest{
|
||||
TokenIDs: []string{token.AccessorID},
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"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
|
||||
// user hasn't accidentally pasted-in the special redacted token name,
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
tokenStore "github.com/hashicorp/consul/agent/token"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
|
@ -570,7 +571,7 @@ func TestPreparedQuery_parseQuery(t *testing.T) {
|
|||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
query.Token = redactedToken
|
||||
query.Token = aclfilter.RedactedToken
|
||||
err = parseQuery(query)
|
||||
if err == nil || !strings.Contains(err.Error(), "Bad Token") {
|
||||
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
|
||||
// will be redacted since this isn't a management token.
|
||||
query.Query.ID = reply
|
||||
query.Query.Token = redactedToken
|
||||
query.Query.Token = aclfilter.RedactedToken
|
||||
{
|
||||
req := &structs.PreparedQuerySpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
|
@ -779,7 +780,7 @@ func TestPreparedQuery_ACLDeny_Catchall_Template(t *testing.T) {
|
|||
}
|
||||
|
||||
// The user can explain and see the redacted token.
|
||||
query.Query.Token = redactedToken
|
||||
query.Query.Token = aclfilter.RedactedToken
|
||||
query.Query.Service.Service = "anything"
|
||||
{
|
||||
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.
|
||||
query.Query.Token = redactedToken
|
||||
query.Query.Token = aclfilter.RedactedToken
|
||||
{
|
||||
req := &structs.PreparedQuerySpecificRequest{
|
||||
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
|
||||
// the captured token gets redacted.
|
||||
query.Query.ID = reply
|
||||
query.Query.Token = redactedToken
|
||||
query.Query.Token = aclfilter.RedactedToken
|
||||
{
|
||||
req := &structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
|
@ -1355,7 +1356,7 @@ func TestPreparedQuery_Explain(t *testing.T) {
|
|||
}
|
||||
|
||||
// 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"
|
||||
{
|
||||
req := &structs.PreparedQueryExecuteRequest{
|
||||
|
|
|
@ -23,6 +23,7 @@ type ServerDataSourceDeps struct {
|
|||
EventPublisher *stream.EventPublisher
|
||||
Logger hclog.Logger
|
||||
ACLResolver submatview.ACLResolver
|
||||
GetStore func() Store
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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) {
|
||||
|
|
|
@ -3,14 +3,25 @@ package proxycfgglue
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/go-memdb"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
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/rpcclient/health"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"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
|
||||
// the agent cache.
|
||||
func CacheCARoots(c *cache.Cache) proxycfg.CARoots {
|
||||
|
@ -134,7 +145,7 @@ func (c *cacheProxyDataSource[ReqType]) Notify(
|
|||
correlationID string,
|
||||
ch chan<- proxycfg.UpdateEvent,
|
||||
) 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
|
||||
|
@ -153,10 +164,10 @@ func (h *healthWrapper) Notify(
|
|||
correlationID string,
|
||||
ch chan<- proxycfg.UpdateEvent,
|
||||
) 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) {
|
||||
u := proxycfg.UpdateEvent{
|
||||
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)
|
||||
|
||||
intentions := ServerIntentions(ServerDataSourceDeps{
|
||||
ACLResolver: manageAllResolver{},
|
||||
ACLResolver: staticResolver{acl.ManageAll()},
|
||||
ViewStore: store,
|
||||
EventPublisher: publisher,
|
||||
Logger: logger,
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestServerIntentions(t *testing.T) {
|
|||
go publisher.Run(ctx)
|
||||
|
||||
intentions := ServerIntentions(ServerDataSourceDeps{
|
||||
ACLResolver: manageAllResolver{},
|
||||
ACLResolver: staticResolver{acl.ManageAll()},
|
||||
ViewStore: store,
|
||||
EventPublisher: publisher,
|
||||
Logger: logger,
|
||||
|
@ -146,8 +146,10 @@ func TestServerIntentions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type manageAllResolver struct{}
|
||||
|
||||
func (manageAllResolver) ResolveTokenAndDefaultMeta(token string, entMeta *acl.EnterpriseMeta, authzContext *acl.AuthorizerContext) (resolver.Result, error) {
|
||||
return resolver.Result{Authorizer: acl.ManageAll()}, nil
|
||||
type staticResolver struct {
|
||||
authorizer acl.Authorizer
|
||||
}
|
||||
|
||||
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