-
Notifications
You must be signed in to change notification settings - Fork 96
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
Fix and refactor role based filter to allow CORS #99
Conversation
05ab769
to
33f7810
Compare
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.
Needs CHANGELOG entry
return allowedUserRoles.isEmpty() ? true : allowedUserRoles.stream().anyMatch(securityContext::isUserInRole); | ||
} | ||
|
||
/**\ |
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.
s// (ie. typo... \
shouldn't be there)
} | ||
} | ||
|
||
/** | ||
* Request authorization logic. |
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.
It would be good to highlight what that logic is, rather than forcing the user to read the code to understand it. In particular: If the HTTP method is OPTIONS or if the user has a role that's in the configured allowed list
} | ||
|
||
/**\ | ||
* By default, returns true to bypass the filter if the request is a Http OPTIONS method to allow CORS. |
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.
No need to have byDefault
, since this method (being private
) can't be overridden.
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.
Making it protected
to allow for override
// Release the test web container | ||
jtb.tearDown() | ||
roleBasedAuthFilter = new RoleBasedAuthFilter() | ||
SYSTEM_CONFIG.setProperty(SYSTEM_CONFIG.getPackageVariableName("user_roles"), "foo,bar"); |
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.
We need to make sure this gets cleaned up and re-set to it's original value. Property leaks are SUCH a pain to find.
} | ||
ContainerRequestContext containerRequestContext; | ||
SecurityContext securityContext; | ||
RoleBasedAuthFilter roleBasedAuthFilter; |
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.
No semicolons in groovy
def "GET request is not authorized if the user is not in an allowed role"() { | ||
when: "received a request and that the user is not in the authorized roles" | ||
containerRequestContext.getMethod() >> "GET" | ||
securityContext.isUserInRole() >> false |
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.
This mock won't work as you're expecting. It shouldn't ever trigger because it's not valid to call that method with no parameters. If you want the mock to trigger on all calls to that method, you should use the wildcard parameter syntax: securityContext.isUserInRole(*_)
when: "received a request and that the user is not in the authorized roles" | ||
containerRequestContext.getMethod() >> "GET" | ||
SYSTEM_CONFIG.clearProperty(SYSTEM_CONFIG.getPackageVariableName("user_roles")) | ||
securityContext.isUserInRole() >> false |
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.
See previous note on making this trigger the way I think you want it to.
securityContext.isUserInRole() >> false | ||
|
||
then: "the request is not authorized" | ||
roleBasedAuthFilter.isAuthorized(containerRequestContext) == false |
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.
These assertions can be simplified, since they are all pretty much boolean responses. Instead of == false
, you can just prepend !
to the call, and you don't need == true
if you're asserting truth.
containerRequestContext.getMethod() >> "GET" | ||
securityContext.isUserInRole() >> false | ||
|
||
then: "the request is not authorized" |
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.
None of these tests are "stimulus / assertion on the state of the world" tests, but instead they are all "given the state of the world, assert a behavior", which lends itself much more to the given / expect
style of tests, rather than the when / then
style. So, it would be good to convert all of these tests to given / expect
RoleBasedAuthFilter roleBasedAuthFilter = new RoleBasedAuthFilter() | ||
def "OPTIONS request is authorized which bypasses the filter"() { | ||
when: "received an OPTIONS request and that the user is in the authorized roles" | ||
securityContext.isUserInRole() >> false |
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 not be triggering when you expect it to
985e810
to
cf6928a
Compare
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.
CHANGELOG still needed
*/ | ||
protected boolean isUserInRole (List<String> allowedUserRoles, ContainerRequestContext containerRequestContext) { | ||
SecurityContext securityContext = containerRequestContext.getSecurityContext(); | ||
return allowedUserRoles.isEmpty() ? true : allowedUserRoles.stream().anyMatch(securityContext::isUserInRole); |
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.
Can simplify this using ||
like so: allowedUserRoles.isEmpty() || allowedUserRoles.stream().anyMatch(securityContext::isUserInRole);
} | ||
|
||
def cleanup() { | ||
// Release the test web container | ||
jtb.tearDown() | ||
SYSTEM_CONFIG.clearProperty(SYSTEM_CONFIG.getPackageVariableName("user_roles")) |
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.
We can't assume that this was unset when the test started. We need to capture the value of the property (as a String
) in the setup
, and then use that captured value to reset(capturedValue)
in the cleanup.
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.
Just those last three small things and this will be ready!
|
||
try { | ||
oldUserRoles = Optional.of(SYSTEM_CONFIG.getStringProperty(userRolesSetting)) | ||
} catch(SystemConfigException e) { |
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.
For catch
blocks where we're ignoring the exception, it's best to call the exception ignored
(or something like that) to make it clear that we're intentionally not doing anything with the exception.
@@ -15,49 +18,71 @@ import javax.ws.rs.core.SecurityContext | |||
@Timeout(30) | |||
class RoleBasedAuthFilterSpec extends Specification { | |||
|
|||
JerseyTestBinder jtb | |||
private static final SystemConfig SYSTEM_CONFIG = SystemConfigProvider.getInstance() | |||
private static final String userRolesSetting = SYSTEM_CONFIG.getPackageVariableName("user_roles") |
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.
Since this variable is static
, it should be in STATIC_CASE
// Release the test web container | ||
jtb.tearDown() | ||
// Restore pre-test setting value if exists | ||
oldUserRoles.ifPresent({userRoles -> SYSTEM_CONFIG.resetProperty(userRolesSetting, userRoles)}) |
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.
This needs to clear the setting if the Optional
is empty, which this doesn't do.
9e641be
to
bf007a7
Compare
bf007a7
to
ebedd47
Compare
ebedd47
to
26d4ae0
Compare
👍 |
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.
Just minor comments
@@ -28,12 +28,16 @@ Current | |||
|
|||
### Fixed: | |||
|
|||
- [Fix and refactor role based filter to allow CORS](https://github.com/yahoo/fili/pull/99) | |||
* Fix `RoleBasedAuthFilter` to bypass `OPTIONS` request for CORS |
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.
Can we expand what CORS
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.
I think changelog is not the place to explain things, google CORS will give plenty of answers. What do you think? @cdeszaq
* @return true if request is authorized, false otherwise | ||
*/ | ||
private boolean isAuthorized(ContainerRequestContext containerRequestContext) { | ||
return isBypassMethod(containerRequestContext) || isUserInRole(getAllowedUserRoles(), containerRequestContext); |
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.
In getAllowedUserRoles()
what is the list that is returned when the bard__user_roles
variable is uncommented. Is it not an empty list?
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.
returns it is a list with an empty string
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.
This is a good point. It might not be a bad idea to add this behavior description to the getList*
methods in the SystemConfig
interface (and possibly add a test asserting the behavior).
That said, I'm also OK making this piece an issue and coming back to it.
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 was thinking we should just make it a bug issue and fix it later on, because it should be obvious that this behavior is undesirable.
👍 |
RoleBasedAuthFilter
to bypassOPTIONS
request for CORSuser_roles
is declared but unset still reads as a list with empty string (included a temporary fix by deleting the variable declaration)RoleBasedAuthFilter
andRoleBasedAuthFilterSpec
for better testing