search: refactor OSS/ENT split for ACL checks (#13760)

The split between OSS/ENT in ACL checks for the Search RPC has a lot
of repeated code that results in merge conflicts. Move most of the
logic into the shared code so that we can call out to thin functions
for ENT checks.
This commit is contained in:
Tim Gross 2022-07-14 11:31:08 -04:00 committed by GitHub
parent d73d0aac21
commit 0cf8a580c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 103 additions and 103 deletions

View File

@ -609,6 +609,47 @@ func (s *Search) PrefixSearch(args *structs.SearchRequest, reply *structs.Search
return s.srv.blockingRPC(&opts)
}
// sufficientSearchPerms returns true if the provided ACL has access to any
// capabilities required for prefix searching.
//
// Returns true if aclObj is nil or is for a management token
func sufficientSearchPerms(aclObj *acl.ACL, namespace string, context structs.Context) bool {
if aclObj == nil || aclObj.IsManagement() {
return true
}
nodeRead := aclObj.AllowNodeRead()
allowNS := aclObj.AllowNamespace(namespace)
jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
allowEnt := sufficientSearchPermsEnt(aclObj)
if !nodeRead && !allowNS && !allowEnt && !jobRead {
return false
}
// Reject requests that explicitly specify a disallowed context. This
// should give the user better feedback than simply filtering out all
// results and returning an empty list.
switch context {
case structs.Nodes:
return nodeRead
case structs.Namespaces:
return allowNS
case structs.Allocs, structs.Deployments, structs.Evals, structs.Jobs:
return jobRead
case structs.Volumes:
return acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume,
acl.NamespaceCapabilityCSIReadVolume,
acl.NamespaceCapabilityListJobs,
acl.NamespaceCapabilityReadJob)(aclObj, namespace)
case structs.SecureVariables:
// FIXME: Replace with real variables capability
return aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
}
return true
}
// FuzzySearch is used to list fuzzy or prefix matches for a given text argument and Context.
// If the Context is "all", all searchable contexts are searched. If ACLs are enabled,
// results are limited to policies of the provided ACL token.
@ -782,6 +823,65 @@ func sufficientFuzzySearchPerms(aclObj *acl.ACL, namespace string, context struc
return sufficientSearchPerms(aclObj, namespace, context)
}
// filteredSearchContexts returns the expanded set of contexts, filtered down
// to the subset of contexts the aclObj is valid for.
//
// If aclObj is nil, no contexts are filtered out.
func filteredSearchContexts(aclObj *acl.ACL, namespace string, context structs.Context) []structs.Context {
desired := expandContext(context)
// If ACLs aren't enabled return all contexts
if aclObj == nil {
return desired
}
if aclObj.IsManagement() {
return desired
}
jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume,
acl.NamespaceCapabilityCSIReadVolume,
acl.NamespaceCapabilityListJobs,
acl.NamespaceCapabilityReadJob)
volRead := allowVolume(aclObj, namespace)
policyRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityListScalingPolicies)
// Filter contexts down to those the ACL grants access to
available := make([]structs.Context, 0, len(desired))
for _, c := range desired {
switch c {
case structs.Allocs, structs.Jobs, structs.Evals, structs.Deployments:
if jobRead {
available = append(available, c)
}
case structs.ScalingPolicies:
if policyRead || jobRead {
available = append(available, c)
}
case structs.Namespaces:
if aclObj.AllowNamespace(namespace) {
available = append(available, c)
}
case structs.SecureVariables:
if jobRead {
available = append(available, c)
}
case structs.Nodes:
if aclObj.AllowNodeRead() {
available = append(available, c)
}
case structs.Volumes:
if volRead {
available = append(available, c)
}
default:
if ok := filteredSearchContextsEnt(aclObj, namespace, c); ok {
available = append(available, c)
}
}
}
return available
}
// filterFuzzySearchContexts returns every context asked for if the searched namespace
// is the wildcard namespace, indicating we should bypass ACL checks otherwise
// performed by filterSearchContexts. Instead we will rely on iterator filters to

View File

@ -48,110 +48,10 @@ func getEnterpriseFuzzyResourceIter(context structs.Context, _ *acl.ACL, _ strin
return nil, fmt.Errorf("context must be one of %v or 'all' for all contexts; got %q", allContexts, context)
}
// sufficientSearchPerms returns true if the provided ACL has access to each
// capability required for prefix searching for the given context.
//
// Returns true if aclObj is nil.
func sufficientSearchPerms(aclObj *acl.ACL, namespace string, context structs.Context) bool {
if aclObj == nil {
return true
}
if aclObj.IsManagement() {
return true
}
nodeRead := aclObj.AllowNodeRead()
allowNS := aclObj.AllowNamespace(namespace)
jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume,
acl.NamespaceCapabilityCSIReadVolume,
acl.NamespaceCapabilityListJobs,
acl.NamespaceCapabilityReadJob)
volRead := allowVolume(aclObj, namespace)
// FIXME: Replace with real variables capability
allowVariables := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
if !nodeRead && !jobRead && !volRead && !allowNS && !allowVariables {
return false
}
// Reject requests that explicitly specify a disallowed context. This
// should give the user better feedback then simply filtering out all
// results and returning an empty list.
if !nodeRead && context == structs.Nodes {
return false
}
if !allowNS && context == structs.Namespaces {
return false
}
if !jobRead {
switch context {
case structs.Allocs, structs.Deployments, structs.Evals, structs.Jobs:
return false
}
}
if !allowVariables && context == structs.SecureVariables {
return false
}
if !volRead && context == structs.Volumes {
return false
}
func sufficientSearchPermsEnt(aclObj *acl.ACL) bool {
return true
}
// filteredSearchContexts returns the expanded set of contexts, filtered down
// to the subset of contexts the aclObj is valid for.
//
// If aclObj is nil, no contexts are filtered out.
func filteredSearchContexts(aclObj *acl.ACL, namespace string, context structs.Context) []structs.Context {
desired := expandContext(context)
// If ACLs aren't enabled return all contexts
if aclObj == nil {
return desired
}
if aclObj.IsManagement() {
return desired
}
jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob)
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume,
acl.NamespaceCapabilityCSIReadVolume,
acl.NamespaceCapabilityListJobs,
acl.NamespaceCapabilityReadJob)
volRead := allowVolume(aclObj, namespace)
policyRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityListScalingPolicies)
// Filter contexts down to those the ACL grants access to
available := make([]structs.Context, 0, len(desired))
for _, c := range desired {
switch c {
case structs.Allocs, structs.Jobs, structs.Evals, structs.Deployments:
if jobRead {
available = append(available, c)
}
case structs.ScalingPolicies:
if policyRead || jobRead {
available = append(available, c)
}
case structs.Namespaces:
if aclObj.AllowNamespace(namespace) {
available = append(available, c)
}
case structs.SecureVariables:
if jobRead {
available = append(available, c)
}
case structs.Nodes:
if aclObj.AllowNodeRead() {
available = append(available, c)
}
case structs.Volumes:
if volRead {
available = append(available, c)
}
}
}
return available
func filteredSearchContextsEnt(aclObj *acl.ACL, namespace string, context structs.Context) bool {
return true
}