diff --git a/api/http/proxy/filter.go b/api/http/proxy/filter.go index bc72987d0..005b11469 100644 --- a/api/http/proxy/filter.go +++ b/api/http/proxy/filter.go @@ -160,3 +160,26 @@ func filterSecretList(secretData []interface{}, resourceControls []portainer.Res return filteredSecretData, nil } + +// filterTaskList loops through all tasks, filters tasks without any resource control (public resources) or with +// any resource control giving access to the user based on the associated service identifier. +// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList +func filterTaskList(taskData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) { + filteredTaskData := make([]interface{}, 0) + + for _, task := range taskData { + taskObject := task.(map[string]interface{}) + if taskObject[taskServiceIdentifier] == nil { + return nil, ErrDockerTaskServiceIdentifierNotFound + } + + serviceID := taskObject[taskServiceIdentifier].(string) + + resourceControl := getResourceControlByResourceID(serviceID, resourceControls) + if resourceControl == nil || (resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl)) { + filteredTaskData = append(filteredTaskData, taskObject) + } + } + + return filteredTaskData, nil +} diff --git a/api/http/proxy/tasks.go b/api/http/proxy/tasks.go new file mode 100644 index 000000000..b9073458b --- /dev/null +++ b/api/http/proxy/tasks.go @@ -0,0 +1,36 @@ +package proxy + +import ( + "net/http" + + "github.com/portainer/portainer" +) + +const ( + // ErrDockerTaskServiceIdentifierNotFound defines an error raised when Portainer is unable to find the service identifier associated to a task + ErrDockerTaskServiceIdentifierNotFound = portainer.Error("Docker task service identifier not found") + taskServiceIdentifier = "ServiceID" +) + +// taskListOperation extracts the response as a JSON object, loop through the tasks array +// and filter the tasks based on resource controls before rewriting the response +func taskListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error { + var err error + + // TaskList response is a JSON array + // https://docs.docker.com/engine/api/v1.28/#operation/TaskList + responseArray, err := getResponseAsJSONArray(response) + if err != nil { + return err + } + + if !executor.operationContext.isAdmin { + responseArray, err = filterTaskList(responseArray, executor.operationContext.resourceControls, + executor.operationContext.userID, executor.operationContext.userTeamIDs) + if err != nil { + return err + } + } + + return rewriteResponse(response, responseArray, http.StatusOK) +} diff --git a/api/http/proxy/transport.go b/api/http/proxy/transport.go index 11d98c3d6..83f746d0e 100644 --- a/api/http/proxy/transport.go +++ b/api/http/proxy/transport.go @@ -68,6 +68,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon return p.proxySwarmRequest(request) case strings.HasPrefix(path, "/nodes"): return p.proxyNodeRequest(request) + case strings.HasPrefix(path, "/tasks"): + return p.proxyTaskRequest(request) default: return p.executeDockerRequest(request) } @@ -203,6 +205,16 @@ func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Respons return p.administratorOperation(request) } +func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/tasks": + return p.rewriteOperation(request, taskListOperation) + default: + // assume /tasks/{id} + return p.executeDockerRequest(request) + } +} + // restrictedOperation ensures that the current user has the required authorizations // before executing the original request. func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {