diff --git a/api/http/handler/resource_control.go b/api/http/handler/resource_control.go
index 7c35dec39..d84824cb5 100644
--- a/api/http/handler/resource_control.go
+++ b/api/http/handler/resource_control.go
@@ -78,6 +78,8 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht
resourceControlType = portainer.ServiceResourceControl
case "volume":
resourceControlType = portainer.VolumeResourceControl
+ case "network":
+ resourceControlType = portainer.NetworkResourceControl
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 cc35fa7a3..7881f697a 100644
--- a/api/http/proxy/decorator.go
+++ b/api/http/proxy/decorator.go
@@ -82,6 +82,30 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer
return decoratedServiceData, nil
}
+// decorateNetworkList loops through all networks and will decorate any network with an existing resource control.
+// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
+func decorateNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
+ decoratedNetworkData := make([]interface{}, 0)
+
+ for _, network := range networkData {
+
+ networkObject := network.(map[string]interface{})
+ if networkObject[networkIdentifier] == nil {
+ return nil, ErrDockerNetworkIdentifierNotFound
+ }
+
+ networkID := networkObject[networkIdentifier].(string)
+ resourceControl := getResourceControlByResourceID(networkID, resourceControls)
+ if resourceControl != nil {
+ networkObject = decorateObject(networkObject, resourceControl)
+ }
+
+ decoratedNetworkData = append(decoratedNetworkData, networkObject)
+ }
+
+ return decoratedNetworkData, 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 0e66ab4fd..3f555f0be 100644
--- a/api/http/proxy/filter.go
+++ b/api/http/proxy/filter.go
@@ -110,3 +110,28 @@ func filterServiceList(serviceData []interface{}, resourceControls []portainer.R
return filteredServiceData, nil
}
+
+// filterNetworkList loops through all networks, filters networks without any resource control (public resources) or with
+// any resource control giving access to the user (these networks will be decorated).
+// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
+func filterNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
+ filteredNetworkData := make([]interface{}, 0)
+
+ for _, network := range networkData {
+ networkObject := network.(map[string]interface{})
+ if networkObject[networkIdentifier] == nil {
+ return nil, ErrDockerNetworkIdentifierNotFound
+ }
+
+ networkID := networkObject[networkIdentifier].(string)
+ resourceControl := getResourceControlByResourceID(networkID, resourceControls)
+ if resourceControl == nil {
+ filteredNetworkData = append(filteredNetworkData, networkObject)
+ } else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
+ networkObject = decorateObject(networkObject, resourceControl)
+ filteredNetworkData = append(filteredNetworkData, networkObject)
+ }
+ }
+
+ return filteredNetworkData, nil
+}
diff --git a/api/http/proxy/networks.go b/api/http/proxy/networks.go
new file mode 100644
index 000000000..2e2549408
--- /dev/null
+++ b/api/http/proxy/networks.go
@@ -0,0 +1,66 @@
+package proxy
+
+import (
+ "net/http"
+
+ "github.com/portainer/portainer"
+)
+
+const (
+ // ErrDockerNetworkIdentifierNotFound defines an error raised when Portainer is unable to find a network identifier
+ ErrDockerNetworkIdentifierNotFound = portainer.Error("Docker network identifier not found")
+ networkIdentifier = "Id"
+)
+
+// networkListOperation extracts the response as a JSON object, loop through the networks array
+// decorate and/or filter the networks based on resource controls before rewriting the response
+func networkListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
+ var err error
+ // NetworkList response is a JSON array
+ // https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
+ responseArray, err := getResponseAsJSONArray(response)
+ if err != nil {
+ return err
+ }
+
+ if executor.operationContext.isAdmin {
+ responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls)
+ } else {
+ responseArray, err = filterNetworkList(responseArray, executor.operationContext.resourceControls,
+ executor.operationContext.userID, executor.operationContext.userTeamIDs)
+ }
+ if err != nil {
+ return err
+ }
+
+ return rewriteResponse(response, responseArray, http.StatusOK)
+}
+
+// networkInspectOperation extracts the response as a JSON object, verify that the user
+// has access to the network based on resource control and either rewrite an access denied response
+// or a decorated network.
+func networkInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
+ // NetworkInspect response is a JSON object
+ // https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
+ responseObject, err := getResponseAsJSONOBject(response)
+ if err != nil {
+ return err
+ }
+
+ if responseObject[networkIdentifier] == nil {
+ return ErrDockerNetworkIdentifierNotFound
+ }
+ networkID := responseObject[networkIdentifier].(string)
+
+ resourceControl := getResourceControlByResourceID(networkID, 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 76c4f43f7..09f6418b2 100644
--- a/api/http/proxy/transport.go
+++ b/api/http/proxy/transport.go
@@ -53,17 +53,20 @@ func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Resp
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
path := request.URL.Path
- if strings.HasPrefix(path, "/containers") {
+ switch {
+ case strings.HasPrefix(path, "/containers"):
return p.proxyContainerRequest(request)
- } else if strings.HasPrefix(path, "/services") {
+ case strings.HasPrefix(path, "/services"):
return p.proxyServiceRequest(request)
- } else if strings.HasPrefix(path, "/volumes") {
+ case strings.HasPrefix(path, "/volumes"):
return p.proxyVolumeRequest(request)
- } else if strings.HasPrefix(path, "/swarm") {
+ case strings.HasPrefix(path, "/networks"):
+ return p.proxyNetworkRequest(request)
+ case strings.HasPrefix(path, "/swarm"):
return p.proxySwarmRequest(request)
+ default:
+ return p.executeDockerRequest(request)
}
-
- return p.executeDockerRequest(request)
}
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
@@ -145,6 +148,24 @@ func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Respon
}
}
+func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
+ switch requestPath := request.URL.Path; requestPath {
+ case "/networks/create":
+ return p.executeDockerRequest(request)
+
+ case "/networks":
+ return p.rewriteOperation(request, networkListOperation)
+
+ default:
+ // assume /networks/{name}
+ if request.Method == http.MethodGet {
+ return p.rewriteOperation(request, networkInspectOperation)
+ }
+ volumeID := path.Base(requestPath)
+ return p.restrictedOperation(request, volumeID)
+ }
+}
+
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
return p.administratorOperation(request)
}
diff --git a/api/portainer.go b/api/portainer.go
index 13d8b82e1..024f8998a 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -399,4 +399,6 @@ const (
ServiceResourceControl
// VolumeResourceControl represents a resource control associated to a Docker volume
VolumeResourceControl
+ // NetworkResourceControl represents a resource control associated to a Docker network
+ NetworkResourceControl
)
diff --git a/app/components/createNetwork/createNetworkController.js b/app/components/createNetwork/createNetworkController.js
index ee0a1e838..dc804b7a3 100644
--- a/app/components/createNetwork/createNetworkController.js
+++ b/app/components/createNetwork/createNetworkController.js
@@ -1,12 +1,17 @@
angular.module('createNetwork', [])
-.controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'Network', 'LabelHelper',
-function ($q, $scope, $state, PluginService, Notifications, Network, LabelHelper) {
+.controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
+function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
Gateway: '',
- Labels: []
+ Labels: [],
+ AccessControlData: new AccessControlFormData()
+ };
+
+ $scope.state = {
+ formValidationError: ''
};
$scope.availableNetworkDrivers = [];
@@ -40,23 +45,6 @@ function ($q, $scope, $state, PluginService, Notifications, Network, LabelHelper
$scope.formValues.Labels.splice(index, 1);
};
- function createNetwork(config) {
- $('#createNetworkSpinner').show();
- Network.create(config, function (d) {
- if (d.message) {
- $('#createNetworkSpinner').hide();
- Notifications.error('Unable to create network', {}, d.message);
- } else {
- Notifications.success('Network created', d.Id);
- $('#createNetworkSpinner').hide();
- $state.go('networks', {}, {reload: true});
- }
- }, function (e) {
- $('#createNetworkSpinner').hide();
- Notifications.error('Failure', e, 'Unable to create network');
- });
- }
-
function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) {
var ipamConfig = {};
@@ -88,9 +76,47 @@ function ($q, $scope, $state, PluginService, Notifications, Network, LabelHelper
return 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 () {
- var config = prepareConfiguration();
- createNetwork(config);
+ $('#createResourceSpinner').show();
+
+ var networkConfiguration = prepareConfiguration();
+ var accessControlData = $scope.formValues.AccessControlData;
+ var userDetails = Authentication.getUserDetails();
+ var isAdmin = userDetails.role === 1 ? true : false;
+
+ if (!validateForm(accessControlData, isAdmin)) {
+ $('#createResourceSpinner').hide();
+ return;
+ }
+
+ NetworkService.create(networkConfiguration)
+ .then(function success(data) {
+ var networkIdentifier = data.Id;
+ var userId = userDetails.ID;
+ return ResourceControlService.applyResourceControl('network', networkIdentifier, userId, accessControlData, []);
+ })
+ .then(function success() {
+ Notifications.success('Network successfully created');
+ $state.go('networks', {}, {reload: true});
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'An error occured during network creation');
+ })
+ .finally(function final() {
+ $('#createResourceSpinner').hide();
+ });
};
function initView() {
diff --git a/app/components/createNetwork/createnetwork.html b/app/components/createNetwork/createnetwork.html
index c01da87e2..4d5636a10 100644
--- a/app/components/createNetwork/createnetwork.html
+++ b/app/components/createNetwork/createnetwork.html
@@ -121,6 +121,9 @@
+
+