-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RHCLOUD-37536 | feature: authorization annotation for improved mainta…
…inability (#3293) * feature: authorization annotation for improved maintainability The implemented annotation allows specifying the legacy RBAC role that will be used when RBAC is enabled, and the Kessel permissions for when that one is being used as the authorization back end. The goal is to reduce the boilerplate and repetitive code that we have in the code base, and make it easier for the developers to add authorization in a similar way that it is done with the "RolesAllowed" annotation. * chore: fix typo RHCLOUD-37536 Co-authored-by: Guillaume Duval <[email protected]> --------- Co-authored-by: Guillaume Duval <[email protected]>
- Loading branch information
1 parent
f090ca4
commit ee7973e
Showing
7 changed files
with
748 additions
and
0 deletions.
There are no files selected for viewing
40 changes: 40 additions & 0 deletions
40
backend/src/main/java/com/redhat/cloud/notifications/auth/annotation/Authorization.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.redhat.cloud.notifications.auth.annotation; | ||
|
||
import com.redhat.cloud.notifications.auth.kessel.permission.IntegrationPermission; | ||
import com.redhat.cloud.notifications.auth.kessel.permission.WorkspacePermission; | ||
import jakarta.enterprise.util.Nonbinding; | ||
import jakarta.interceptor.InterceptorBinding; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* Defines the annotation to be able to perform permission checks with Kessel | ||
* at the method level. | ||
*/ | ||
@InterceptorBinding | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target({ElementType.METHOD, ElementType.TYPE}) | ||
public @interface Authorization { | ||
/** | ||
* The Kessel integration permissions defined by the developer. These | ||
* permissions require the method's integration's identifier to be | ||
* annotated with the {@link IntegrationId} annotation to work. | ||
* @return an array of defined integration permissions. | ||
*/ | ||
@Nonbinding IntegrationPermission[] integrationPermissions() default {}; | ||
|
||
/** | ||
* The legacy RBAC role that will be checked when Kessel is disabled. | ||
* @return the legacy RBAC role set for the method. | ||
*/ | ||
@Nonbinding String legacyRBACRole(); | ||
|
||
/** | ||
* The Kessel workspace permissions defined by the developer. | ||
* @return an array of defined workspace permissions. | ||
*/ | ||
@Nonbinding WorkspacePermission[] workspacePermissions() default {}; | ||
} |
157 changes: 157 additions & 0 deletions
157
...rc/main/java/com/redhat/cloud/notifications/auth/annotation/AuthorizationInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package com.redhat.cloud.notifications.auth.annotation; | ||
|
||
import com.redhat.cloud.notifications.auth.kessel.KesselAuthorization; | ||
import com.redhat.cloud.notifications.auth.kessel.permission.IntegrationPermission; | ||
import com.redhat.cloud.notifications.auth.kessel.permission.WorkspacePermission; | ||
import com.redhat.cloud.notifications.auth.rbac.workspace.WorkspaceUtils; | ||
import com.redhat.cloud.notifications.config.BackendConfig; | ||
import com.redhat.cloud.notifications.routers.SecurityContextUtil; | ||
import jakarta.annotation.Priority; | ||
import jakarta.interceptor.AroundInvoke; | ||
import jakarta.interceptor.Interceptor; | ||
import jakarta.interceptor.InvocationContext; | ||
import jakarta.ws.rs.ForbiddenException; | ||
import jakarta.ws.rs.core.SecurityContext; | ||
|
||
import java.lang.reflect.Parameter; | ||
import java.util.UUID; | ||
|
||
/** | ||
* The interceptor's implementation for the {@link Authorization} annotaiton. | ||
* On any method annotated with the aforementioned annotation, it will perform | ||
* authorization checks depending on whether RBAC or Kessel authorization back | ||
* ends are enabled. | ||
*/ | ||
@Interceptor | ||
@Authorization(legacyRBACRole = "") | ||
@Priority(Interceptor.Priority.APPLICATION) | ||
public class AuthorizationInterceptor { | ||
|
||
private final BackendConfig backendConfig; | ||
private final KesselAuthorization kesselAuthorization; | ||
private final WorkspaceUtils workspaceUtils; | ||
|
||
/** | ||
* Constructor for the interceptor. Helps with testing, as otherwise the | ||
* interceptor cannot get its dependencies injected. | ||
* @param backendConfig the back end's configuration bean. | ||
* @param kesselAuthorization the Kessel's authorization bean. | ||
* @param workspaceUtils the workspace utils' bean. | ||
*/ | ||
public AuthorizationInterceptor( | ||
final BackendConfig backendConfig, | ||
final KesselAuthorization kesselAuthorization, | ||
final WorkspaceUtils workspaceUtils | ||
) { | ||
this.backendConfig = backendConfig; | ||
this.kesselAuthorization = kesselAuthorization; | ||
this.workspaceUtils = workspaceUtils; | ||
} | ||
|
||
@AroundInvoke | ||
public Object aroundInvoke(final InvocationContext ctx) throws Exception { | ||
// Grab the annotation from the method. The annotation will never be | ||
// null, because the interceptor only works with those methods that | ||
// have been annotated. | ||
final Authorization annotation = ctx.getMethod().getDeclaredAnnotation(Authorization.class); | ||
|
||
// Get the parameter indexes from the method's definition. By having | ||
// these indexes, we can simply then grab the exact intercepted | ||
// parameters from the array of intercepted method parameters. | ||
final ParameterIndexes parameterIndexes = this.getParameterIndexes(ctx); | ||
|
||
// When the method doesn't have a "SecurityContext" parameter we cannot | ||
// perform the authorization checks, and therefore we cannot continue. | ||
if (parameterIndexes.getSecurityContextIndex().isEmpty()) { | ||
throw new IllegalStateException(String.format("The security context is not set on the method \"%s\", which is needed for the \"KesselRequiredPermission\" annotation to work", ctx.getMethod().getName())); | ||
} | ||
|
||
// Grab the intercepted parameters and make sure that the security | ||
// context is present, as otherwise we cannot perform any authorization | ||
// checks. | ||
final Object[] interceptedParameters = ctx.getParameters(); | ||
|
||
// Since we grabbed the parameter index for the "SecurityContext" | ||
// parameter, we can safely cast that parameter to that so that we can | ||
// use it. | ||
final SecurityContext securityContext = (SecurityContext) interceptedParameters[parameterIndexes.getSecurityContextIndex().get()]; | ||
|
||
// Perform the legacy RBAC check first. The only reason is to be able | ||
// to return early and make the code easier to follow. | ||
if (!this.backendConfig.isKesselRelationsEnabled(SecurityContextUtil.getOrgId(securityContext))) { | ||
// Legacy RBAC permission checking. The permission will have been | ||
// prefetched and processed by the "ConsoleIdentityProvider". | ||
if (securityContext.isUserInRole(annotation.legacyRBACRole())) { | ||
return ctx.proceed(); | ||
} else { | ||
throw new ForbiddenException(); | ||
} | ||
} | ||
|
||
// When the execution reaches this point we can be sure that the | ||
// enabled authorization back end is "Kessel", so we proceed to get the | ||
// Kessel permissions. | ||
final IntegrationPermission[] integrationPermissions = annotation.integrationPermissions(); | ||
final WorkspacePermission[] workspacePermissions = annotation.workspacePermissions(); | ||
|
||
// Make sure that we have either integration permissions or workspace | ||
// permissions to check. If we don't, that probably signals a mistake | ||
// on the developer's side. | ||
if (integrationPermissions.length == 0 && workspacePermissions.length == 0) { | ||
throw new IllegalStateException(String.format("No integration or workspace permissions were set for method \"%s\", and at least one of them is required for the \"KesselRequiredPermission\" annotation to work", ctx.getMethod().getName())); | ||
} | ||
|
||
// Check the workspace permissions first since they are more generic. | ||
for (final WorkspacePermission workspacePermission : workspacePermissions) { | ||
final UUID workspaceId = this.workspaceUtils.getDefaultWorkspaceId(SecurityContextUtil.getOrgId(securityContext)); | ||
|
||
this.kesselAuthorization.hasPermissionOnWorkspace(securityContext, workspacePermission, workspaceId); | ||
} | ||
|
||
// If no integration permissions are specified we can simply skip any | ||
// further checks. | ||
if (integrationPermissions.length == 0) { | ||
return ctx.proceed(); | ||
} | ||
|
||
// We need to make sure that we spotted the integration's identifier in | ||
// the method, as otherwise we will not be able to perform the checks. | ||
if (parameterIndexes.getIntegrationIdIndex().isEmpty()) { | ||
throw new IllegalStateException(String.format("The integration ID is not annotated on the method \"%s\", which is needed for the \"KesselRequiredPermission\" annotation to work", ctx.getMethod().getName())); | ||
} | ||
|
||
// Now it is safe to grab the intercepted integration id... | ||
final UUID integrationId = (UUID) interceptedParameters[parameterIndexes.getIntegrationIdIndex().get()]; | ||
// ... and check the principal's permission. | ||
for (final IntegrationPermission integrationPermission : integrationPermissions) { | ||
this.kesselAuthorization.hasPermissionOnIntegration(securityContext, integrationPermission, integrationId); | ||
} | ||
|
||
return ctx.proceed(); | ||
} | ||
|
||
/** | ||
* Grabs the indexes for the parameters we are interested in. | ||
* @param ctx the method's invocation context. | ||
* @return a {@link ParameterIndexes} object containing the relevant | ||
* indexes we are interested in. | ||
*/ | ||
private ParameterIndexes getParameterIndexes(final InvocationContext ctx) { | ||
final ParameterIndexes parameterIndexes = new ParameterIndexes(); | ||
|
||
final Parameter[] methodParameters = ctx.getMethod().getParameters(); | ||
for (int i = 0; i < methodParameters.length; i++) { | ||
final Parameter parameter = methodParameters[i]; | ||
|
||
if (parameter.getType().equals(SecurityContext.class)) { | ||
parameterIndexes.setSecurityContextIndex(i); | ||
continue; | ||
} | ||
|
||
if (parameter.getAnnotation(IntegrationId.class) != null) { | ||
parameterIndexes.setIntegrationIdIndex(i); | ||
} | ||
} | ||
return parameterIndexes; | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
backend/src/main/java/com/redhat/cloud/notifications/auth/annotation/IntegrationId.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.redhat.cloud.notifications.auth.annotation; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* An annotation that is simply used to tell the {@link AuthorizationInterceptor} | ||
* which parameter contains the received integration identifier. | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target({ElementType.PARAMETER}) | ||
public @interface IntegrationId { | ||
} |
30 changes: 30 additions & 0 deletions
30
backend/src/main/java/com/redhat/cloud/notifications/auth/annotation/ParameterIndexes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.redhat.cloud.notifications.auth.annotation; | ||
|
||
import java.util.Optional; | ||
|
||
/** | ||
* Holds the required parameter indexes to be able to perform the proper | ||
* authorizations in the {@link AuthorizationInterceptor}. | ||
*/ | ||
class ParameterIndexes { | ||
private Integer securityContextIndex; | ||
private Integer integrationIdIndex; | ||
|
||
ParameterIndexes() { } | ||
|
||
public Optional<Integer> getSecurityContextIndex() { | ||
return Optional.ofNullable(this.securityContextIndex); | ||
} | ||
|
||
public void setSecurityContextIndex(final Integer securityContextIndex) { | ||
this.securityContextIndex = securityContextIndex; | ||
} | ||
|
||
public Optional<Integer> getIntegrationIdIndex() { | ||
return Optional.ofNullable(this.integrationIdIndex); | ||
} | ||
|
||
public void setIntegrationIdIndex(final Integer integrationIdIndex) { | ||
this.integrationIdIndex = integrationIdIndex; | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
...t/java/com/redhat/cloud/notifications/auth/annotation/AuthorizationInterceptorHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package com.redhat.cloud.notifications.auth.annotation; | ||
|
||
import com.redhat.cloud.notifications.auth.kessel.permission.IntegrationPermission; | ||
import com.redhat.cloud.notifications.auth.kessel.permission.WorkspacePermission; | ||
import jakarta.ws.rs.core.SecurityContext; | ||
|
||
import java.util.UUID; | ||
|
||
/** | ||
* A simple helper class which defines a few methods for the {@link AuthorizationInterceptorTest} | ||
* class. For some reason, defining them there made the method's annotations | ||
* disappear when trying to use reflection to get them. | ||
*/ | ||
public class AuthorizationInterceptorHelper { | ||
|
||
@Authorization(legacyRBACRole = AuthorizationInterceptorTest.LEGACY_RBAC_ROLE) | ||
public void testMethodWithoutSecurityContext(final String ignored, final UUID ignoredTwo) { } | ||
|
||
@Authorization(legacyRBACRole = AuthorizationInterceptorTest.LEGACY_RBAC_ROLE) | ||
public void testMethodWithRBACRole(final SecurityContext ignored, final String ignoredTwo, final UUID ignoredThree) { } | ||
|
||
@Authorization( | ||
legacyRBACRole = AuthorizationInterceptorTest.LEGACY_RBAC_ROLE, | ||
workspacePermissions = {WorkspacePermission.BUNDLES_VIEW, WorkspacePermission.APPLICATIONS_VIEW, WorkspacePermission.EVENT_TYPES_VIEW} | ||
) | ||
public void testMethodWithWorkspacePermissions(final SecurityContext ignored, final String ignoredTwo, final UUID ignoredThree) { } | ||
|
||
@Authorization( | ||
legacyRBACRole = AuthorizationInterceptorTest.LEGACY_RBAC_ROLE, | ||
workspacePermissions = {WorkspacePermission.BUNDLES_VIEW, WorkspacePermission.APPLICATIONS_VIEW, WorkspacePermission.EVENT_TYPES_VIEW}, | ||
integrationPermissions = {IntegrationPermission.VIEW, IntegrationPermission.VIEW_HISTORY} | ||
) | ||
public void testMethodWithWorkspaceAndIntegrationPermissionsMissingIntegrationId(final SecurityContext ignored, final String ignoredTwo, final UUID ignoredThree) { } | ||
|
||
@Authorization( | ||
legacyRBACRole = AuthorizationInterceptorTest.LEGACY_RBAC_ROLE, | ||
workspacePermissions = {WorkspacePermission.BUNDLES_VIEW, WorkspacePermission.APPLICATIONS_VIEW, WorkspacePermission.EVENT_TYPES_VIEW}, | ||
integrationPermissions = {IntegrationPermission.VIEW, IntegrationPermission.VIEW_HISTORY} | ||
) | ||
public void testMethodWithWorkspaceAndIntegrationPermissions(final SecurityContext ignored, final String ignoredTwo, @IntegrationId final UUID ignoredThree) { } | ||
|
||
} |
Oops, something went wrong.