diff --git a/api/http/handler/resource_control.go b/api/http/handler/resource_control.go
index d84824cb5..623e595e9 100644
--- a/api/http/handler/resource_control.go
+++ b/api/http/handler/resource_control.go
@@ -80,6 +80,8 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht
resourceControlType = portainer.VolumeResourceControl
case "network":
resourceControlType = portainer.NetworkResourceControl
+ case "secret":
+ resourceControlType = portainer.SecretResourceControl
default:
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
return
diff --git a/api/http/proxy/decorator.go b/api/http/proxy/decorator.go
index 7881f697a..ff075cc69 100644
--- a/api/http/proxy/decorator.go
+++ b/api/http/proxy/decorator.go
@@ -106,6 +106,30 @@ func decorateNetworkList(networkData []interface{}, resourceControls []portainer
return decoratedNetworkData, nil
}
+// decorateSecretList loops through all secrets and will decorate any secret with an existing resource control.
+// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
+func decorateSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
+ decoratedSecretData := make([]interface{}, 0)
+
+ for _, secret := range secretData {
+
+ secretObject := secret.(map[string]interface{})
+ if secretObject[secretIdentifier] == nil {
+ return nil, ErrDockerSecretIdentifierNotFound
+ }
+
+ secretID := secretObject[secretIdentifier].(string)
+ resourceControl := getResourceControlByResourceID(secretID, resourceControls)
+ if resourceControl != nil {
+ secretObject = decorateObject(secretObject, resourceControl)
+ }
+
+ decoratedSecretData = append(decoratedSecretData, secretObject)
+ }
+
+ return decoratedSecretData, nil
+}
+
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
metadata := make(map[string]interface{})
metadata["ResourceControl"] = resourceControl
diff --git a/api/http/proxy/filter.go b/api/http/proxy/filter.go
index 3f555f0be..bc72987d0 100644
--- a/api/http/proxy/filter.go
+++ b/api/http/proxy/filter.go
@@ -135,3 +135,28 @@ func filterNetworkList(networkData []interface{}, resourceControls []portainer.R
return filteredNetworkData, nil
}
+
+// filterSecretList loops through all secrets, filters secrets without any resource control (public resources) or with
+// any resource control giving access to the user (these secrets will be decorated).
+// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
+func filterSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
+ filteredSecretData := make([]interface{}, 0)
+
+ for _, secret := range secretData {
+ secretObject := secret.(map[string]interface{})
+ if secretObject[secretIdentifier] == nil {
+ return nil, ErrDockerSecretIdentifierNotFound
+ }
+
+ secretID := secretObject[secretIdentifier].(string)
+ resourceControl := getResourceControlByResourceID(secretID, resourceControls)
+ if resourceControl == nil {
+ filteredSecretData = append(filteredSecretData, secretObject)
+ } else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
+ secretObject = decorateObject(secretObject, resourceControl)
+ filteredSecretData = append(filteredSecretData, secretObject)
+ }
+ }
+
+ return filteredSecretData, nil
+}
diff --git a/api/http/proxy/secrets.go b/api/http/proxy/secrets.go
new file mode 100644
index 000000000..d0001d3b9
--- /dev/null
+++ b/api/http/proxy/secrets.go
@@ -0,0 +1,67 @@
+package proxy
+
+import (
+ "net/http"
+
+ "github.com/portainer/portainer"
+)
+
+const (
+ // ErrDockerSecretIdentifierNotFound defines an error raised when Portainer is unable to find a secret identifier
+ ErrDockerSecretIdentifierNotFound = portainer.Error("Docker secret identifier not found")
+ secretIdentifier = "ID"
+)
+
+// secretListOperation extracts the response as a JSON object, loop through the secrets array
+// decorate and/or filter the secrets based on resource controls before rewriting the response
+func secretListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
+ var err error
+
+ // SecretList response is a JSON array
+ // https://docs.docker.com/engine/api/v1.28/#operation/SecretList
+ responseArray, err := getResponseAsJSONArray(response)
+ if err != nil {
+ return err
+ }
+
+ if executor.operationContext.isAdmin {
+ responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
+ } else {
+ responseArray, err = filterSecretList(responseArray, executor.operationContext.resourceControls,
+ executor.operationContext.userID, executor.operationContext.userTeamIDs)
+ }
+ if err != nil {
+ return err
+ }
+
+ return rewriteResponse(response, responseArray, http.StatusOK)
+}
+
+// secretInspectOperation extracts the response as a JSON object, verify that the user
+// has access to the secret based on resource control (check are done based on the secretID and optional Swarm service ID)
+// and either rewrite an access denied response or a decorated secret.
+func secretInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
+ // SecretInspect response is a JSON object
+ // https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
+ responseObject, err := getResponseAsJSONOBject(response)
+ if err != nil {
+ return err
+ }
+
+ if responseObject[secretIdentifier] == nil {
+ return ErrDockerSecretIdentifierNotFound
+ }
+ secretID := responseObject[secretIdentifier].(string)
+
+ resourceControl := getResourceControlByResourceID(secretID, executor.operationContext.resourceControls)
+ if resourceControl != nil {
+ if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
+ executor.operationContext.userTeamIDs, resourceControl) {
+ responseObject = decorateObject(responseObject, resourceControl)
+ } else {
+ return rewriteAccessDeniedResponse(response)
+ }
+ }
+
+ return rewriteResponse(response, responseObject, http.StatusOK)
+}
diff --git a/api/http/proxy/transport.go b/api/http/proxy/transport.go
index 09f6418b2..35ee6cf2d 100644
--- a/api/http/proxy/transport.go
+++ b/api/http/proxy/transport.go
@@ -62,6 +62,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
return p.proxyVolumeRequest(request)
case strings.HasPrefix(path, "/networks"):
return p.proxyNetworkRequest(request)
+ case strings.HasPrefix(path, "/secrets"):
+ return p.proxySecretRequest(request)
case strings.HasPrefix(path, "/swarm"):
return p.proxySwarmRequest(request)
default:
@@ -157,12 +159,30 @@ func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Respo
return p.rewriteOperation(request, networkListOperation)
default:
- // assume /networks/{name}
+ // assume /networks/{id}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, networkInspectOperation)
}
- volumeID := path.Base(requestPath)
- return p.restrictedOperation(request, volumeID)
+ networkID := path.Base(requestPath)
+ return p.restrictedOperation(request, networkID)
+ }
+}
+
+func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) {
+ switch requestPath := request.URL.Path; requestPath {
+ case "/secrets/create":
+ return p.executeDockerRequest(request)
+
+ case "/secrets":
+ return p.rewriteOperation(request, secretListOperation)
+
+ default:
+ // assume /secrets/{id}
+ if request.Method == http.MethodGet {
+ return p.rewriteOperation(request, secretInspectOperation)
+ }
+ secretID := path.Base(requestPath)
+ return p.restrictedOperation(request, secretID)
}
}
diff --git a/api/portainer.go b/api/portainer.go
index 024f8998a..78c303e37 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -401,4 +401,6 @@ const (
VolumeResourceControl
// NetworkResourceControl represents a resource control associated to a Docker network
NetworkResourceControl
+ // SecretResourceControl represents a resource control associated to a Docker secret
+ SecretResourceControl
)
diff --git a/app/components/createSecret/createSecretController.js b/app/components/createSecret/createSecretController.js
index 3f2533270..ce94e676b 100644
--- a/app/components/createSecret/createSecretController.js
+++ b/app/components/createSecret/createSecretController.js
@@ -1,11 +1,17 @@
angular.module('createSecret', [])
-.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper',
-function ($scope, $state, Notifications, SecretService, LabelHelper) {
+.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
+function ($scope, $state, Notifications, SecretService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
+
$scope.formValues = {
Name: '',
Data: '',
Labels: [],
- encodeSecret: true
+ encodeSecret: true,
+ AccessControlData: new AccessControlFormData()
+ };
+
+ $scope.state = {
+ formValidationError: ''
};
$scope.addLabel = function() {
@@ -36,10 +42,38 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
return config;
}
- function createSecret(config) {
- $('#createSecretSpinner').show();
- SecretService.create(config)
+ function validateForm(accessControlData, isAdmin) {
+ $scope.state.formValidationError = '';
+ var error = '';
+ error = FormValidator.validateAccessControl(accessControlData, isAdmin);
+
+ if (error) {
+ $scope.state.formValidationError = error;
+ return false;
+ }
+ return true;
+ }
+
+ $scope.create = function () {
+ $('#createResourceSpinner').show();
+
+ var accessControlData = $scope.formValues.AccessControlData;
+ var userDetails = Authentication.getUserDetails();
+ var isAdmin = userDetails.role === 1 ? true : false;
+
+ if (!validateForm(accessControlData, isAdmin)) {
+ $('#createResourceSpinner').hide();
+ return;
+ }
+
+ var secretConfiguration = prepareConfiguration();
+ SecretService.create(secretConfiguration)
.then(function success(data) {
+ var secretIdentifier = data.ID;
+ var userId = userDetails.ID;
+ return ResourceControlService.applyResourceControl('secret', secretIdentifier, userId, accessControlData, []);
+ })
+ .then(function success() {
Notifications.success('Secret successfully created');
$state.go('secrets', {}, {reload: true});
})
@@ -47,12 +81,7 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
Notifications.error('Failure', err, 'Unable to create secret');
})
.finally(function final() {
- $('#createSecretSpinner').hide();
+ $('#createResourceSpinner').hide();
});
- }
-
- $scope.create = function () {
- var config = prepareConfiguration();
- createSecret(config);
};
}]);
diff --git a/app/components/createSecret/createsecret.html b/app/components/createSecret/createsecret.html
index c918e8cd5..dbf4efe06 100644
--- a/app/components/createSecret/createsecret.html
+++ b/app/components/createSecret/createsecret.html
@@ -66,6 +66,9 @@
+
+