From 94d3148111978f7966c9374a5f83a14ffc32fcab Mon Sep 17 00:00:00 2001 From: JumasJM <108924588+JumasJM@users.noreply.github.com> Date: Mon, 24 Jun 2024 12:36:55 +0200 Subject: [PATCH] RBAC integration (#168) PR includes: - RBAC integretion in 'rest' module - updated tests - added dependencies in build.gradle files - V3 schema in archive module for storing roles and groups from json_data into rbac_labels - javadoc Added new package 'rbac' to rest sub-module with sub-package 'annotations' for spring AOP. Added new classes: RbacHttpFilter First step of validation. Handles: - no validation healthcheck - stores all groups and roles values from request - creates object of type UserType for further processing - prevents triggering endpoints when request is not validated RbacAccessAspect Aspect class for handling annotated rest controller methods. Handles: - triggering methods only accessible by admin - triggering BulkResponse methods and modifing returned object - triggering methods that require certain parameters in path or in body of request - when user has not access, 403 is thrown RbacDbHandler Validation of certain parameteres before reaching endpoints and returning of certain object (omitting direct endpoints). Handles: - checks if certain parameters are present in db and if user has access due to provided values in groups and roles of header - returns all Task/Workflow definitions and SeachResult object with param Workflow/WorkflowSummary directly RbacProperties Properties class to return admin roles and groups specified in properties file. UserType POJO for creating user object. TYPE: Improvement JIRA: DEP-686 Signed-off-by: jmasar Co-authored-by: jmasar --- .../client/http/ClientRequestHandler.java | 8 +- conductor-community | 2 +- .../core/dal/ExecutionDAOFacade.java | 34 +++ .../conductor/core/index/NoopIndexDAO.java | 5 + .../netflix/conductor/dao/ExecutionDAO.java | 11 + .../com/netflix/conductor/dao/IndexDAO.java | 2 + .../netflix/conductor/dao/MetadataDAO.java | 9 + .../conductor/service/ExecutionService.java | 28 ++ .../conductor/service/MetadataService.java | 9 + .../service/MetadataServiceImpl.java | 21 ++ .../conductor/service/WorkflowService.java | 14 + .../service/WorkflowServiceImpl.java | 35 +++ .../es6/dao/index/ElasticSearchDAOV6.java | 6 + .../es6/dao/index/ElasticSearchRestDAOV6.java | 6 + .../redis/dao/RedisExecutionDAO.java | 26 ++ .../conductor/redis/dao/RedisMetadataDAO.java | 23 ++ rest/build.gradle | 4 + .../rest/controllers/AdminResource.java | 6 + .../ApplicationExceptionMapper.java | 17 ++ .../rest/controllers/EventResource.java | 6 + .../rest/controllers/MetadataResource.java | 15 + .../rest/controllers/QueueAdminResource.java | 5 + .../rest/controllers/TaskResource.java | 14 + .../controllers/WorkflowBulkResource.java | 6 + .../rest/controllers/WorkflowResource.java | 22 ++ .../conductor/rest/rbac/RbacAccessAspect.java | 261 ++++++++++++++++++ .../conductor/rest/rbac/RbacDbHandler.java | 151 ++++++++++ .../conductor/rest/rbac/RbacHttpFilter.java | 202 ++++++++++++++ .../conductor/rest/rbac/RbacProperties.java | 45 +++ .../netflix/conductor/rest/rbac/UserType.java | 34 +++ .../rbac/annotations/RbacAdminAccess.java | 22 ++ .../rest/rbac/annotations/RbacBulkAccess.java | 22 ++ .../rbac/annotations/RbacPathVarObject.java | 22 ++ .../src/main/resources/application.properties | 6 +- .../src/test/resources/application.properties | 2 +- test-harness/build.gradle | 1 + .../resiliency/QueueResiliencySpec.groovy | 6 + .../http/AbstractHttpEndToEndTest.java | 12 + .../application-integrationtest.properties | 2 +- 39 files changed, 1117 insertions(+), 5 deletions(-) create mode 100644 rest/src/main/java/com/netflix/conductor/rest/rbac/RbacAccessAspect.java create mode 100644 rest/src/main/java/com/netflix/conductor/rest/rbac/RbacDbHandler.java create mode 100644 rest/src/main/java/com/netflix/conductor/rest/rbac/RbacHttpFilter.java create mode 100644 rest/src/main/java/com/netflix/conductor/rest/rbac/RbacProperties.java create mode 100644 rest/src/main/java/com/netflix/conductor/rest/rbac/UserType.java create mode 100644 rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacAdminAccess.java create mode 100644 rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacBulkAccess.java create mode 100644 rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacPathVarObject.java diff --git a/client/src/main/java/com/netflix/conductor/client/http/ClientRequestHandler.java b/client/src/main/java/com/netflix/conductor/client/http/ClientRequestHandler.java index 38749c1c55..55d2cd5a3a 100644 --- a/client/src/main/java/com/netflix/conductor/client/http/ClientRequestHandler.java +++ b/client/src/main/java/com/netflix/conductor/client/http/ClientRequestHandler.java @@ -33,6 +33,9 @@ public class ClientRequestHandler { private final Client client; + private final String FROM_HEADER = "from"; + private final String HEADER_VALUE = "test"; + public ClientRequestHandler( ClientConfig config, ClientHandler handler, ClientFilter... filters) { ObjectMapper objectMapper = new ObjectMapperProvider().getObjectMapper(); @@ -60,9 +63,10 @@ public BulkResponse delete(URI uri, Object body) { if (body != null) { return client.resource(uri) .type(MediaType.APPLICATION_JSON_TYPE) + .header(FROM_HEADER, HEADER_VALUE) .delete(BulkResponse.class, body); } else { - client.resource(uri).delete(); + client.resource(uri).header(FROM_HEADER, HEADER_VALUE).delete(); } return null; } @@ -70,12 +74,14 @@ public BulkResponse delete(URI uri, Object body) { public ClientResponse get(URI uri) { return client.resource(uri) .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN) + .header(FROM_HEADER, HEADER_VALUE) .get(ClientResponse.class); } public WebResource.Builder getWebResourceBuilder(URI URI, Object entity) { return client.resource(URI) .type(MediaType.APPLICATION_JSON) + .header(FROM_HEADER, HEADER_VALUE) .entity(entity) .accept(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON); } diff --git a/conductor-community b/conductor-community index 8a73d3f9b6..b38adcebe0 160000 --- a/conductor-community +++ b/conductor-community @@ -1 +1 @@ -Subproject commit 8a73d3f9b6ad2589b2a9f7c94391b32acf337615 +Subproject commit b38adcebe08e29c2c3b38dcb780621ee0c74ca28 diff --git a/core/src/main/java/com/netflix/conductor/core/dal/ExecutionDAOFacade.java b/core/src/main/java/com/netflix/conductor/core/dal/ExecutionDAOFacade.java index 3921b86442..dae2bc1f00 100644 --- a/core/src/main/java/com/netflix/conductor/core/dal/ExecutionDAOFacade.java +++ b/core/src/main/java/com/netflix/conductor/core/dal/ExecutionDAOFacade.java @@ -759,6 +759,40 @@ public void populateTaskData(TaskModel taskModel) { } } + public boolean hasAccess(Object[] args, List labels) { + return executionDAO.hasAccess(args, labels); + } + + public boolean exists(Object[] args) { + return executionDAO.exists(args); + } + + public List getUserWorkflowIds(List labels) { + return executionDAO.getUserWorkflowIds(labels); + } + + public List getPresentIds(List ids) { + return executionDAO.getPresentIds(ids); + } + + public SearchResult getSearchResultIds(List roles) { + return executionDAO.getSearchResultIds(roles); + } + + public SearchResult getSummaries(SearchResult searchResultIds) { + return indexDAO.getSummaries(searchResultIds); + } + + public SearchResult getUserWorkflows(SearchResult searchResultIds) { + final List workflows = + searchResultIds.getResults().stream() + .map(id -> executionDAO.getWorkflow(id, false)) + .filter(Objects::nonNull) + .map(WorkflowModel::toWorkflow) + .toList(); + return new SearchResult<>(workflows.size(), workflows); + } + class DelayWorkflowUpdate implements Runnable { private final String workflowId; diff --git a/core/src/main/java/com/netflix/conductor/core/index/NoopIndexDAO.java b/core/src/main/java/com/netflix/conductor/core/index/NoopIndexDAO.java index 5b2d958702..df979ba6b7 100644 --- a/core/src/main/java/com/netflix/conductor/core/index/NoopIndexDAO.java +++ b/core/src/main/java/com/netflix/conductor/core/index/NoopIndexDAO.java @@ -160,4 +160,9 @@ public List searchArchivableWorkflows(String indexName, long archiveTtlD public long getWorkflowCount(String query, String freeText) { return 0; } + + @Override + public SearchResult getSummaries(SearchResult searchResultIds) { + return new SearchResult<>(0, Collections.emptyList()); + } } diff --git a/core/src/main/java/com/netflix/conductor/dao/ExecutionDAO.java b/core/src/main/java/com/netflix/conductor/dao/ExecutionDAO.java index 4d17140272..130b1c4930 100644 --- a/core/src/main/java/com/netflix/conductor/dao/ExecutionDAO.java +++ b/core/src/main/java/com/netflix/conductor/dao/ExecutionDAO.java @@ -16,6 +16,7 @@ import com.netflix.conductor.common.metadata.events.EventExecution; import com.netflix.conductor.common.metadata.tasks.TaskDef; +import com.netflix.conductor.common.run.SearchResult; import com.netflix.conductor.model.TaskModel; import com.netflix.conductor.model.WorkflowModel; @@ -222,4 +223,14 @@ List getWorkflowsByCorrelationId( default List getWorkflowFamily(String workflowId, boolean summaryOnly) { throw new UnsupportedOperationException(); } + + boolean hasAccess(Object[] args, List labels); + + boolean exists(Object[] args); + + List getUserWorkflowIds(List labels); + + List getPresentIds(List ids); + + SearchResult getSearchResultIds(List roles); } diff --git a/core/src/main/java/com/netflix/conductor/dao/IndexDAO.java b/core/src/main/java/com/netflix/conductor/dao/IndexDAO.java index 14e53b2ab5..e2d9f85b5c 100644 --- a/core/src/main/java/com/netflix/conductor/dao/IndexDAO.java +++ b/core/src/main/java/com/netflix/conductor/dao/IndexDAO.java @@ -247,4 +247,6 @@ CompletableFuture asyncUpdateTask( * @return Number of matches for the query */ long getWorkflowCount(String query, String freeText); + + SearchResult getSummaries(SearchResult searchResultIds); } diff --git a/core/src/main/java/com/netflix/conductor/dao/MetadataDAO.java b/core/src/main/java/com/netflix/conductor/dao/MetadataDAO.java index b7e39cf3ad..c3992e16e6 100644 --- a/core/src/main/java/com/netflix/conductor/dao/MetadataDAO.java +++ b/core/src/main/java/com/netflix/conductor/dao/MetadataDAO.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Optional; +import com.netflix.conductor.common.metadata.BaseDef; import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; @@ -86,4 +87,12 @@ public interface MetadataDAO { * @return List the latest versions of the workflow definitions */ List getAllWorkflowDefsLatestVersions(); + + boolean hasAccess(Object[] args, List labels, String uri); + + boolean exists(Object[] args, String uri); + + List getUserTaskDefs(List roles); + + List getUserWorkflowDefs(List roles); } diff --git a/core/src/main/java/com/netflix/conductor/service/ExecutionService.java b/core/src/main/java/com/netflix/conductor/service/ExecutionService.java index e20496de36..d0ce3b1ead 100644 --- a/core/src/main/java/com/netflix/conductor/service/ExecutionService.java +++ b/core/src/main/java/com/netflix/conductor/service/ExecutionService.java @@ -611,4 +611,32 @@ public ExternalStorageLocation getExternalStorageLocation( public List getWorkflowPath(String workflowId) { return executionDAOFacade.getWorkflowPath(workflowId); } + + public boolean hasAccess(Object[] args, List labels) { + return executionDAOFacade.hasAccess(args, labels); + } + + public boolean exists(Object[] args) { + return executionDAOFacade.exists(args); + } + + public List getUserWorkflowIds(List labels) { + return executionDAOFacade.getUserWorkflowIds(labels); + } + + public List getPresentIds(List ids) { + return executionDAOFacade.getPresentIds(ids); + } + + public SearchResult getSearchResultIds(List roles) { + return executionDAOFacade.getSearchResultIds(roles); + } + + public SearchResult getSummaries(SearchResult searchResultIds) { + return executionDAOFacade.getSummaries(searchResultIds); + } + + public SearchResult getUserWorkflows(SearchResult searchResultIds) { + return executionDAOFacade.getUserWorkflows(searchResultIds); + } } diff --git a/core/src/main/java/com/netflix/conductor/service/MetadataService.java b/core/src/main/java/com/netflix/conductor/service/MetadataService.java index 701055ef84..a165d5700c 100644 --- a/core/src/main/java/com/netflix/conductor/service/MetadataService.java +++ b/core/src/main/java/com/netflix/conductor/service/MetadataService.java @@ -23,6 +23,7 @@ import org.springframework.validation.annotation.Validated; +import com.netflix.conductor.common.metadata.BaseDef; import com.netflix.conductor.common.metadata.events.EventHandler; import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; @@ -156,4 +157,12 @@ List getEventHandlersForEvent( boolean activeOnly); List getWorkflowDefsLatestVersions(); + + boolean hasAccess(Object[] args, List labels, String uri); + + boolean exists(Object[] args, String uri); + + List getUserTaskDefs(List roles); + + List getUserWorkflowDefs(List roles); } diff --git a/core/src/main/java/com/netflix/conductor/service/MetadataServiceImpl.java b/core/src/main/java/com/netflix/conductor/service/MetadataServiceImpl.java index 7326ab62df..0460748a80 100644 --- a/core/src/main/java/com/netflix/conductor/service/MetadataServiceImpl.java +++ b/core/src/main/java/com/netflix/conductor/service/MetadataServiceImpl.java @@ -23,6 +23,7 @@ import org.springframework.stereotype.Service; import com.netflix.conductor.common.constraints.OwnerEmailMandatoryConstraint; +import com.netflix.conductor.common.metadata.BaseDef; import com.netflix.conductor.common.metadata.events.EventHandler; import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; @@ -223,6 +224,26 @@ public List getWorkflowDefsLatestVersions() { return metadataDAO.getAllWorkflowDefsLatestVersions(); } + @Override + public boolean hasAccess(Object[] args, List labels, String uri) { + return metadataDAO.hasAccess(args, labels, uri); + } + + @Override + public boolean exists(Object[] args, String uri) { + return metadataDAO.exists(args, uri); + } + + @Override + public List getUserTaskDefs(List roles) { + return metadataDAO.getUserTaskDefs(roles); + } + + @Override + public List getUserWorkflowDefs(List roles) { + return metadataDAO.getUserWorkflowDefs(roles); + } + public Map> getWorkflowNamesAndVersions() { List workflowDefs = metadataDAO.getAllWorkflowDefs(); diff --git a/core/src/main/java/com/netflix/conductor/service/WorkflowService.java b/core/src/main/java/com/netflix/conductor/service/WorkflowService.java index 8a07bc8d41..296b0340be 100644 --- a/core/src/main/java/com/netflix/conductor/service/WorkflowService.java +++ b/core/src/main/java/com/netflix/conductor/service/WorkflowService.java @@ -401,4 +401,18 @@ ExternalStorageLocation getExternalStorageLocation( List getWorkflowPath(String workflowId); List getWorkflowFamily(String workflowId, boolean summaryOnly); + + boolean hasAccess(Object[] args, List labels); + + boolean exists(Object[] args); + + List getUserWorkflowIds(List labels); + + List getPresentIds(List ids); + + SearchResult getSearchResultIds(List roles); + + SearchResult getSummaries(SearchResult searchResultIds); + + SearchResult getUserWorkflows(SearchResult searchResultIds); } diff --git a/core/src/main/java/com/netflix/conductor/service/WorkflowServiceImpl.java b/core/src/main/java/com/netflix/conductor/service/WorkflowServiceImpl.java index d0ab9c0cbe..2b047c7bc9 100644 --- a/core/src/main/java/com/netflix/conductor/service/WorkflowServiceImpl.java +++ b/core/src/main/java/com/netflix/conductor/service/WorkflowServiceImpl.java @@ -195,6 +195,41 @@ public List getWorkflowFamily(String workflowId, boolean summaryOnly) return workflows; } + @Override + public boolean hasAccess(Object[] args, List labels) { + return executionService.hasAccess(args, labels); + } + + @Override + public boolean exists(Object[] args) { + return executionService.exists(args); + } + + @Override + public List getUserWorkflowIds(List labels) { + return executionService.getUserWorkflowIds(labels); + } + + @Override + public List getPresentIds(List ids) { + return executionService.getPresentIds(ids); + } + + @Override + public SearchResult getSearchResultIds(List roles) { + return executionService.getSearchResultIds(roles); + } + + @Override + public SearchResult getSummaries(SearchResult searchResultIds) { + return executionService.getSummaries(searchResultIds); + } + + @Override + public SearchResult getUserWorkflows(SearchResult searchResultIds) { + return executionService.getUserWorkflows(searchResultIds); + } + /** * Removes the workflow from the system. * diff --git a/es6-persistence/src/main/java/com/netflix/conductor/es6/dao/index/ElasticSearchDAOV6.java b/es6-persistence/src/main/java/com/netflix/conductor/es6/dao/index/ElasticSearchDAOV6.java index 0a8c86f353..b33b8e18f1 100644 --- a/es6-persistence/src/main/java/com/netflix/conductor/es6/dao/index/ElasticSearchDAOV6.java +++ b/es6-persistence/src/main/java/com/netflix/conductor/es6/dao/index/ElasticSearchDAOV6.java @@ -680,6 +680,12 @@ public long getWorkflowCount(String query, String freeText) { return count(query, freeText, WORKFLOW_DOC_TYPE); } + @Override + public SearchResult getSummaries(SearchResult searchResultIds) { + throw new UnsupportedOperationException( + "getSummaries is not supported in ElasticSearchDAOV6"); + } + @Override public SearchResult searchTasks( String query, String freeText, int start, int count, List sort) { diff --git a/es6-persistence/src/main/java/com/netflix/conductor/es6/dao/index/ElasticSearchRestDAOV6.java b/es6-persistence/src/main/java/com/netflix/conductor/es6/dao/index/ElasticSearchRestDAOV6.java index 9792d52225..67d6187ef5 100644 --- a/es6-persistence/src/main/java/com/netflix/conductor/es6/dao/index/ElasticSearchRestDAOV6.java +++ b/es6-persistence/src/main/java/com/netflix/conductor/es6/dao/index/ElasticSearchRestDAOV6.java @@ -1117,6 +1117,12 @@ public long getWorkflowCount(String query, String freeText) { } } + @Override + public SearchResult getSummaries(SearchResult searchResultIds) { + throw new UnsupportedOperationException( + "getSummaries is not supported in ElasticSearchRestDAOV6"); + } + private long getObjectCounts(String structuredQuery, String freeTextQuery, String docType) throws ParserException, IOException { QueryBuilder queryBuilder = boolQueryBuilder(structuredQuery, freeTextQuery); diff --git a/redis-persistence/src/main/java/com/netflix/conductor/redis/dao/RedisExecutionDAO.java b/redis-persistence/src/main/java/com/netflix/conductor/redis/dao/RedisExecutionDAO.java index 33902640d4..df204f9390 100644 --- a/redis-persistence/src/main/java/com/netflix/conductor/redis/dao/RedisExecutionDAO.java +++ b/redis-persistence/src/main/java/com/netflix/conductor/redis/dao/RedisExecutionDAO.java @@ -23,6 +23,7 @@ import com.netflix.conductor.common.metadata.events.EventExecution; import com.netflix.conductor.common.metadata.tasks.TaskDef; +import com.netflix.conductor.common.run.SearchResult; import com.netflix.conductor.core.config.ConductorProperties; import com.netflix.conductor.core.exception.TransientException; import com.netflix.conductor.dao.ConcurrentExecutionLimitDAO; @@ -726,6 +727,31 @@ public void removeEventExecution(EventExecution eventExecution) { } } + @Override + public boolean hasAccess(Object[] args, List labels) { + return false; + } + + @Override + public boolean exists(Object[] args) { + return false; + } + + @Override + public List getUserWorkflowIds(List labels) { + return List.of(); + } + + @Override + public List getPresentIds(List ids) { + return List.of(); + } + + @Override + public SearchResult getSearchResultIds(List roles) { + return new SearchResult<>(0, Collections.emptyList()); + } + public List getEventExecutions( String eventHandlerName, String eventName, String messageId, int max) { try { diff --git a/redis-persistence/src/main/java/com/netflix/conductor/redis/dao/RedisMetadataDAO.java b/redis-persistence/src/main/java/com/netflix/conductor/redis/dao/RedisMetadataDAO.java index f7951c1e32..d9bc0560e3 100644 --- a/redis-persistence/src/main/java/com/netflix/conductor/redis/dao/RedisMetadataDAO.java +++ b/redis-persistence/src/main/java/com/netflix/conductor/redis/dao/RedisMetadataDAO.java @@ -29,6 +29,7 @@ import org.springframework.context.annotation.Conditional; import org.springframework.stereotype.Component; +import com.netflix.conductor.common.metadata.BaseDef; import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; import com.netflix.conductor.core.config.ConductorProperties; @@ -320,6 +321,28 @@ public List getAllWorkflowDefsLatestVersions() { return workflows; } + @Override + public boolean hasAccess(Object[] args, List labels, String uri) { + throw new UnsupportedOperationException("hasAccess is not supported in RedisMetadataDAO"); + } + + @Override + public boolean exists(Object[] args, String uri) { + throw new UnsupportedOperationException("exists is not supported in RedisMetadataDAO"); + } + + @Override + public List getUserTaskDefs(List roles) { + throw new UnsupportedOperationException( + "getUserTaskDefs is not supported in RedisMetadataDAO"); + } + + @Override + public List getUserWorkflowDefs(List roles) { + throw new UnsupportedOperationException( + "getUserWorkflowDefs is not supported in RedisMetadataDAO"); + } + private void _createOrUpdate(WorkflowDef workflowDef) { // First set the workflow def jedisProxy.hset( diff --git a/rest/build.gradle b/rest/build.gradle index 97d66d816f..38c74c25ab 100644 --- a/rest/build.gradle +++ b/rest/build.gradle @@ -2,10 +2,14 @@ dependencies { implementation project(':conductor-common') implementation project(':conductor-core') + implementation project(':conductor-community:archive') + implementation project(':conductor-community:persistence:postgres-persistence') implementation 'org.springframework.boot:spring-boot-starter-web' implementation "com.netflix.runtime:health-api:${revHealth}" implementation "org.springdoc:springdoc-openapi-ui:${revOpenapi}" + + implementation 'org.springframework.boot:spring-boot-starter-aop:3.2.1' } diff --git a/rest/src/main/java/com/netflix/conductor/rest/controllers/AdminResource.java b/rest/src/main/java/com/netflix/conductor/rest/controllers/AdminResource.java index 4221917c57..660ede4573 100644 --- a/rest/src/main/java/com/netflix/conductor/rest/controllers/AdminResource.java +++ b/rest/src/main/java/com/netflix/conductor/rest/controllers/AdminResource.java @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController; import com.netflix.conductor.common.metadata.tasks.Task; +import com.netflix.conductor.rest.rbac.annotations.RbacAdminAccess; import com.netflix.conductor.service.AdminService; import io.swagger.v3.oas.annotations.Operation; @@ -41,12 +42,14 @@ public AdminResource(AdminService adminService) { this.adminService = adminService; } + @RbacAdminAccess @Operation(summary = "Get all the configuration parameters") @GetMapping("/config") public Map getAllConfig() { return adminService.getAllConfig(); } + @RbacAdminAccess @GetMapping("/task/{tasktype}") @Operation(summary = "Get the list of pending tasks for a given task type") public List view( @@ -56,12 +59,14 @@ public List view( return adminService.getListOfPendingTask(taskType, start, count); } + @RbacAdminAccess @PostMapping(value = "/sweep/requeue/{workflowId}", produces = TEXT_PLAIN_VALUE) @Operation(summary = "Queue up all the running workflows for sweep") public String requeueSweep(@PathVariable("workflowId") String workflowId) { return adminService.requeueSweep(workflowId); } + @RbacAdminAccess @PostMapping(value = "/consistency/verifyAndRepair/{workflowId}", produces = TEXT_PLAIN_VALUE) @Operation(summary = "Verify and repair workflow consistency") public String verifyAndRepairWorkflowConsistency( @@ -69,6 +74,7 @@ public String verifyAndRepairWorkflowConsistency( return String.valueOf(adminService.verifyAndRepairWorkflowConsistency(workflowId)); } + @RbacAdminAccess @GetMapping("/queues") @Operation(summary = "Get registered queues") public Map getEventQueues( diff --git a/rest/src/main/java/com/netflix/conductor/rest/controllers/ApplicationExceptionMapper.java b/rest/src/main/java/com/netflix/conductor/rest/controllers/ApplicationExceptionMapper.java index ab5c47eee9..8cde364079 100644 --- a/rest/src/main/java/com/netflix/conductor/rest/controllers/ApplicationExceptionMapper.java +++ b/rest/src/main/java/com/netflix/conductor/rest/controllers/ApplicationExceptionMapper.java @@ -24,6 +24,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; import com.netflix.conductor.common.validation.ErrorResponse; import com.netflix.conductor.core.exception.ConflictException; @@ -56,6 +57,12 @@ public class ApplicationExceptionMapper { public ResponseEntity handleAll(HttpServletRequest request, Throwable th) { logException(request, th); + if (th instanceof HttpClientErrorException httpExc) { + ErrorResponse errorResponse = + createUnathorizedErrorResponse(request, httpExc, httpExc.getStatusCode()); + return new ResponseEntity<>(errorResponse, httpExc.getStatusCode()); + } + HttpStatus status = EXCEPTION_STATUS_MAP.getOrDefault(th.getClass(), HttpStatus.INTERNAL_SERVER_ERROR); @@ -78,4 +85,14 @@ private void logException(HttpServletRequest request, Throwable exception) { request.getRequestURI(), exception); } + + private ErrorResponse createUnathorizedErrorResponse( + HttpServletRequest request, HttpClientErrorException exception, HttpStatus status) { + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.setInstance(request.getServerName()); + errorResponse.setStatus(status.value()); + errorResponse.setMessage(exception.getMessage()); + errorResponse.setRetryable(false); + return errorResponse; + } } diff --git a/rest/src/main/java/com/netflix/conductor/rest/controllers/EventResource.java b/rest/src/main/java/com/netflix/conductor/rest/controllers/EventResource.java index 02b620c1ec..67ece3d532 100644 --- a/rest/src/main/java/com/netflix/conductor/rest/controllers/EventResource.java +++ b/rest/src/main/java/com/netflix/conductor/rest/controllers/EventResource.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController; import com.netflix.conductor.common.metadata.events.EventHandler; +import com.netflix.conductor.rest.rbac.annotations.RbacAdminAccess; import com.netflix.conductor.service.EventService; import io.swagger.v3.oas.annotations.Operation; @@ -41,30 +42,35 @@ public EventResource(EventService eventService) { this.eventService = eventService; } + @RbacAdminAccess @PostMapping @Operation(summary = "Add a new event handler.") public void addEventHandler(@RequestBody EventHandler eventHandler) { eventService.addEventHandler(eventHandler); } + @RbacAdminAccess @PutMapping @Operation(summary = "Update an existing event handler.") public void updateEventHandler(@RequestBody EventHandler eventHandler) { eventService.updateEventHandler(eventHandler); } + @RbacAdminAccess @DeleteMapping("/{name}") @Operation(summary = "Remove an event handler") public void removeEventHandlerStatus(@PathVariable("name") String name) { eventService.removeEventHandlerStatus(name); } + @RbacAdminAccess @GetMapping @Operation(summary = "Get all the event handlers") public List getEventHandlers() { return eventService.getEventHandlers(); } + @RbacAdminAccess @GetMapping("/{event}") @Operation(summary = "Get event handlers for a given event") public List getEventHandlersForEvent( diff --git a/rest/src/main/java/com/netflix/conductor/rest/controllers/MetadataResource.java b/rest/src/main/java/com/netflix/conductor/rest/controllers/MetadataResource.java index 023ed2b57d..d3a4be10d2 100644 --- a/rest/src/main/java/com/netflix/conductor/rest/controllers/MetadataResource.java +++ b/rest/src/main/java/com/netflix/conductor/rest/controllers/MetadataResource.java @@ -29,6 +29,8 @@ import com.netflix.conductor.common.metadata.workflow.WorkflowDef; import com.netflix.conductor.common.metadata.workflow.WorkflowDefSummary; import com.netflix.conductor.common.model.BulkResponse; +import com.netflix.conductor.rest.rbac.annotations.RbacAdminAccess; +import com.netflix.conductor.rest.rbac.annotations.RbacPathVarObject; import com.netflix.conductor.service.MetadataService; import io.swagger.v3.oas.annotations.Operation; @@ -45,24 +47,28 @@ public MetadataResource(MetadataService metadataService) { this.metadataService = metadataService; } + @RbacAdminAccess @PostMapping("/workflow") @Operation(summary = "Create a new workflow definition") public void create(@RequestBody WorkflowDef workflowDef) { metadataService.registerWorkflowDef(workflowDef); } + @RbacAdminAccess @PostMapping("/workflow/validate") @Operation(summary = "Validates a new workflow definition") public void validate(@RequestBody WorkflowDef workflowDef) { metadataService.validateWorkflowDef(workflowDef); } + @RbacAdminAccess @PutMapping("/workflow") @Operation(summary = "Create or update workflow definition") public BulkResponse update(@RequestBody List workflowDefs) { return metadataService.updateWorkflowDef(workflowDefs); } + @RbacPathVarObject @Operation(summary = "Retrieves workflow definition along with blueprint") @GetMapping("/workflow/{name}") public WorkflowDef get( @@ -71,6 +77,7 @@ public WorkflowDef get( return metadataService.getWorkflowDef(name, version); } + @RbacPathVarObject @Operation(summary = "Retrieves all workflow definition along with blueprint") @GetMapping("/workflow") public List getAll() { @@ -83,12 +90,14 @@ public List getAll() { return metadataService.getWorkflowNamesAndVersions(); } + @RbacAdminAccess @Operation(summary = "Returns only the latest version of all workflow definitions") @GetMapping("/workflow/latest-versions") public List getAllWorkflowsWithLatestVersions() { return metadataService.getWorkflowDefsLatestVersions(); } + @RbacAdminAccess @DeleteMapping("/workflow/{name}/{version}") @Operation( summary = @@ -98,30 +107,36 @@ public void unregisterWorkflowDef( metadataService.unregisterWorkflowDef(name, version); } + @RbacAdminAccess @PostMapping("/taskdefs") @Operation(summary = "Create new task definition(s)") public void registerTaskDef(@RequestBody List taskDefs) { metadataService.registerTaskDef(taskDefs); } + @RbacAdminAccess @PutMapping("/taskdefs") @Operation(summary = "Update an existing task") public void registerTaskDef(@RequestBody TaskDef taskDef) { metadataService.updateTaskDef(taskDef); } + @RbacPathVarObject @GetMapping(value = "/taskdefs") @Operation(summary = "Gets all task definition") public List getTaskDefs() { return metadataService.getTaskDefs(); } + @RbacPathVarObject + @RbacAdminAccess @GetMapping("/taskdefs/{tasktype}") @Operation(summary = "Gets the task definition") public TaskDef getTaskDef(@PathVariable("tasktype") String taskType) { return metadataService.getTaskDef(taskType); } + @RbacAdminAccess @DeleteMapping("/taskdefs/{tasktype}") @Operation(summary = "Remove a task definition") public void unregisterTaskDef(@PathVariable("tasktype") String taskType) { diff --git a/rest/src/main/java/com/netflix/conductor/rest/controllers/QueueAdminResource.java b/rest/src/main/java/com/netflix/conductor/rest/controllers/QueueAdminResource.java index 70eb058722..b268674024 100644 --- a/rest/src/main/java/com/netflix/conductor/rest/controllers/QueueAdminResource.java +++ b/rest/src/main/java/com/netflix/conductor/rest/controllers/QueueAdminResource.java @@ -23,6 +23,7 @@ import com.netflix.conductor.core.events.queue.DefaultEventQueueProcessor; import com.netflix.conductor.model.TaskModel.Status; +import com.netflix.conductor.rest.rbac.annotations.RbacAdminAccess; import io.swagger.v3.oas.annotations.Operation; @@ -38,18 +39,21 @@ public QueueAdminResource(DefaultEventQueueProcessor defaultEventQueueProcessor) this.defaultEventQueueProcessor = defaultEventQueueProcessor; } + @RbacAdminAccess @Operation(summary = "Get the queue length") @GetMapping(value = "/size") public Map size() { return defaultEventQueueProcessor.size(); } + @RbacAdminAccess @Operation(summary = "Get Queue Names") @GetMapping(value = "/") public Map names() { return defaultEventQueueProcessor.queues(); } + @RbacAdminAccess @Operation(summary = "Publish a message in queue to mark a wait task as completed.") @PostMapping(value = "/update/{workflowId}/{taskRefName}/{status}") public void update( @@ -61,6 +65,7 @@ public void update( defaultEventQueueProcessor.updateByTaskRefName(workflowId, taskRefName, output, status); } + @RbacAdminAccess @Operation(summary = "Publish a message in queue to mark a wait task (by taskId) as completed.") @PostMapping("/update/{workflowId}/task/{taskId}/{status}") public void updateByTaskId( diff --git a/rest/src/main/java/com/netflix/conductor/rest/controllers/TaskResource.java b/rest/src/main/java/com/netflix/conductor/rest/controllers/TaskResource.java index 1b1fe6b7a3..250bdfb364 100644 --- a/rest/src/main/java/com/netflix/conductor/rest/controllers/TaskResource.java +++ b/rest/src/main/java/com/netflix/conductor/rest/controllers/TaskResource.java @@ -32,6 +32,7 @@ import com.netflix.conductor.common.run.ExternalStorageLocation; import com.netflix.conductor.common.run.SearchResult; import com.netflix.conductor.common.run.TaskSummary; +import com.netflix.conductor.rest.rbac.annotations.RbacAdminAccess; import com.netflix.conductor.service.TaskService; import io.swagger.v3.oas.annotations.Operation; @@ -50,6 +51,7 @@ public TaskResource(TaskService taskService) { this.taskService = taskService; } + @RbacAdminAccess @GetMapping("/poll/{tasktype}") @Operation(summary = "Poll for a task of a certain type") public ResponseEntity poll( @@ -62,6 +64,7 @@ public ResponseEntity poll( .orElse(ResponseEntity.noContent().build()); } + @RbacAdminAccess @GetMapping("/poll/batch/{tasktype}") @Operation(summary = "Batch poll for a task of a certain type") public ResponseEntity> batchPoll( @@ -77,6 +80,7 @@ public ResponseEntity> batchPoll( .orElse(ResponseEntity.noContent().build()); } + @RbacAdminAccess @PostMapping(produces = TEXT_PLAIN_VALUE) @Operation(summary = "Update a task") public String updateTask(@RequestBody TaskResult taskResult) { @@ -95,6 +99,7 @@ public List getTaskLogs(@PathVariable("taskId") String taskId) { return taskService.getTaskLogs(taskId); } + @RbacAdminAccess @GetMapping("/{taskId}") @Operation(summary = "Get task by Id") public ResponseEntity getTask(@PathVariable("taskId") String taskId) { @@ -104,6 +109,7 @@ public ResponseEntity getTask(@PathVariable("taskId") String taskId) { .orElse(ResponseEntity.noContent().build()); } + @RbacAdminAccess @GetMapping("/queue/sizes") @Operation(summary = "Deprecated. Please use /tasks/queue/size endpoint") @Deprecated @@ -112,6 +118,7 @@ public Map size( return taskService.getTaskQueueSizes(taskTypes); } + @RbacAdminAccess @GetMapping("/queue/size") @Operation(summary = "Get queue size for a task type.") public Integer taskDepth( @@ -123,36 +130,42 @@ public Integer taskDepth( return taskService.getTaskQueueSize(taskType, domain, executionNamespace, isolationGroupId); } + @RbacAdminAccess @GetMapping("/queue/all/verbose") @Operation(summary = "Get the details about each queue") public Map>> allVerbose() { return taskService.allVerbose(); } + @RbacAdminAccess @GetMapping("/queue/all") @Operation(summary = "Get the details about each queue") public Map all() { return taskService.getAllQueueDetails(); } + @RbacAdminAccess @GetMapping("/queue/polldata") @Operation(summary = "Get the last poll data for a given task type") public List getPollData(@RequestParam("taskType") String taskType) { return taskService.getPollData(taskType); } + @RbacAdminAccess @GetMapping("/queue/polldata/all") @Operation(summary = "Get the last poll data for all task types") public List getAllPollData() { return taskService.getAllPollData(); } + @RbacAdminAccess @PostMapping(value = "/queue/requeue/{taskType}", produces = TEXT_PLAIN_VALUE) @Operation(summary = "Requeue pending tasks") public String requeuePendingTask(@PathVariable("taskType") String taskType) { return taskService.requeuePendingTask(taskType); } + @RbacAdminAccess @Operation( summary = "Search for tasks based in payload and other parameters", description = @@ -168,6 +181,7 @@ public SearchResult search( return taskService.search(start, size, sort, freeText, query); } + @RbacAdminAccess @Operation( summary = "Search for tasks based in payload and other parameters", description = diff --git a/rest/src/main/java/com/netflix/conductor/rest/controllers/WorkflowBulkResource.java b/rest/src/main/java/com/netflix/conductor/rest/controllers/WorkflowBulkResource.java index 409328e811..a002e9cc8c 100644 --- a/rest/src/main/java/com/netflix/conductor/rest/controllers/WorkflowBulkResource.java +++ b/rest/src/main/java/com/netflix/conductor/rest/controllers/WorkflowBulkResource.java @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController; import com.netflix.conductor.common.model.BulkResponse; +import com.netflix.conductor.rest.rbac.annotations.RbacBulkAccess; import com.netflix.conductor.service.WorkflowBulkService; import io.swagger.v3.oas.annotations.Operation; @@ -46,6 +47,7 @@ public WorkflowBulkResource(WorkflowBulkService workflowBulkService) { * @return bulk response object containing a list of succeeded workflows and a list of failed * ones with errors */ + @RbacBulkAccess @PutMapping("/pause") @Operation(summary = "Pause the list of workflows") public BulkResponse pauseWorkflow(@RequestBody List workflowIds) { @@ -59,6 +61,7 @@ public BulkResponse pauseWorkflow(@RequestBody List workflowIds) { * @return bulk response object containing a list of succeeded workflows and a list of failed * ones with errors */ + @RbacBulkAccess @PutMapping("/resume") @Operation(summary = "Resume the list of workflows") public BulkResponse resumeWorkflow(@RequestBody List workflowIds) { @@ -73,6 +76,7 @@ public BulkResponse resumeWorkflow(@RequestBody List workflowIds) { * @return bulk response object containing a list of succeeded workflows and a list of failed * ones with errors */ + @RbacBulkAccess @PostMapping("/restart") @Operation(summary = "Restart the list of completed workflow") public BulkResponse restart( @@ -89,6 +93,7 @@ public BulkResponse restart( * @return bulk response object containing a list of succeeded workflows and a list of failed * ones with errors */ + @RbacBulkAccess @PostMapping("/retry") @Operation(summary = "Retry the last failed task for each workflow from the list") public BulkResponse retry(@RequestBody List workflowIds) { @@ -104,6 +109,7 @@ public BulkResponse retry(@RequestBody List workflowIds) { * @return bulk response object containing a list of succeeded workflows and a list of failed * ones with errors */ + @RbacBulkAccess @PostMapping("/terminate") @Operation(summary = "Terminate workflows execution") public BulkResponse terminate( diff --git a/rest/src/main/java/com/netflix/conductor/rest/controllers/WorkflowResource.java b/rest/src/main/java/com/netflix/conductor/rest/controllers/WorkflowResource.java index 7011c3e2ce..360c510721 100644 --- a/rest/src/main/java/com/netflix/conductor/rest/controllers/WorkflowResource.java +++ b/rest/src/main/java/com/netflix/conductor/rest/controllers/WorkflowResource.java @@ -31,6 +31,8 @@ import com.netflix.conductor.common.metadata.workflow.SkipTaskRequest; import com.netflix.conductor.common.metadata.workflow.StartWorkflowRequest; import com.netflix.conductor.common.run.*; +import com.netflix.conductor.rest.rbac.annotations.RbacAdminAccess; +import com.netflix.conductor.rest.rbac.annotations.RbacPathVarObject; import com.netflix.conductor.service.WorkflowService; import com.netflix.conductor.service.WorkflowTestService; @@ -55,6 +57,7 @@ public WorkflowResource( this.workflowTestService = workflowTestService; } + @RbacPathVarObject @PostMapping(produces = TEXT_PLAIN_VALUE) @Operation( summary = @@ -63,6 +66,7 @@ public String startWorkflow(@RequestBody StartWorkflowRequest request) { return workflowService.startWorkflow(request); } + @RbacAdminAccess @PostMapping(value = "/{name}", produces = TEXT_PLAIN_VALUE) @Operation( summary = @@ -76,6 +80,7 @@ public String startWorkflow( return workflowService.startWorkflow(name, version, correlationId, priority, input); } + @RbacAdminAccess @GetMapping("/{name}/correlated/{correlationId}") @Operation(summary = "Lists workflows for the given correlation id") public List getWorkflows( @@ -88,6 +93,7 @@ public List getWorkflows( return workflowService.getWorkflows(name, correlationId, includeClosed, includeTasks); } + @RbacAdminAccess @PostMapping(value = "/{name}/correlated") @Operation(summary = "Lists workflows for the given correlation id list") public Map> getWorkflows( @@ -100,6 +106,7 @@ public Map> getWorkflows( return workflowService.getWorkflows(name, includeClosed, includeTasks, correlationIds); } + @RbacPathVarObject @GetMapping("/{workflowId}") @Operation(summary = "Gets the workflow by workflow id") public Workflow getExecutionStatus( @@ -109,6 +116,7 @@ public Workflow getExecutionStatus( return workflowService.getExecutionStatus(workflowId, includeTasks); } + @RbacPathVarObject @GetMapping("/family/{workflowId}") @Operation(summary = "Gets the workflow by workflow id") public List getWorkflowFamily( @@ -124,6 +132,7 @@ public List getWorkflowPath(@PathVariable("workflowId") String workflowI return workflowService.getWorkflowPath(workflowId); } + @RbacAdminAccess @DeleteMapping("/{workflowId}/remove") @Operation(summary = "Removes the workflow from the system") public void delete( @@ -143,24 +152,28 @@ public List getRunningWorkflow( return workflowService.getRunningWorkflows(workflowName, version, startTime, endTime); } + @RbacAdminAccess @PutMapping("/decide/{workflowId}") @Operation(summary = "Starts the decision task for a workflow") public void decide(@PathVariable("workflowId") String workflowId) { workflowService.decideWorkflow(workflowId); } + @RbacAdminAccess @PutMapping("/{workflowId}/pause") @Operation(summary = "Pauses the workflow") public void pauseWorkflow(@PathVariable("workflowId") String workflowId) { workflowService.pauseWorkflow(workflowId); } + @RbacAdminAccess @PutMapping("/{workflowId}/resume") @Operation(summary = "Resumes the workflow") public void resumeWorkflow(@PathVariable("workflowId") String workflowId) { workflowService.resumeWorkflow(workflowId); } + @RbacAdminAccess @PutMapping("/{workflowId}/skiptask/{taskReferenceName}") @Operation(summary = "Skips a given task from a current running workflow") public void skipTaskFromWorkflow( @@ -170,6 +183,7 @@ public void skipTaskFromWorkflow( workflowService.skipTaskFromWorkflow(workflowId, taskReferenceName, skipTaskRequest); } + @RbacAdminAccess @PostMapping(value = "/{workflowId}/rerun", produces = TEXT_PLAIN_VALUE) @Operation(summary = "Reruns the workflow from a specific task") public String rerun( @@ -178,6 +192,7 @@ public String rerun( return workflowService.rerunWorkflow(workflowId, request); } + @RbacAdminAccess @PostMapping("/{workflowId}/restart") @Operation(summary = "Restarts a completed workflow") @ResponseStatus( @@ -190,6 +205,7 @@ public void restart( workflowService.restartWorkflow(workflowId, useLatestDefinitions); } + @RbacAdminAccess @PostMapping("/{workflowId}/retry") @Operation(summary = "Retries the last failed task") @ResponseStatus( @@ -205,6 +221,7 @@ public void retry( workflowService.retryWorkflow(workflowId, resumeSubworkflowTasks); } + @RbacAdminAccess @PostMapping("/{workflowId}/resetcallbacks") @Operation(summary = "Resets callback times of all non-terminal SIMPLE tasks to 0") @ResponseStatus( @@ -214,6 +231,7 @@ public void resetWorkflow(@PathVariable("workflowId") String workflowId) { workflowService.resetWorkflow(workflowId); } + @RbacAdminAccess @DeleteMapping("/{workflowId}") @Operation(summary = "Terminate workflow execution") public void terminate( @@ -222,6 +240,7 @@ public void terminate( workflowService.terminateWorkflow(workflowId, reason); } + @RbacPathVarObject @Operation( summary = "Search for workflows based on payload and other parameters", description = @@ -237,6 +256,7 @@ public SearchResult search( return workflowService.searchWorkflows(start, size, sort, freeText, query); } + @RbacPathVarObject @Operation( summary = "Search for workflows based on payload and other parameters", description = @@ -252,6 +272,7 @@ public SearchResult searchV2( return workflowService.searchWorkflowsV2(start, size, sort, freeText, query); } + @RbacAdminAccess @Operation( summary = "Search for workflows based on task parameters", description = @@ -267,6 +288,7 @@ public SearchResult searchWorkflowsByTasks( return workflowService.searchWorkflowsByTasks(start, size, sort, freeText, query); } + @RbacAdminAccess @Operation( summary = "Search for workflows based on task parameters", description = diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacAccessAspect.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacAccessAspect.java new file mode 100644 index 0000000000..c0dadca3c1 --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacAccessAspect.java @@ -0,0 +1,261 @@ +/* + * Copyright 2024 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.netflix.conductor.rest.rbac; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; + +import com.netflix.conductor.common.metadata.BaseDef; +import com.netflix.conductor.common.metadata.workflow.StartWorkflowRequest; +import com.netflix.conductor.common.model.BulkResponse; +import com.netflix.conductor.core.exception.NotFoundException; + +@Aspect +@Component +public class RbacAccessAspect { + + private final RbacDbHandler handler; + + private final RbacHttpFilter filter; + + private Object[] arguments; + + public RbacAccessAspect(RbacDbHandler handler, RbacHttpFilter filter) { + this.handler = handler; + this.filter = filter; + } + + @Pointcut("@annotation(com.netflix.conductor.rest.rbac.annotations.RbacBulkAccess)") + public void rbacBulkAccess() {} + + /** + * Executes methods with admin access control. + * + *

Proceeds only in situations when user is admin. Otherwise, throws 403. + * + * @param joinPoint The proceeding join point, enabling interception of the method call. + * @return The result of the intercepted method call. + * @throws Throwable Throws any throwable exception that occurs during method execution. + */ + @Around("@annotation(com.netflix.conductor.rest.rbac.annotations.RbacAdminAccess)") + public Object triggerAdminMethods(ProceedingJoinPoint joinPoint) throws Throwable { + if (filter.getUser().isAdmin()) { + return joinPoint.proceed(); + } else { + throw new HttpClientErrorException(HttpStatus.FORBIDDEN); + } + } + + /** + * Returns BulkResponse object based on workflow IDs, incorporating role-based access control + * (RBAC). + * + *

When user is not admin, method checks first if roles and groups in header of request are + * present. Ids in request body are checked for presence in db and existing ones checked for + * groups and roles. Ids, that matched are set as a new request body parameter and method + * proceeds. After returning response from endpoint, BulkResponse object is modified with failed + * ids such as ids are missing or user is not eligible to trigger ids and updated BulkResponse + * object is returned. + * + * @param joinPoint The proceeding join point, enabling interception of the method call. + * @param workflowIds A list of workflow IDs for which bulk response is requested. + * @return The bulk response object. + * @throws Throwable Throws any throwable exception that occurs during method execution. + */ + @Around("rbacBulkAccess() && args(workflowIds,..))") + public Object getBulkResponse(ProceedingJoinPoint joinPoint, List workflowIds) + throws Throwable { + + if (filter.getUser().isAdmin()) { + return joinPoint.proceed(); + } else if (!filter.getUser().getRoles().isEmpty()) { + List presentIds = handler.getPresentBulkIds(workflowIds); + List ids = handler.getUserIds(filter.getRoles()); + + if (!ids.isEmpty()) { + List userIds = workflowIds.stream().filter(ids::contains).toList(); + Object[] newArgs = Arrays.copyOf(joinPoint.getArgs(), joinPoint.getArgs().length); + newArgs[0] = userIds; + BulkResponse response = (BulkResponse) joinPoint.proceed(newArgs); + return modifyBulkResponse(workflowIds, presentIds, ids, response); + } + } + throw new HttpClientErrorException(HttpStatus.FORBIDDEN); + } + + /** + * A method to retrieve an object based on the method signature, incorporating role-based access + * control (RBAC). + * + *

When user is not admin, method checks first if roles and groups in header of request are + * present. Method first stores arguments from join point and URI of request. + * + *

When no arguments were provided in the request, user triggered endpoint to get all task or + * workflow definitions. In this case method returns definitions directly via getDefinitions() + * method. + * + *

When URI contains "search", db handler returns all workflow summaries accessible by + * specified user and when URI contains "v2", db handler returns all workflows. + * + *

If previous criteria were not met, method checks if given arguments exists in the db and + * if user is eligible and afterward proceeds to return expected object. Otherwise, 404 is + * thrown when data were not found in db or 403 is thrown when user has no access. + * + * @param joinPoint The proceeding join point, enabling interception of the method call. + * @return The retrieved object based variables. + * @throws Throwable Throws any throwable exception that occurs during method execution. + */ + @Around("@annotation(com.netflix.conductor.rest.rbac.annotations.RbacPathVarObject)") + public Object getPathVarObject(ProceedingJoinPoint joinPoint) throws Throwable { + HttpServletRequest request = filter.getRequest(); + + final String search = "search"; + final String searchV2 = "v2"; + + if (filter.getUser().isAdmin()) { + return joinPoint.proceed(); + } else if (!filter.getUser().getRoles().isEmpty()) { + String objectType = getUriObjectType(request.getRequestURI()); + arguments = joinPoint.getArgs(); + + if (arguments.length == 0) { + return getDefinitions(objectType); + } + + String type = getType(arguments, request); + if (type.contains(search)) { + if (type.contains(searchV2)) { + return handler.getUserWorkflows(filter.getRoles()); + } + return handler.getUserSummaries(filter.getRoles()); + } + + boolean hasAccess = handler.hasAccess(arguments, filter.getRoles(), type); + boolean exists = handler.exists(arguments, type); + + if (exists) { + if (hasAccess) { + return joinPoint.proceed(); + } + } else { + throw new NotFoundException( + "No such %s with param %s found.", objectType, arguments[0]); + } + } + throw new HttpClientErrorException(HttpStatus.FORBIDDEN); + } + + /** + * Determines the type of request based on the provided arguments and HTTP request. + * + *

If the first argument is an instance of {@link StartWorkflowRequest}, method sets name and + * version as arguments. It then returns the string "startWf" to indicate a start workflow + * request. + * + *

Otherwise, it returns the URI of the HTTP request. + * + * @param args The arguments provided to the method. + * @param request The HTTP servlet request associated with the method call. + * @return The type of request, either "startWf" for starting a workflow or the URI of the HTTP + * request. + */ + private String getType(Object[] args, HttpServletRequest request) { + if (args[0] instanceof StartWorkflowRequest wfRequest) { + final String startWf = "startWf"; + arguments = new Object[] {wfRequest.getName(), wfRequest.getVersion()}; + return startWf; + } + return request.getRequestURI(); + } + + /** + * Returns list of TaskDefs or WorkflowDefs accessible by user or empty list. + * + *

Method uses db handler to get definitions due to provided groups and roles and URI. + * + * @param objectType The URI of the request. + * @return List of either TaskDefs or WorkflowDefs accessible by user. Otherwise, if no + * definitions were found, returns empty list. + */ + private List getDefinitions(String objectType) { + List definitions = handler.getUserDefs(filter.getRoles(), objectType); + if (!definitions.isEmpty()) { + return definitions; + } else { + return Collections.emptyList(); + } + } + + /** + * Helper to determine if request targets task or workflow data. + * + * @param uri URI of the request. + * @return Either "task" or "workflow" due to URI. + */ + private String getUriObjectType(String uri) { + final String task = "task"; + final String workflow = "workflow"; + return uri.contains(task) ? task : workflow; + } + + /** + * Modifies returned BulkResponse object. + * + *

Method modifies BulkResponse object by appending failed ids in situation when ids from + * request were not found in the db or user is not eligible to work with. + * + * @param requestIds All ids from original request body. + * @param presentIds Ids from original request body which were found in the db. + * @param userIds Ids that user can work with. + * @param response BulkResponse object. + * @return Modifier BulkResponse object. + */ + private BulkResponse modifyBulkResponse( + List requestIds, + List presentIds, + List userIds, + BulkResponse response) { + List notAccessibleIds = + new ArrayList<>(requestIds.stream().filter(id -> !userIds.contains(id)).toList()); + + if (!notAccessibleIds.isEmpty()) { + List notFoundIds = + notAccessibleIds.stream().filter(id -> !presentIds.contains(id)).toList(); + if (!notFoundIds.isEmpty()) { + notAccessibleIds.removeAll(notFoundIds); + notFoundIds.forEach( + id -> + response.appendFailedResponse( + id, "No such workflow found by id: " + id)); + } + notAccessibleIds.forEach( + id -> + response.appendFailedResponse( + id, + "User is not authorized to trigger workflow with id: " + id)); + } + return response; + } +} diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacDbHandler.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacDbHandler.java new file mode 100644 index 0000000000..1cb3bc846d --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacDbHandler.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.netflix.conductor.rest.rbac; + +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.netflix.conductor.common.metadata.BaseDef; +import com.netflix.conductor.common.run.SearchResult; +import com.netflix.conductor.common.run.Workflow; +import com.netflix.conductor.common.run.WorkflowSummary; +import com.netflix.conductor.core.exception.NotFoundException; +import com.netflix.conductor.service.MetadataService; +import com.netflix.conductor.service.WorkflowService; + +@Component +public class RbacDbHandler { + + private final MetadataService metadataService; + + private final WorkflowService workflowService; + + private static final String METADATA_TYPE = "metadata"; + + private static final String START_WF = "startWf"; + + @Autowired + public RbacDbHandler(MetadataService metadataService, WorkflowService workflowService) { + this.metadataService = metadataService; + this.workflowService = workflowService; + } + + /** + * Returns list of ids accessible by the specified user. + * + *

Method returns ids from db that share values in rbac_labels column with provided roles and + * groups values from request. + * + * @param labels Roles and groups values from request. + * @return List of user accessible ids if found. Otherwise, empty list. + */ + public List getUserIds(List labels) { + return !workflowService.getUserWorkflowIds(labels).isEmpty() + ? workflowService.getUserWorkflowIds(labels) + : Collections.emptyList(); + } + + /** + * Checks if user has access to resource due to provided parameters. + * + *

Method determines if data should be returned from public or archive table and returns + * true/false by getting data from db using provided parameters. + * + * @param args Request arguments. + * @param labels Groups and roles list. + * @param uri URI. + * @return True if the user has access to the resource, false otherwise. + */ + public boolean hasAccess(Object[] args, List labels, String uri) { + if (uri.contains(METADATA_TYPE) || uri.equals(START_WF)) { + return metadataService.hasAccess(args, labels, uri); + } else { + return workflowService.hasAccess(args, labels); + } + } + + /** + * Checks if resource exists in db due to provided parameters. + * + *

Method determines if data should be returned from public or archive table and returns + * true/false if requested data were found. + * + * @param args Request arguments. + * @param uri URI. + * @return True if data exists in the db, false otherwise. + */ + public boolean exists(Object[] args, String uri) { + if (uri.contains(METADATA_TYPE) || uri.equals(START_WF)) { + return metadataService.exists(args, uri); + } else { + return workflowService.exists(args); + } + } + + /** + * Returns found ids in db from body of a request. + * + * @param ids Ids from request body. + * @return List of ids present in db. + */ + public List getPresentBulkIds(List ids) { + return workflowService.getPresentIds(ids); + } + + /** + * Returns TaskDefs or WorkflowDefs accessible by the specified user. + * + * @param roles Roles and groups values list from request. + * @param objectType Either "task" or "workflow". + * @return List of task or workflow definitions. + */ + public List getUserDefs(List roles, String objectType) { + final String taskType = "task"; + return objectType.equals(taskType) + ? metadataService.getUserTaskDefs(roles) + : metadataService.getUserWorkflowDefs(roles); + } + + /** + * Returns list of workflow summaries accessible by the specified user. + * + * @param roles Roles and groups values list from request. + * @return List of workflow summaries accessible if found. Otherwise, throws 404. + */ + public SearchResult getUserSummaries(List roles) { + SearchResult result = + workflowService.getSummaries(workflowService.getSearchResultIds(roles)); + if (!result.getResults().isEmpty()) { + return result; + } + throw new NotFoundException("No workflow summaries to be returned."); + } + + /** + * Returns list of workflows accessible by the specified user. + * + * @param roles Roles and groups values list from request. + * @return List of workflows accessible if found. Otherwise, throws 404. + */ + public SearchResult getUserWorkflows(List roles) { + SearchResult result = + workflowService.getUserWorkflows(workflowService.getSearchResultIds(roles)); + if (!result.getResults().isEmpty()) { + return result; + } + throw new NotFoundException("No workflows to be returned."); + } +} diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacHttpFilter.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacHttpFilter.java new file mode 100644 index 0000000000..f4c75c6285 --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacHttpFilter.java @@ -0,0 +1,202 @@ +/* + * Copyright 2024 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.netflix.conductor.rest.rbac; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +@Component +public class RbacHttpFilter implements Filter { + + private UserType user; + + private final RbacProperties properties; + + private List roles; + + private HttpServletRequest request; + + private boolean testingUser; + + public RbacHttpFilter(RbacProperties properties) { + this.properties = properties; + } + + /** + * Intercepts HTTP requests and filters them based on certain criteria. + * + *

If request is of type "healthcheck", no validation is done. Otherwise, method stores all + * groups and roles values from request, validates headers by checking if headers names contains + * "from" and creates UserType object due to provided header values in properties file and in + * request. + * + *

If validation criteria are met, method proceeds with filtering. Otherwise, sends 401 + * error. + * + * @param servletRequest ServletRequest object representing the HTTP request. + * @param servletResponse ServletResponse object representing the HTTP response + * @param filterChain FilterChain object to proceed with the filter chain. + * @throws IOException IOException if an input or output error occurs while filtering the + * request or response. + * @throws ServletException ServletException if the request could not be handled. + */ + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + final String healthCheck = "health"; + if (request.getRequestURI().contains(healthCheck)) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + setRolesList(request); + this.request = request; + + if (validateHeaders(Collections.list(request.getHeaderNames()))) { + if (!testingUser) { + user = createUser(getAdminRoles(), roles); + } + filterChain.doFilter(servletRequest, servletResponse); + } else { + response.sendError(HttpStatus.UNAUTHORIZED.value()); + } + } + + /** + * Checks if headers names contains "from". + * + * @param headers List of headers names. + * @return True if found, false otherwise. + */ + private boolean validateHeaders(List headers) { + final String fromHeader = "from"; + return headers.stream().anyMatch(fromHeader::equals); + } + + /** + * Creates a user object based on the provided roles and groups, considering administrative + * access. + * + *

If roles and groups from request contain same values as set in properties file, UserType + * object is created as admin. Otherwise, UserType object is created as user. + * + * @param adminRoles Roles and groups set in properties file + * @param roles Roles and groups from the request + * @return UserType object representing the user with appropriate roles and administrative + * status. + */ + private UserType createUser(List adminRoles, List roles) { + if (adminRoles.stream().anyMatch(roles::contains)) { + return new UserType(adminRoles, true); + } else { + if (!roles.isEmpty()) { + return new UserType(roles, false); + } else { + return new UserType(Collections.emptyList(), false); + } + } + } + + /** + * Stores roles and groups from request into collection. + * + *

By using getHeadersList(), method gets all values from specified headers and stores them + * into collection. + * + * @param request HttpServletRequest object + */ + private void setRolesList(HttpServletRequest request) { + final String requestRoles = "x-auth-user-roles"; + final String requestGroups = "x-auth-user-groups"; + roles = + Stream.of( + getHeadersList(request.getHeader(requestRoles)), + getHeadersList(request.getHeader(requestGroups))) + .flatMap(List::stream) + .toList(); + } + + /** + * Returns list of all values present in specified request header. + * + *

Method updates received value from request header if present and returns all values from + * request header in list. + * + * @param request Name of the request header + * @return Collection of values from specified header if header is present. Otherwise, returns + * empty list. + */ + private List getHeadersList(String request) { + if (request != null && !request.isEmpty()) { + return Arrays.stream(request.split(",\\s*")).map(String::trim).toList(); + } + return Collections.emptyList(); + } + + /** + * Returns admin roles and groups as admin roles list. + * + * @return List of admin roles and groups + */ + private List getAdminRoles() { + List adminRoles = + new ArrayList<>( + Optional.ofNullable(properties.getAdminRoles()) + .orElse(Collections.emptyList())); + List adminGroups = + Optional.ofNullable(properties.getAdminGroups()).orElse(Collections.emptyList()); + adminRoles.addAll(adminGroups); + return adminRoles; + } + + public UserType getUser() { + return user; + } + + public void setUser(UserType user) { + this.user = user; + } + + public List getRoles() { + return roles; + } + + public HttpServletRequest getRequest() { + return request; + } + + public void setTestingUser(boolean testingUser) { + this.testingUser = testingUser; + } +} diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacProperties.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacProperties.java new file mode 100644 index 0000000000..3ac51ff889 --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacProperties.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.netflix.conductor.rest.rbac; + +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@Primary +@Component +@ConfigurationProperties(prefix = "conductor.rbac.admin") +public class RbacProperties { + + private List adminRoles = List.of("network-admin", "owner", "admin"); + + private List adminGroups = List.of("group-admin", "group-owner"); + + public List getAdminRoles() { + return adminRoles; + } + + public List getAdminGroups() { + return adminGroups; + } + + public void setAdminRoles(List adminRoles) { + this.adminRoles = adminRoles; + } + + public void setAdminGroups(List adminGroups) { + this.adminGroups = adminGroups; + } +} diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/UserType.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/UserType.java new file mode 100644 index 0000000000..27ef04ad64 --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/UserType.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.netflix.conductor.rest.rbac; + +import java.util.List; + +public class UserType { + + private List roles; + private boolean isAdmin; + + public UserType(List roles, boolean isAdmin) { + this.roles = roles; + this.isAdmin = isAdmin; + } + + public List getRoles() { + return roles; + } + + public boolean isAdmin() { + return isAdmin; + } +} diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacAdminAccess.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacAdminAccess.java new file mode 100644 index 0000000000..eb4a11ef04 --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacAdminAccess.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.netflix.conductor.rest.rbac.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RbacAdminAccess {} diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacBulkAccess.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacBulkAccess.java new file mode 100644 index 0000000000..ea6afe4b28 --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacBulkAccess.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.netflix.conductor.rest.rbac.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RbacBulkAccess {} diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacPathVarObject.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacPathVarObject.java new file mode 100644 index 0000000000..d292b1aece --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/annotations/RbacPathVarObject.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.netflix.conductor.rest.rbac.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RbacPathVarObject {} diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 802f1cc18b..9a57b3b4c6 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -142,4 +142,8 @@ conductor.workflow-execution-lock.type=local_only conductor.app.workflowExecutionLockEnabled=true # Outbox table setting -conductor.outbox.table.enabled=false \ No newline at end of file +conductor.outbox.table.enabled=false + +# RBAC admin setting +#conductor.rbac.admin.admin-roles[0]= +#conductor.rbac.admin.admin-groups[0]= \ No newline at end of file diff --git a/server/src/test/resources/application.properties b/server/src/test/resources/application.properties index e002b55e23..b1dea7d4ba 100644 --- a/server/src/test/resources/application.properties +++ b/server/src/test/resources/application.properties @@ -123,4 +123,4 @@ conductor.workflow-execution-lock.type=noop_lock # Additional modules for metrics collection exposed to Datadog (optional) management.metrics.export.datadog.enabled=${conductor.metrics-datadog.enabled:false} -management.metrics.export.datadog.api-key=${conductor.metrics-datadog.api-key:} +management.metrics.export.datadog.api-key=${conductor.metrics-datadog.api-key:} \ No newline at end of file diff --git a/test-harness/build.gradle b/test-harness/build.gradle index 764f6a2771..3e3f44d4b0 100644 --- a/test-harness/build.gradle +++ b/test-harness/build.gradle @@ -51,6 +51,7 @@ dependencies { testImplementation "org.junit.vintage:junit-vintage-engine" testImplementation "javax.ws.rs:javax.ws.rs-api:${revJAXRS}" testImplementation "org.glassfish.jersey.core:jersey-common:${revJerseyCommon}" + testImplementation "javax.servlet:javax.servlet-api:4.0.1" } test { diff --git a/test-harness/src/test/groovy/com/netflix/conductor/test/resiliency/QueueResiliencySpec.groovy b/test-harness/src/test/groovy/com/netflix/conductor/test/resiliency/QueueResiliencySpec.groovy index 44f7ade064..e98dae24bb 100644 --- a/test-harness/src/test/groovy/com/netflix/conductor/test/resiliency/QueueResiliencySpec.groovy +++ b/test-harness/src/test/groovy/com/netflix/conductor/test/resiliency/QueueResiliencySpec.groovy @@ -27,6 +27,8 @@ import com.netflix.conductor.core.utils.QueueUtils import com.netflix.conductor.core.utils.Utils import com.netflix.conductor.rest.controllers.TaskResource import com.netflix.conductor.rest.controllers.WorkflowResource +import com.netflix.conductor.rest.rbac.RbacHttpFilter +import com.netflix.conductor.rest.rbac.UserType import com.netflix.conductor.test.base.AbstractResiliencySpecification /** @@ -44,6 +46,9 @@ class QueueResiliencySpec extends AbstractResiliencySpecification { @Autowired TaskResource taskResource + @Autowired + RbacHttpFilter filter; + def SIMPLE_TWO_TASK_WORKFLOW = 'integration_test_wf' def setup() { @@ -51,6 +56,7 @@ class QueueResiliencySpec extends AbstractResiliencySpecification { workflowTestUtil.registerWorkflows( 'simple_workflow_1_integration_test.json' ) + filter.setUser(new UserType(List.of("admin"), true)) } /// Workflow Resource endpoints diff --git a/test-harness/src/test/java/com/netflix/conductor/test/integration/http/AbstractHttpEndToEndTest.java b/test-harness/src/test/java/com/netflix/conductor/test/integration/http/AbstractHttpEndToEndTest.java index 7bf791d751..b3ed989b16 100644 --- a/test-harness/src/test/java/com/netflix/conductor/test/integration/http/AbstractHttpEndToEndTest.java +++ b/test-harness/src/test/java/com/netflix/conductor/test/integration/http/AbstractHttpEndToEndTest.java @@ -18,8 +18,10 @@ import java.util.List; import java.util.stream.Collectors; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.web.server.LocalServerPort; @@ -47,6 +49,8 @@ import com.netflix.conductor.common.run.Workflow.WorkflowStatus; import com.netflix.conductor.common.run.WorkflowSummary; import com.netflix.conductor.common.validation.ValidationError; +import com.netflix.conductor.rest.rbac.RbacHttpFilter; +import com.netflix.conductor.rest.rbac.UserType; import com.netflix.conductor.test.integration.AbstractEndToEndTest; import static org.junit.Assert.assertEquals; @@ -69,6 +73,14 @@ public abstract class AbstractHttpEndToEndTest extends AbstractEndToEndTest { protected static MetadataClient metadataClient; protected static EventClient eventClient; + @Autowired RbacHttpFilter filter; + + @Before + public void before() { + filter.setTestingUser(true); + filter.setUser(new UserType(List.of("admin"), true)); + } + @Override protected String startWorkflow(String workflowExecutionName, WorkflowDef workflowDefinition) { StartWorkflowRequest workflowRequest = diff --git a/test-harness/src/test/resources/application-integrationtest.properties b/test-harness/src/test/resources/application-integrationtest.properties index 3c93ecadb4..00f197e1c0 100644 --- a/test-harness/src/test/resources/application-integrationtest.properties +++ b/test-harness/src/test/resources/application-integrationtest.properties @@ -52,4 +52,4 @@ conductor.redis.queue-namespace-prefix=integtest conductor.elasticsearch.index-prefix=conductor conductor.elasticsearch.cluster-health-color=yellow -management.metrics.export.datadog.enabled=false +management.metrics.export.datadog.enabled=false \ No newline at end of file