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..8c3ae8b968 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 @@ -60,9 +60,14 @@ public BulkResponse delete(URI uri, Object body) { if (body != null) { return client.resource(uri) .type(MediaType.APPLICATION_JSON_TYPE) + .header("from", "admin") + .header("x-auth-user-roles", "admin") .delete(BulkResponse.class, body); } else { - client.resource(uri).delete(); + client.resource(uri) + .header("from", "admin") + .header("x-auth-user-roles", "admin") + .delete(); } return null; } @@ -70,6 +75,8 @@ 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", "admin") + .header("x-auth-user-roles", "admin") .get(ClientResponse.class); } @@ -77,7 +84,9 @@ public WebResource.Builder getWebResourceBuilder(URI URI, Object entity) { return client.resource(URI) .type(MediaType.APPLICATION_JSON) .entity(entity) - .accept(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON); + .accept(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON) + .header("From", "admin") + .header("x-auth-user-roles", "admin"); } private boolean isNewerJacksonVersion() { diff --git a/conductor-community b/conductor-community index 8a73d3f9b6..94a40ea412 160000 --- a/conductor-community +++ b/conductor-community @@ -1 +1 @@ -Subproject commit 8a73d3f9b6ad2589b2a9f7c94391b32acf337615 +Subproject commit 94a40ea412585836062027a2837a8c284b17d6ed 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..98a5580588 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,22 @@ public void populateTaskData(TaskModel taskModel) { } } + public List getLabels(String wfId) { + return executionDAO.getLabels(wfId); + } + + public List getTaskDescription(String taskType) { + return executionDAO.getTaskDescription(taskType); + } + + public SearchResult getUserSummaries(List groupsAndRoles) { + return indexDAO.getUserSummaries(groupsAndRoles); + } + + public List getUserIds(List groupsAndRoles, List wfIds) { + return executionDAO.getUserIds(groupsAndRoles, wfIds); + } + class DelayWorkflowUpdate implements Runnable { private final String workflowId; diff --git a/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutor.java b/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutor.java index 3a5c801aa8..d9351670fd 100644 --- a/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutor.java +++ b/core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutor.java @@ -1790,4 +1790,8 @@ private void expediteLazyWorkflowEvaluation(String workflowId) { LOGGER.info("Pushed workflow {} to {} for expedited evaluation", workflowId, DECIDER_QUEUE); } + + public List getUserIds(List groupsAndRoles, List wfIds) { + return executionDAOFacade.getUserIds(groupsAndRoles, wfIds); + } } 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..0847fd6f3f 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 getUserSummaries(List groupsAndRoles) { + return null; + } } 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..16f58276a0 100644 --- a/core/src/main/java/com/netflix/conductor/dao/ExecutionDAO.java +++ b/core/src/main/java/com/netflix/conductor/dao/ExecutionDAO.java @@ -222,4 +222,10 @@ List getWorkflowsByCorrelationId( default List getWorkflowFamily(String workflowId, boolean summaryOnly) { throw new UnsupportedOperationException(); } + + List getLabels(String wfId); + + List getTaskDescription(String taskType); + + List getUserIds(List groupsAndRoles, List wfIds); } 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..a8b5366ca8 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 getUserSummaries(List groupsAndRoles); } 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..85ac19ff6a 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,10 @@ public interface MetadataDAO { * @return List the latest versions of the workflow definitions */ List getAllWorkflowDefsLatestVersions(); + + List getDescription(BaseDef def); + + List getUserWorkflowDefs(List groupsAndRoles); + + List getUserTaskDefs(List groupsAndRoles); } 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..09c00b9d62 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,16 @@ public ExternalStorageLocation getExternalStorageLocation( public List getWorkflowPath(String workflowId) { return executionDAOFacade.getWorkflowPath(workflowId); } + + public List getLabels(String wfId) { + return executionDAOFacade.getLabels(wfId); + } + + public List getTaskDescription(String taskType) { + return executionDAOFacade.getTaskDescription(taskType); + } + + public SearchResult getUserSummaries(List groupsAndRoles) { + return executionDAOFacade.getUserSummaries(groupsAndRoles); + } } 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..91dba77634 100644 --- a/core/src/main/java/com/netflix/conductor/service/MetadataService.java +++ b/core/src/main/java/com/netflix/conductor/service/MetadataService.java @@ -156,4 +156,12 @@ List getEventHandlersForEvent( boolean activeOnly); List getWorkflowDefsLatestVersions(); + + List getWorkflowDescription(String id, Integer version); + + List getTaskDescription(String id); + + List getUserWorkflowDefs(List groupsAndRoles); + + List getUserTaskDefs(List groupsAndRoles); } 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..c094a20441 100644 --- a/core/src/main/java/com/netflix/conductor/service/MetadataServiceImpl.java +++ b/core/src/main/java/com/netflix/conductor/service/MetadataServiceImpl.java @@ -223,6 +223,26 @@ public List getWorkflowDefsLatestVersions() { return metadataDAO.getAllWorkflowDefsLatestVersions(); } + @Override + public List getWorkflowDescription(String name, Integer version) { + return metadataDAO.getDescription(getWorkflowDef(name, version)); + } + + @Override + public List getTaskDescription(String name) { + return metadataDAO.getDescription(getTaskDef(name)); + } + + @Override + public List getUserWorkflowDefs(List groupsAndRoles) { + return metadataDAO.getUserWorkflowDefs(groupsAndRoles); + } + + @Override + public List getUserTaskDefs(List groupsAndRoles) { + return metadataDAO.getUserTaskDefs(groupsAndRoles); + } + public Map> getWorkflowNamesAndVersions() { List workflowDefs = metadataDAO.getAllWorkflowDefs(); diff --git a/core/src/main/java/com/netflix/conductor/service/TaskService.java b/core/src/main/java/com/netflix/conductor/service/TaskService.java index 7f4f3d0a67..2ab92355d7 100644 --- a/core/src/main/java/com/netflix/conductor/service/TaskService.java +++ b/core/src/main/java/com/netflix/conductor/service/TaskService.java @@ -250,4 +250,6 @@ SearchResult search( */ ExternalStorageLocation getExternalStorageLocation( String path, String operation, String payloadType); + + List getTaskDefinition(String taskType); } diff --git a/core/src/main/java/com/netflix/conductor/service/TaskServiceImpl.java b/core/src/main/java/com/netflix/conductor/service/TaskServiceImpl.java index 5c07d5cffe..9e8cb9f4da 100644 --- a/core/src/main/java/com/netflix/conductor/service/TaskServiceImpl.java +++ b/core/src/main/java/com/netflix/conductor/service/TaskServiceImpl.java @@ -366,4 +366,9 @@ public ExternalStorageLocation getExternalStorageLocation( String path, String operation, String type) { return executionService.getExternalStorageLocation(path, operation, type); } + + @Override + public List getTaskDefinition(String taskType) { + return executionService.getTaskDescription(taskType); + } } diff --git a/core/src/main/java/com/netflix/conductor/service/WorkflowBulkService.java b/core/src/main/java/com/netflix/conductor/service/WorkflowBulkService.java index 2c1ef0f7fe..74db761fa4 100644 --- a/core/src/main/java/com/netflix/conductor/service/WorkflowBulkService.java +++ b/core/src/main/java/com/netflix/conductor/service/WorkflowBulkService.java @@ -67,4 +67,6 @@ BulkResponse terminate( "Cannot process more than {max} workflows. Please use multiple requests.") List workflowIds, String reason); + + List getUserIds(List groupsAndRoles, List wfIds); } diff --git a/core/src/main/java/com/netflix/conductor/service/WorkflowBulkServiceImpl.java b/core/src/main/java/com/netflix/conductor/service/WorkflowBulkServiceImpl.java index c637b8bd91..809e732385 100644 --- a/core/src/main/java/com/netflix/conductor/service/WorkflowBulkServiceImpl.java +++ b/core/src/main/java/com/netflix/conductor/service/WorkflowBulkServiceImpl.java @@ -164,4 +164,9 @@ public BulkResponse terminate(List workflowIds, String reason) { } return bulkResponse; } + + @Override + public List getUserIds(List groupsAndRoles, List wfIds) { + return workflowExecutor.getUserIds(groupsAndRoles, wfIds); + } } 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..c393f83c46 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,10 @@ ExternalStorageLocation getExternalStorageLocation( List getWorkflowPath(String workflowId); List getWorkflowFamily(String workflowId, boolean summaryOnly); + + List getWorkflowDescription(String id, Integer version); + + List getLabels(String wfId); + + SearchResult getUserSummaries(List groupsAndRoles); } 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..9d81c7c2cd 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,21 @@ public List getWorkflowFamily(String workflowId, boolean summaryOnly) return workflows; } + @Override + public List getWorkflowDescription(String id, Integer version) { + return metadataService.getWorkflowDescription(id, version); + } + + @Override + public List getLabels(String wfId) { + return executionService.getLabels(wfId); + } + + @Override + public SearchResult getUserSummaries(List groupsAndRoles) { + return executionService.getUserSummaries(groupsAndRoles); + } + /** * 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..c9124c0716 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,11 @@ public long getWorkflowCount(String query, String freeText) { return count(query, freeText, WORKFLOW_DOC_TYPE); } + @Override + public SearchResult getUserSummaries(List groupsAndRoles) { + return null; + } + @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..2e8cdb5af0 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,11 @@ public long getWorkflowCount(String query, String freeText) { } } + @Override + public SearchResult getUserSummaries(List groupsAndRoles) { + return null; + } + 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..f1796b23a4 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 @@ -726,6 +726,21 @@ public void removeEventExecution(EventExecution eventExecution) { } } + @Override + public List getLabels(String wfId) { + return null; + } + + @Override + public List getTaskDescription(String taskType) { + return null; + } + + @Override + public List getUserIds(List groupsAndRoles, List wfIds) { + return null; + } + 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..07a25ce7bd 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,21 @@ public List getAllWorkflowDefsLatestVersions() { return workflows; } + @Override + public List getDescription(BaseDef def) { + return null; + } + + @Override + public List getUserWorkflowDefs(List groupsAndRoles) { + return null; + } + + @Override + public List getUserTaskDefs(List groupsAndRoles) { + return null; + } + private void _createOrUpdate(WorkflowDef workflowDef) { // First set the workflow def jedisProxy.hset( diff --git a/rest/build.gradle b/rest/build.gradle index 97d66d816f..0f049bdead 100644 --- a/rest/build.gradle +++ b/rest/build.gradle @@ -1,5 +1,6 @@ dependencies { + implementation project(':conductor-client') implementation project(':conductor-common') implementation project(':conductor-core') 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..9a59195363 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 @@ -14,6 +14,8 @@ import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -23,8 +25,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpServerErrorException; import com.netflix.conductor.common.metadata.events.EventHandler; +import com.netflix.conductor.rest.rbac.HeaderValidatorFilter; import com.netflix.conductor.service.EventService; import io.swagger.v3.oas.annotations.Operation; @@ -41,16 +45,28 @@ public EventResource(EventService eventService) { this.eventService = eventService; } + @Autowired HeaderValidatorFilter filter; + @PostMapping @Operation(summary = "Add a new event handler.") public void addEventHandler(@RequestBody EventHandler eventHandler) { - eventService.addEventHandler(eventHandler); + + if (filter.getUser().isAdmin()) { + eventService.addEventHandler(eventHandler); + } else { + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); + } } @PutMapping @Operation(summary = "Update an existing event handler.") public void updateEventHandler(@RequestBody EventHandler eventHandler) { - eventService.updateEventHandler(eventHandler); + + if (filter.getUser().isAdmin()) { + eventService.updateEventHandler(eventHandler); + } else { + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); + } } @DeleteMapping("/{name}") @@ -62,7 +78,11 @@ public void removeEventHandlerStatus(@PathVariable("name") String name) { @GetMapping @Operation(summary = "Get all the event handlers") public List getEventHandlers() { - return eventService.getEventHandlers(); + + if (filter.getUser().isAdmin()) { + return eventService.getEventHandlers(); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @GetMapping("/{event}") 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..fbce38be69 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 @@ -15,6 +15,8 @@ import java.util.List; import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -24,11 +26,14 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpServerErrorException; import com.netflix.conductor.common.metadata.tasks.TaskDef; 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.HeaderValidatorFilter; +import com.netflix.conductor.rest.rbac.RbacUtils; import com.netflix.conductor.service.MetadataService; import io.swagger.v3.oas.annotations.Operation; @@ -45,10 +50,17 @@ public MetadataResource(MetadataService metadataService) { this.metadataService = metadataService; } + @Autowired HeaderValidatorFilter filter; + @PostMapping("/workflow") @Operation(summary = "Create a new workflow definition") public void create(@RequestBody WorkflowDef workflowDef) { - metadataService.registerWorkflowDef(workflowDef); + + if (filter.getUser().isAdmin()) { + metadataService.registerWorkflowDef(workflowDef); + } else { + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); + } } @PostMapping("/workflow/validate") @@ -60,7 +72,11 @@ public void validate(@RequestBody WorkflowDef workflowDef) { @PutMapping("/workflow") @Operation(summary = "Create or update workflow definition") public BulkResponse update(@RequestBody List workflowDefs) { - return metadataService.updateWorkflowDef(workflowDefs); + + if (filter.getUser().isAdmin()) { + return metadataService.updateWorkflowDef(workflowDefs); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @Operation(summary = "Retrieves workflow definition along with blueprint") @@ -68,13 +84,26 @@ public BulkResponse update(@RequestBody List workflowDefs) { public WorkflowDef get( @PathVariable("name") String name, @RequestParam(value = "version", required = false) Integer version) { - return metadataService.getWorkflowDef(name, version); + + if (filter.getUser().isAdmin()) { + return metadataService.getWorkflowDef(name, version); + } else if (RbacUtils.isUserInGroup( + filter.getUser().getGroupsAndRoles(), + metadataService.getWorkflowDescription(name, version))) { + return metadataService.getWorkflowDef(name, version); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @Operation(summary = "Retrieves all workflow definition along with blueprint") @GetMapping("/workflow") public List getAll() { - return metadataService.getWorkflowDefs(); + + if (filter.getUser().isAdmin()) { + return metadataService.getWorkflowDefs(); + } else { + return metadataService.getUserWorkflowDefs(filter.getUser().getGroupsAndRoles()); + } } @Operation(summary = "Returns workflow names and versions only (no definition bodies)") @@ -95,36 +124,69 @@ public List getAllWorkflowsWithLatestVersions() { "Removes workflow definition. It does not remove workflows associated with the definition.") public void unregisterWorkflowDef( @PathVariable("name") String name, @PathVariable("version") Integer version) { - metadataService.unregisterWorkflowDef(name, version); + + if (filter.getUser().isAdmin()) { + metadataService.unregisterWorkflowDef(name, version); + } else { + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); + } } @PostMapping("/taskdefs") @Operation(summary = "Create new task definition(s)") public void registerTaskDef(@RequestBody List taskDefs) { - metadataService.registerTaskDef(taskDefs); + + if (filter.getUser().isAdmin()) { + metadataService.registerTaskDef(taskDefs); + } else { + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); + } } @PutMapping("/taskdefs") @Operation(summary = "Update an existing task") public void registerTaskDef(@RequestBody TaskDef taskDef) { - metadataService.updateTaskDef(taskDef); + + if (filter.getUser().isAdmin()) { + metadataService.updateTaskDef(taskDef); + } else { + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); + } } @GetMapping(value = "/taskdefs") @Operation(summary = "Gets all task definition") public List getTaskDefs() { - return metadataService.getTaskDefs(); + + if (filter.getUser().isAdmin()) { + return metadataService.getTaskDefs(); + } else { + return metadataService.getUserTaskDefs(filter.getUser().getGroupsAndRoles()); + } } @GetMapping("/taskdefs/{tasktype}") @Operation(summary = "Gets the task definition") public TaskDef getTaskDef(@PathVariable("tasktype") String taskType) { - return metadataService.getTaskDef(taskType); + + if (filter.getUser().isAdmin()) { + return metadataService.getTaskDef(taskType); + } else if (RbacUtils.isUserInGroup( + filter.getUser().getGroupsAndRoles(), + metadataService.getTaskDescription(taskType))) { + return metadataService.getTaskDef(taskType); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @DeleteMapping("/taskdefs/{tasktype}") @Operation(summary = "Remove a task definition") public void unregisterTaskDef(@PathVariable("tasktype") String taskType) { - metadataService.unregisterTaskDef(taskType); + + if (filter.getUser().isAdmin()) { + metadataService.unregisterTaskDef(taskType); + } else { + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); + } } } 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..55a63ebb53 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 @@ -16,6 +16,8 @@ import java.util.Map; import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -24,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpServerErrorException; import com.netflix.conductor.common.metadata.tasks.PollData; import com.netflix.conductor.common.metadata.tasks.Task; @@ -32,6 +35,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.HeaderValidatorFilter; import com.netflix.conductor.service.TaskService; import io.swagger.v3.oas.annotations.Operation; @@ -50,6 +54,8 @@ public TaskResource(TaskService taskService) { this.taskService = taskService; } + @Autowired HeaderValidatorFilter filter; + @GetMapping("/poll/{tasktype}") @Operation(summary = "Poll for a task of a certain type") public ResponseEntity poll( @@ -57,9 +63,13 @@ public ResponseEntity poll( @RequestParam(value = "workerid", required = false) String workerId, @RequestParam(value = "domain", required = false) String domain) { // for backwards compatibility with 2.x client which expects a 204 when no Task is found - return Optional.ofNullable(taskService.poll(taskType, workerId, domain)) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.noContent().build()); + + if (filter.getUser().isAdmin()) { + return Optional.ofNullable(taskService.poll(taskType, workerId, domain)) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.noContent().build()); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @GetMapping("/poll/batch/{tasktype}") @@ -71,10 +81,14 @@ public ResponseEntity> batchPoll( @RequestParam(value = "count", defaultValue = "1") int count, @RequestParam(value = "timeout", defaultValue = "100") int timeout) { // for backwards compatibility with 2.x client which expects a 204 when no Task is found - return Optional.ofNullable( - taskService.batchPoll(taskType, workerId, domain, count, timeout)) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.noContent().build()); + + if (filter.getUser().isAdmin()) { + return Optional.ofNullable( + taskService.batchPoll(taskType, workerId, domain, count, timeout)) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.noContent().build()); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @PostMapping(produces = TEXT_PLAIN_VALUE) @@ -132,7 +146,11 @@ public Map>> allVerbose() { @GetMapping("/queue/all") @Operation(summary = "Get the details about each queue") public Map all() { - return taskService.getAllQueueDetails(); + + if (filter.getUser().isAdmin()) { + return taskService.getAllQueueDetails(); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @GetMapping("/queue/polldata") 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..a943e62d68 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 @@ -14,14 +14,18 @@ import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpServerErrorException; import com.netflix.conductor.common.model.BulkResponse; +import com.netflix.conductor.rest.rbac.HeaderValidatorFilter; import com.netflix.conductor.service.WorkflowBulkService; import io.swagger.v3.oas.annotations.Operation; @@ -39,6 +43,8 @@ public WorkflowBulkResource(WorkflowBulkService workflowBulkService) { this.workflowBulkService = workflowBulkService; } + @Autowired HeaderValidatorFilter filter; + /** * Pause the list of workflows. * @@ -49,7 +55,20 @@ public WorkflowBulkResource(WorkflowBulkService workflowBulkService) { @PutMapping("/pause") @Operation(summary = "Pause the list of workflows") public BulkResponse pauseWorkflow(@RequestBody List workflowIds) { - return workflowBulkService.pauseWorkflow(workflowIds); + + if (filter.getUser().isAdmin()) { + return workflowBulkService.pauseWorkflow(workflowIds); + } else { + List userIds = + workflowBulkService.getUserIds( + filter.getUser().getGroupsAndRoles(), workflowIds); + if (!userIds.isEmpty()) { + return workflowBulkService.pauseWorkflow(userIds); + } else { + throw new HttpServerErrorException( + HttpStatus.UNAUTHORIZED, "Workflows from list cannot be paused by user"); + } + } } /** @@ -62,7 +81,20 @@ public BulkResponse pauseWorkflow(@RequestBody List workflowIds) { @PutMapping("/resume") @Operation(summary = "Resume the list of workflows") public BulkResponse resumeWorkflow(@RequestBody List workflowIds) { - return workflowBulkService.resumeWorkflow(workflowIds); + + if (filter.getUser().isAdmin()) { + return workflowBulkService.resumeWorkflow(workflowIds); + } else { + List userIds = + workflowBulkService.getUserIds( + filter.getUser().getGroupsAndRoles(), workflowIds); + if (!userIds.isEmpty()) { + return workflowBulkService.resumeWorkflow(userIds); + } else { + throw new HttpServerErrorException( + HttpStatus.UNAUTHORIZED, "Workflows from list cannot be resumed by user"); + } + } } /** @@ -79,7 +111,20 @@ public BulkResponse restart( @RequestBody List workflowIds, @RequestParam(value = "useLatestDefinitions", defaultValue = "false", required = false) boolean useLatestDefinitions) { - return workflowBulkService.restart(workflowIds, useLatestDefinitions); + + if (filter.getUser().isAdmin()) { + return workflowBulkService.restart(workflowIds, useLatestDefinitions); + } else { + List userIds = + workflowBulkService.getUserIds( + filter.getUser().getGroupsAndRoles(), workflowIds); + if (!userIds.isEmpty()) { + return workflowBulkService.restart(userIds, useLatestDefinitions); + } else { + throw new HttpServerErrorException( + HttpStatus.UNAUTHORIZED, "Workflows from list cannot be restarted by user"); + } + } } /** @@ -92,7 +137,20 @@ public BulkResponse restart( @PostMapping("/retry") @Operation(summary = "Retry the last failed task for each workflow from the list") public BulkResponse retry(@RequestBody List workflowIds) { - return workflowBulkService.retry(workflowIds); + + if (filter.getUser().isAdmin()) { + return workflowBulkService.retry(workflowIds); + } else { + List userIds = + workflowBulkService.getUserIds( + filter.getUser().getGroupsAndRoles(), workflowIds); + if (!userIds.isEmpty()) { + return workflowBulkService.retry(userIds); + } else { + throw new HttpServerErrorException( + HttpStatus.UNAUTHORIZED, "Workflows from list cannot be retried by user"); + } + } } /** @@ -109,6 +167,20 @@ public BulkResponse retry(@RequestBody List workflowIds) { public BulkResponse terminate( @RequestBody List workflowIds, @RequestParam(value = "reason", required = false) String reason) { - return workflowBulkService.terminate(workflowIds, reason); + + if (filter.getUser().isAdmin()) { + return workflowBulkService.terminate(workflowIds, reason); + } else { + List userIds = + workflowBulkService.getUserIds( + filter.getUser().getGroupsAndRoles(), workflowIds); + if (!userIds.isEmpty()) { + return workflowBulkService.terminate(userIds, reason); + } else { + throw new HttpServerErrorException( + HttpStatus.UNAUTHORIZED, + "Workflows from list cannot be terminated by user"); + } + } } } 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..c8d7f77e71 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 @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -26,11 +27,14 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpServerErrorException; import com.netflix.conductor.common.metadata.workflow.RerunWorkflowRequest; 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.HeaderValidatorFilter; +import com.netflix.conductor.rest.rbac.RbacUtils; import com.netflix.conductor.service.WorkflowService; import com.netflix.conductor.service.WorkflowTestService; @@ -55,12 +59,22 @@ public WorkflowResource( this.workflowTestService = workflowTestService; } + @Autowired HeaderValidatorFilter filter; + @PostMapping(produces = TEXT_PLAIN_VALUE) @Operation( summary = "Start a new workflow with StartWorkflowRequest, which allows task to be executed in a domain") public String startWorkflow(@RequestBody StartWorkflowRequest request) { - return workflowService.startWorkflow(request); + + if (filter.getUser().isAdmin()) { + return workflowService.startWorkflow(request); + } else if (RbacUtils.isUserInGroup( + filter.getUser().getGroupsAndRoles(), + workflowService.getWorkflowDescription(request.getName(), request.getVersion()))) { + return workflowService.startWorkflow(request); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @PostMapping(value = "/{name}", produces = TEXT_PLAIN_VALUE) @@ -106,7 +120,14 @@ public Workflow getExecutionStatus( @PathVariable("workflowId") String workflowId, @RequestParam(value = "includeTasks", defaultValue = "true", required = false) boolean includeTasks) { - return workflowService.getExecutionStatus(workflowId, includeTasks); + + if (filter.getUser().isAdmin()) { + return workflowService.getExecutionStatus(workflowId, includeTasks); + } else if (RbacUtils.isUserInGroup( + filter.getUser().getGroupsAndRoles(), workflowService.getLabels(workflowId))) { + return workflowService.getExecutionStatus(workflowId, includeTasks); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @GetMapping("/family/{workflowId}") @@ -115,7 +136,14 @@ public List getWorkflowFamily( @PathVariable("workflowId") String workflowId, @RequestParam(value = "summaryOnly", defaultValue = "true", required = false) boolean summaryOnly) { - return workflowService.getWorkflowFamily(workflowId, summaryOnly); + + if (filter.getUser().isAdmin()) { + return workflowService.getWorkflowFamily(workflowId, summaryOnly); + } else if (RbacUtils.isUserInGroup( + filter.getUser().getGroupsAndRoles(), workflowService.getLabels(workflowId))) { + return workflowService.getWorkflowFamily(workflowId, summaryOnly); + } + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); } @GetMapping("/path/{workflowId}") @@ -234,7 +262,12 @@ public SearchResult search( @RequestParam(value = "sort", required = false) String sort, @RequestParam(value = "freeText", defaultValue = "*", required = false) String freeText, @RequestParam(value = "query", required = false) String query) { - return workflowService.searchWorkflows(start, size, sort, freeText, query); + + if (filter.getUser().isAdmin()) { + return workflowService.searchWorkflows(start, size, sort, freeText, query); + } else { + return workflowService.getUserSummaries(filter.getUser().getGroupsAndRoles()); + } } @Operation( diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/HeaderValidatorFilter.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/HeaderValidatorFilter.java new file mode 100644 index 0000000000..1624234e00 --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/HeaderValidatorFilter.java @@ -0,0 +1,115 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +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.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpServerErrorException; + +@Component +@EnableConfigurationProperties(RbacProperties.class) +public class HeaderValidatorFilter implements Filter { + + private UserType user; + + private final RbacProperties properties; + + @Autowired + public HeaderValidatorFilter(RbacProperties properties) { + this.properties = properties; + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + if (validateHeaders(Collections.list(request.getHeaderNames()))) { + user = createUser(getAdminRolesAndGroups(), getRolesAndGroupsList(request)); + filterChain.doFilter(servletRequest, servletResponse); + } else { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private boolean validateHeaders(List headers) { + return headers.stream().anyMatch("from"::equals); + } + + private UserType createUser(List adminRolesAndGroups, List rolesAndGroups) { + if (adminRolesAndGroups.stream().anyMatch(rolesAndGroups::contains)) { + return new UserType(adminRolesAndGroups, true); + } else { + if (!rolesAndGroups.isEmpty()) { + return new UserType(rolesAndGroups, false); + } else { + throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED); + } + } + } + + private List getRolesAndGroupsList(HttpServletRequest request) { + return Stream.of( + getHeadersList(request.getHeader("x-auth-user-roles")), + getHeadersList(request.getHeader("x-auth-user-groups"))) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + private List getHeadersList(String request) { + if (request != null && !request.isEmpty()) { + return Arrays.stream(request.split(",\\s*")) + .map(String::trim) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + private List getAdminRolesAndGroups() { + String[] adminRoles = Optional.ofNullable(properties.getAdminRoles()).orElse(new String[0]); + String[] adminGroups = + Optional.ofNullable(properties.getAdminGroups()).orElse(new String[0]); + + return Stream.concat(Arrays.stream(adminRoles), Arrays.stream(adminGroups)) + .collect(Collectors.toList()); + } + + public UserType getUser() { + return user; + } + + public void setUser(UserType user) { + this.user = user; + } +} 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..e355bf2308 --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacProperties.java @@ -0,0 +1,46 @@ +/* + * 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 org.springframework.beans.factory.annotation.Value; +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 { + + @Value("${conductor.rbac.admin.roles}") + private String[] adminRoles; + + @Value("${conductor.rbac.admin.groups}") + private String[] adminGroups; + + public String[] getAdminRoles() { + return adminRoles; + } + + public String[] getAdminGroups() { + return adminGroups; + } + + public void setAdminRoles(String[] adminRoles) { + this.adminRoles = adminRoles; + } + + public void setAdminGroups(String[] adminGroups) { + this.adminGroups = adminGroups; + } +} diff --git a/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacUtils.java b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacUtils.java new file mode 100644 index 0000000000..36eb57414f --- /dev/null +++ b/rest/src/main/java/com/netflix/conductor/rest/rbac/RbacUtils.java @@ -0,0 +1,23 @@ +/* + * 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 RbacUtils { + + public static boolean isUserInGroup(List rolesAndGroups, List labels) { + return rolesAndGroups.stream() + .anyMatch(element -> labels.stream().anyMatch(element::equalsIgnoreCase)); + } +} 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..deeedeb11b --- /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 groupsAndRoles; + private boolean isAdmin; + + public UserType(List groupsAndRoles, boolean isAdmin) { + this.groupsAndRoles = groupsAndRoles; + this.isAdmin = isAdmin; + } + + public List getGroupsAndRoles() { + return groupsAndRoles; + } + + public boolean isAdmin() { + return isAdmin; + } +} diff --git a/rest/src/test/java/com/netflix/conductor/rest/controllers/EventResourceTest.java b/rest/src/test/java/com/netflix/conductor/rest/controllers/EventResourceTest.java index ae1adffb60..ead1913f93 100644 --- a/rest/src/test/java/com/netflix/conductor/rest/controllers/EventResourceTest.java +++ b/rest/src/test/java/com/netflix/conductor/rest/controllers/EventResourceTest.java @@ -20,6 +20,9 @@ import org.mockito.Mock; import com.netflix.conductor.common.metadata.events.EventHandler; +import com.netflix.conductor.rest.rbac.HeaderValidatorFilter; +import com.netflix.conductor.rest.rbac.RbacProperties; +import com.netflix.conductor.rest.rbac.UserType; import com.netflix.conductor.service.EventService; import static org.junit.Assert.assertEquals; @@ -37,10 +40,15 @@ public class EventResourceTest { @Mock private EventService mockEventService; + @Mock RbacProperties properties; + @Before public void setUp() { this.mockEventService = mock(EventService.class); this.eventResource = new EventResource(this.mockEventService); + this.properties = mock(RbacProperties.class); + this.eventResource.filter = new HeaderValidatorFilter(properties); + this.eventResource.filter.setUser(new UserType(new ArrayList<>(List.of("admin")), true)); } @Test diff --git a/rest/src/test/java/com/netflix/conductor/rest/controllers/MetadataResourceTest.java b/rest/src/test/java/com/netflix/conductor/rest/controllers/MetadataResourceTest.java index 36d5b6e70c..5ce1914915 100644 --- a/rest/src/test/java/com/netflix/conductor/rest/controllers/MetadataResourceTest.java +++ b/rest/src/test/java/com/netflix/conductor/rest/controllers/MetadataResourceTest.java @@ -17,9 +17,13 @@ import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; +import com.netflix.conductor.rest.rbac.HeaderValidatorFilter; +import com.netflix.conductor.rest.rbac.RbacProperties; +import com.netflix.conductor.rest.rbac.UserType; import com.netflix.conductor.service.MetadataService; import static org.junit.Assert.assertEquals; @@ -37,10 +41,15 @@ public class MetadataResourceTest { private MetadataService mockMetadataService; + @Mock RbacProperties properties; + @Before public void before() { this.mockMetadataService = mock(MetadataService.class); this.metadataResource = new MetadataResource(this.mockMetadataService); + this.properties = mock(RbacProperties.class); + this.metadataResource.filter = new HeaderValidatorFilter(properties); + this.metadataResource.filter.setUser(new UserType(new ArrayList<>(List.of("admin")), true)); } @Test diff --git a/rest/src/test/java/com/netflix/conductor/rest/controllers/TaskResourceTest.java b/rest/src/test/java/com/netflix/conductor/rest/controllers/TaskResourceTest.java index b9dd18d233..cc673a1383 100644 --- a/rest/src/test/java/com/netflix/conductor/rest/controllers/TaskResourceTest.java +++ b/rest/src/test/java/com/netflix/conductor/rest/controllers/TaskResourceTest.java @@ -20,6 +20,7 @@ import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; import org.springframework.http.ResponseEntity; import com.netflix.conductor.common.metadata.tasks.PollData; @@ -29,6 +30,9 @@ 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.HeaderValidatorFilter; +import com.netflix.conductor.rest.rbac.RbacProperties; +import com.netflix.conductor.rest.rbac.UserType; import com.netflix.conductor.service.TaskService; import static org.junit.Assert.assertEquals; @@ -48,10 +52,15 @@ public class TaskResourceTest { private TaskResource taskResource; + @Mock RbacProperties properties; + @Before public void before() { this.mockTaskService = mock(TaskService.class); this.taskResource = new TaskResource(this.mockTaskService); + this.properties = mock(RbacProperties.class); + this.taskResource.filter = new HeaderValidatorFilter(properties); + this.taskResource.filter.setUser(new UserType(new ArrayList<>(List.of("admin")), true)); } @Test diff --git a/rest/src/test/java/com/netflix/conductor/rest/controllers/WorkflowResourceTest.java b/rest/src/test/java/com/netflix/conductor/rest/controllers/WorkflowResourceTest.java index 2667338a38..e157d5a311 100644 --- a/rest/src/test/java/com/netflix/conductor/rest/controllers/WorkflowResourceTest.java +++ b/rest/src/test/java/com/netflix/conductor/rest/controllers/WorkflowResourceTest.java @@ -25,6 +25,9 @@ import com.netflix.conductor.common.metadata.workflow.SkipTaskRequest; import com.netflix.conductor.common.metadata.workflow.StartWorkflowRequest; import com.netflix.conductor.common.run.Workflow; +import com.netflix.conductor.rest.rbac.HeaderValidatorFilter; +import com.netflix.conductor.rest.rbac.RbacProperties; +import com.netflix.conductor.rest.rbac.UserType; import com.netflix.conductor.service.WorkflowService; import com.netflix.conductor.service.WorkflowTestService; @@ -49,12 +52,17 @@ public class WorkflowResourceTest { private WorkflowResource workflowResource; + @Mock RbacProperties properties; + @Before public void before() { this.mockWorkflowService = mock(WorkflowService.class); this.mockWorkflowTestService = mock(WorkflowTestService.class); this.workflowResource = new WorkflowResource(this.mockWorkflowService, this.mockWorkflowTestService); + this.properties = mock(RbacProperties.class); + this.workflowResource.filter = new HeaderValidatorFilter(properties); + this.workflowResource.filter.setUser(new UserType(new ArrayList<>(List.of("admin")), true)); } @Test diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 802f1cc18b..18ec89a5ba 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.roles=network-admin, owner, admin +conductor.rbac.admin.groups=group-admin, group-owner \ No newline at end of file diff --git a/server/src/test/resources/application.properties b/server/src/test/resources/application.properties index e002b55e23..83092158b1 100644 --- a/server/src/test/resources/application.properties +++ b/server/src/test/resources/application.properties @@ -124,3 +124,6 @@ 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:} + +conductor.rbac.admin.roles=network-admin, owner, admin +conductor.rbac.admin.groups=group-admin, group-owner 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..515f050096 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.HeaderValidatorFilter +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 + HeaderValidatorFilter 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(new ArrayList(List.of("admin")), true)) } /// Workflow Resource endpoints diff --git a/test-harness/src/test/resources/application-integrationtest.properties b/test-harness/src/test/resources/application-integrationtest.properties index 3c93ecadb4..0571754aaa 100644 --- a/test-harness/src/test/resources/application-integrationtest.properties +++ b/test-harness/src/test/resources/application-integrationtest.properties @@ -53,3 +53,6 @@ conductor.elasticsearch.index-prefix=conductor conductor.elasticsearch.cluster-health-color=yellow management.metrics.export.datadog.enabled=false + +conductor.rbac.admin.roles=network-admin, owner, admin +conductor.rbac.admin.groups=group-admin, group-owner