-
Notifications
You must be signed in to change notification settings - Fork 13.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
KAFKA-6591: Move super user check before ACL matching #4618
Conversation
retest this please |
@@ -106,6 +106,12 @@ class SimpleAclAuthorizer extends Authorizer with Logging { | |||
val host = session.clientAddress.getHostAddress | |||
val acls = getAcls(resource) ++ getAcls(new Resource(resource.resourceType, Resource.WildCardResource)) | |||
|
|||
// Check if the user is a super user to avoid needlessly checking all ACLs | |||
// for super users | |||
if (isSuperUser(operation, resource, principal, host)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Can we do this check before initializing acls?
//when no acls are found or if no deny acls are found and at least one allow acls matches. | ||
val authorized = isSuperUser(operation, resource, principal, host) || | ||
isEmptyAclAndAuthorized(operation, resource, principal, host, acls) || | ||
val authorized = isEmptyAclAndAuthorized(operation, resource, principal, host, acls) || | ||
(!denyMatch && allowMatch) | ||
|
||
logAuditMessage(principal, authorized, operation, resource, host) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like we miss audit messages for super users. we need to call logAuditMessage in above check.
Thanks for reviewing @omkreddy . You are right, I did miss the log statement. have refactored so that the same return statement gets used for all cases to avoid duplicating the log statement. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, seems reasonable. Left a couple minor comments.
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
* Licensed to the Apache Software Foundation (ASF) under one or more |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this change intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely not. I just moved to a new laptop and apparently Intellij went back to ignoring whitespace changes in diffs again. I'll revert that.
val allowMatch = allowOps.exists(operation => aclMatch(operation, resource, principal, host, Allow, acls)) | ||
|
||
//we allow an operation if no acls are found and user has configured to allow all users | ||
//when no acls are found or if no deny acls are found and at least one allow acls matches. | ||
isEmptyAclAndAuthorized(operation, resource, principal, host, acls) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to move the empty check above the ACL matching since we don't really care about the results anyway in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I'll amend the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually after doing some more thinking and looking at the code it may make sense to leave the check as is.
The log message that is currently being displayed is:
authorizerLogger.debug(s"No acl found for resource $resource, authorized = $shouldAllowEveryoneIfNoAclIsFound")
Which would not be factually correct any more if we made the switch, as we never looked at ACLs, so there may very well be ACLs as well that would permit the operation when this message is shown.
We could reword the log message, but I'd argue that the information that no ACLs matched can be useful when securing an already busy cluster to find clients that were missed in the migration (and probably other scenarios as well).
The overhead is also reasonable I think, we need to load the ACLs anyway, otherwise we can't perform the check and then its just two .exists checks against an empty collection that need to be performed (once for Deny matches once for Allow matches).
Update:
Something kept nagging me about this and I reread the code. I missed the .isempty check in isEmptyAclAndAuthorized which would suppress the log message in this case. Hence it does make sense to move the check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I didn't see the harm since we would still check isEmptyAclAndAuthorized
in any case. Not that big of a deal though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've created a commit that addresses this comment as well.. I'm not sure that I like it much better than the code that is currently there though..we can't simply add the check as an "if else" block, since we need to retrieve the ACLs after the superuser check, so it becomes a nested if..
Also this created "dead" code in isEmptyAclAndAuthorized so I inlined that function as it was extremely short and I suspect it was only initially created to keep the conditional statement readable.
I've pushed to an alternative branch so I don't have to revert here if we decide against it. Feel free to have a look and let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hachikuji Did you have a chance to have a look at the proposed alternative and have any thoughts on whether we want to use that or keep the PR as is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@soenkeliebau The alternative looks good to me.
@hachikuji I've merged the alternative with this PR. The test failure is unrelated I believe:
so won't request a retest if we exceed a rate limit anyway.. |
retest this please |
…necessary effort when evaluating requests from super users.
I've rebased on trunk to accomodate changes from KAFKA-6841 (I spotted a typo in one comment, will abstain from fixing that for now and do that later if changes should become necessary) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@soenkeliebau LGTM, few suggestions to improve things even more. This is a improvement I noticed when introducing ACLs on prefixed resource patterns, but didn't want to muddy the PR with such changes - great that you picked this up!
} else { | ||
// ACLs found for this resource | ||
// Check if there is any Deny acl match that would disallow this operation. | ||
val denyMatch = aclMatch(operation, resource, principal, host, Deny, acls) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May as well not check for allow acls if there are deny acls, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in the refactored version of the code.
// for superusers | ||
true | ||
} else { | ||
// User is no superuser, load ACLs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: User is not superuser.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a native speaker, so probably on very thin ice here, but to my mind's ear either "no superuser" or "not a superuser" sound right.
Then again, the discussion is mostly academic, as the comment has been removed in the refactored version anyway :)
// Check if the user is a superuser to avoid needlessly checking all ACLs | ||
// for superusers | ||
true | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider moving the 'else' into a separate method, then this becomes, (I don't think any comments are required on such obvious logic):
val authorized = isSuperUser(...) || aclsAllowAccess(...)
Following the same pattern, aclsAllowAccess
could be:
val acls = getMatchingAcls(resource.resourceType, resource.name)
val authorized = isEmptyAclAndAuthorized(...) || (!denyAclsExist(acls, ...) && alllowAclsExist(acls, ...))
This breaking out into different methods will likely lead to move readable code and less reliance on comments to explain what's happening. These methods can be local to authorize
, which I think ends up with something like:
override def authorize(session: Session, operation: Operation, resource: Resource): Boolean = {
if (resource.patternType != PatternType.LITERAL) {
throw new IllegalArgumentException("Only literal resources are supported. Got: " + resource.patternType)
}
val principal = session.principal
val host = session.clientAddress.getHostAddress
def isEmptyAclAndAuthorized(acls: Set[Acl]): Boolean = {
if (acls.isEmpty) {
authorizerLogger.debug(s"No acl found for resource $resource, authorized = $shouldAllowEveryoneIfNoAclIsFound")
shouldAllowEveryoneIfNoAclIsFound
} else false
}
def denyAclsExist(acls: Set[Acl]) = {
aclMatch(operation, resource, principal, host, Deny, acls)
}
def allowAclsExist(acls: Set[Acl]) = {
// Check if there are any Allow ACLs which would allow this operation.
// Allowing read, write, delete, or alter implies allowing describe.
// See #{org.apache.kafka.common.acl.AclOperation} for more details about ACL inheritance.
val allowOps = operation match {
case Describe => Set[Operation](Describe, Read, Write, Delete, Alter)
case DescribeConfigs => Set[Operation](DescribeConfigs, AlterConfigs)
case _ => Set[Operation](operation)
}
allowOps.exists(operation => aclMatch(operation, resource, principal, host, Allow, acls))
}
def aclsAllowAccess = {
val acls = getMatchingAcls(resource.resourceType, resource.name)
isEmptyAclAndAuthorized(acls) || (!denyAclsExist(acls) && allowAclsExist(acls))
}
val authorized = isSuperUser(operation, resource, principal, host) || aclsAllowAccess
logAuditMessage(principal, authorized, operation, resource, host)
authorized
}
Though you'll want to check my quick hacking about!
Also, you might want to check a suitable unit test exists that calls authorize for a super user, where deny acls exist. (which should allow access).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your hacking was pretty much spot on, I only took the liberty to very slightly rename the *AclsExist methods.
testSuperUserHasAccess addresses the scenario where a super user still has access even though there is a Deny Acl in place.
true | ||
} else { | ||
// User is no superuser, load ACLs | ||
// To avoid unnecessarily loading ACLs for super users we need to nest this as a second if |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: there is no 'loading' of ACLs done in getMatchingAcls
: ACLs are evaluated only. They are loaded in 'configure()'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"loading" was probably a poor choice of words here, yes. What I was referring to here was the retrieval of matching ACLs from the Acl-Map.
Comment has become obsolete though.
@big-andy-coates thanks for the review! I've incorporated your comments. Test failure is unrelated I believe. @hachikuji can you take a look as well and merge if happy with the changes? |
retest this please |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM @soenkeliebau. Thanks for making the changes!
cc: @cmccabe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, LGTM
Currently the check whether a user as a super user in `SimpleAclAuthorizer` is performed only after all other ACLs have been evaluated. Since all requests from a super user are granted we don't really need to apply the ACLs. This patch returns true if the user is a super user before checking ACLs, thus bypassing the needless evaluation effort. Reviewers: Andy Coates <big-andy-coates@users.noreply.github.com>, Manikumar Reddy <manikumar.reddy@gmail.com>, Jason Gustafson <jason@confluent.io>
Currently the check whether a user as a super user in SimpleAclAuthorizer is performed only after all other ACLs have been evaluated. Since all requests from a super user are granted we don't really need to apply the ACLs.
This commit returns true if the user is a super user before checking ACLs, thus bypassing the needless evaluation effort.