From 1a28e1091cd4c150faebb25c08077f016046d8a9 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 16 Aug 2017 10:15:58 +0200 Subject: [PATCH 01/33] docs(api): update swagger.yml (#1130) --- api/swagger.yaml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/api/swagger.yaml b/api/swagger.yaml index aa7713195..28e0dc3f1 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -3,7 +3,8 @@ swagger: "2.0" info: description: "Portainer API is an HTTP API served by Portainer. It is used by the\ \ Portainer UI and everything you can do with the UI can be done using the HTTP\ - \ API.\nYou can find out more about Portainer at [http://portainer.io](http://portainer.io)\ + \ API. \nExamples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8\ + \ \nYou can find out more about Portainer at [http://portainer.io](http://portainer.io)\ \ and get some support on [Slack](http://portainer.io/slack/).\n\n# Authentication\n\ \nMost of the API endpoints require to be authenticated as well as some level\ \ of authorization to be used.\nPortainer API uses JSON Web Token to manage authentication\ @@ -20,7 +21,18 @@ info: \ this access policy.\nExtra-checks might be added to ensure access to the resource\ \ is granted. Returned data might also be filtered.\n\n### Administrator access\n\ \nAuthentication as well as an administrator role are required to access the endpoints\ - \ with this access policy.\n" + \ with this access policy.\n\n# Execute Docker requests\n\nPortainer **DO NOT**\ + \ expose specific endpoints to manage your Docker resources (create a container,\ + \ remove a volume, etc...). \n\nInstead, it acts as a reverse-proxy to the Docker\ + \ HTTP API. This means that you can execute Docker requests **via** the Portainer\ + \ HTTP API.\n\nTo do so, you can use the `/endpoints/{id}/docker` Portainer API\ + \ endpoint (which is not documented below due to Swagger limitations). This\n\ + endpoint has a restricted access policy so you still need to be authenticated\ + \ to be able to query this endpoint. Any query on this endpoint will be proxied\ + \ to the\nDocker API of the associated endpoint (requests and responses objects\ + \ are the same as documented in the Docker API).\n\n**NOTE**: You can find more\ + \ information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/)\ + \ as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).\n" version: "1.14.0" title: "Portainer API" contact: From 35dd3916dd779347fc10a5ff1949ac7bd8eab837 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 22 Aug 2017 16:36:12 +0200 Subject: [PATCH 02/33] fix(authentication): do not use $sanitize with LDAP authentication (#1136) --- app/components/auth/authController.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/components/auth/authController.js b/app/components/auth/authController.js index 14ef479d7..cd203cfc0 100644 --- a/app/components/auth/authController.js +++ b/app/components/auth/authController.js @@ -1,6 +1,6 @@ angular.module('auth', []) -.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', -function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) { +.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService', +function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) { $scope.authData = { username: 'admin', @@ -78,9 +78,19 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentica $scope.authenticateUser = function() { $scope.authenticationError = false; - var username = $sanitize($scope.authData.username); - var password = $sanitize($scope.authData.password); - Authentication.login(username, password) + + SettingsService.publicSettings() + .then(function success(data) { + var settings = data; + var username = $scope.authData.username; + var password = $scope.authData.password; + if (settings.AuthenticationMethod === 1) { + username = $sanitize($scope.authData.username); + password = $sanitize($scope.authData.password); + } + + return Authentication.login(username, password); + }) .then(function success(data) { return EndpointService.endpoints(); }) From d50a650686aaef5084fe4ecce4a45df1f97a6ef0 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 23 Aug 2017 09:51:42 +0200 Subject: [PATCH 03/33] feat(dashboard): remove driver information in volumes (#1148) --- app/components/dashboard/dashboard.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/components/dashboard/dashboard.html b/app/components/dashboard/dashboard.html index 8ba214666..d3b1426c5 100644 --- a/app/components/dashboard/dashboard.html +++ b/app/components/dashboard/dashboard.html @@ -125,9 +125,6 @@
-
-
{{ infoData.Driver }} driver
-
{{ volumeData.total }}
Volumes
From a6ef27164cb62696af780648d818ef9b6dd1ccd3 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 23 Aug 2017 10:06:18 +0200 Subject: [PATCH 04/33] feat(container-details): prevent re-creation, edition & duplication for service task (#1149) --- app/components/container/container.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/container/container.html b/app/components/container/container.html index b399a1eca..34590f292 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -20,8 +20,8 @@ - - + + From 8c68e92e748278dbedea749b0d28e557bd45eae8 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 24 Aug 2017 07:53:34 +0200 Subject: [PATCH 05/33] feat(images): use containers instead of /system/df to check unused images (#1150) --- app/components/images/images.html | 6 +++--- app/components/images/imagesController.js | 2 +- .../templates/templatesController.js | 2 +- app/models/docker/image.js | 2 +- app/services/docker/containerService.js | 2 +- app/services/docker/imageService.js | 18 ++++++++++++------ 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/components/images/images.html b/app/components/images/images.html index 5959c0bf2..8e82ff1a6 100644 --- a/app/components/images/images.html +++ b/app/components/images/images.html @@ -70,7 +70,7 @@
- + @@ -121,12 +121,12 @@ - + {{ image.Id|truncate:20}} + ng-if="::image.ContainerCount === 0"> Unused diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index e8c9ca30f..6770f44a9 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -95,7 +95,7 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService) $('#loadImagesSpinner').show(); var endpointProvider = $scope.applicationState.endpoint.mode.provider; var apiVersion = $scope.applicationState.endpoint.apiVersion; - ImageService.images(apiVersion >= 1.25 && endpointProvider !== 'DOCKER_SWARM' && endpointProvider !== 'VMWARE_VIC') + ImageService.images(true) .then(function success(data) { $scope.images = data; }) diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 9a8d90455..347b11d9e 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -151,7 +151,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer $q.all({ templates: TemplateService.getTemplates(templatesKey), - containers: ContainerService.getContainers(0), + containers: ContainerService.containers(0), volumes: VolumeService.getVolumes(), networks: NetworkService.networks( provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', diff --git a/app/models/docker/image.js b/app/models/docker/image.js index 43301787f..5dd073800 100644 --- a/app/models/docker/image.js +++ b/app/models/docker/image.js @@ -3,8 +3,8 @@ function ImageViewModel(data) { this.Tag = data.Tag; this.Repository = data.Repository; this.Created = data.Created; - this.Containers = data.dataUsage ? data.dataUsage.Containers : 0; this.Checked = false; this.RepoTags = data.RepoTags; this.VirtualSize = data.VirtualSize; + this.ContainerCount = data.ContainerCount; } diff --git a/app/services/docker/containerService.js b/app/services/docker/containerService.js index 001b2203d..ee946fec3 100644 --- a/app/services/docker/containerService.js +++ b/app/services/docker/containerService.js @@ -3,7 +3,7 @@ angular.module('portainer.services') 'use strict'; var service = {}; - service.getContainers = function (all) { + service.containers = function(all) { var deferred = $q.defer(); Container.query({ all: all }).$promise .then(function success(data) { diff --git a/app/services/docker/imageService.js b/app/services/docker/imageService.js index 95b0ba92e..181de81e3 100644 --- a/app/services/docker/imageService.js +++ b/app/services/docker/imageService.js @@ -1,5 +1,5 @@ angular.module('portainer.services') -.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', 'SystemService', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, SystemService) { +.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', 'ContainerService', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, ContainerService) { 'use strict'; var service = {}; @@ -24,17 +24,23 @@ angular.module('portainer.services') var deferred = $q.defer(); $q.all({ - dataUsage: withUsage ? SystemService.dataUsage() : { Images: [] }, + containers: withUsage ? ContainerService.containers(1) : [], images: Image.query({}).$promise }) .then(function success(data) { - var images = data.images.map(function(item) { - item.dataUsage = data.dataUsage.Images.find(function(usage) { - return item.Id === usage.Id; - }); + var containers = data.containers; + var images = data.images.map(function(item) { + item.ContainerCount = 0; + for (var i = 0; i < containers.length; i++) { + var container = containers[i]; + if (container.ImageID === item.Id) { + item.ContainerCount++; + } + } return new ImageViewModel(item); }); + deferred.resolve(images); }) .catch(function error(err) { From c1e486bf438f01a00d8550fae11553a18cc724e1 Mon Sep 17 00:00:00 2001 From: Adam Snodgrass Date: Mon, 28 Aug 2017 13:53:36 -0500 Subject: [PATCH 06/33] feat(templates): add support for bind mounts in volumes * #777 feat(templates): add support for binding to host path * #777 feat(templates): add link to templates documentation * refactor(templates): update warning style to match theme * fix(templates): remove trailing comma * refactor(templates): use bind instead of self declaration * feat(templates): support readonly property in template volumes * #777 refactor(templates): remove deprecation notice * #777 refactor(templates): remove deprecated condition from template --- app/components/templates/templates.html | 2 +- app/models/api/template.js | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html index 4d2e0dd71..bf05cbbfb 100644 --- a/app/components/templates/templates.html +++ b/app/components/templates/templates.html @@ -27,7 +27,7 @@
- +
diff --git a/app/models/api/template.js b/app/models/api/template.js index 01532f6f6..3883edf4f 100644 --- a/app/models/api/template.js +++ b/app/models/api/template.js @@ -16,11 +16,18 @@ function TemplateViewModel(data) { this.Volumes = []; if (data.volumes) { this.Volumes = data.volumes.map(function (v) { - return { - readOnly: false, - containerPath: v, + var volume = { + readOnly: v.readonly || false, + containerPath: v.container || v, type: 'auto' }; + + if (v.bind) { + volume.name = v.bind; + volume.type = 'bind'; + } + + return volume; }); } this.Ports = []; From 13b2fcffd21f93ced1bb026b4bc772abda084089 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 28 Aug 2017 20:57:41 +0200 Subject: [PATCH 07/33] docs(templates): add deprecation notice for old volume format --- app/models/api/template.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/api/template.js b/app/models/api/template.js index 3883edf4f..0123f92e4 100644 --- a/app/models/api/template.js +++ b/app/models/api/template.js @@ -16,6 +16,8 @@ function TemplateViewModel(data) { this.Volumes = []; if (data.volumes) { this.Volumes = data.volumes.map(function (v) { + // @DEPRECATED: New volume definition introduced + // via https://github.com/portainer/portainer/pull/1154 var volume = { readOnly: v.readonly || false, containerPath: v.container || v, From e65d132b3dc40eea45560db2583d96de91a5dde1 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 28 Aug 2017 20:59:13 +0200 Subject: [PATCH 08/33] feat(init-admin): allow to specify a username for the initial admin account (#1160) --- api/errors.go | 6 +- api/http/handler/user.go | 33 +++- app/app.js | 39 +++- app/components/auth/auth.html | 78 ++------ app/components/auth/authController.js | 183 ++++++++---------- app/components/initAdmin/initAdmin.html | 80 ++++++++ .../initAdmin/initAdminController.js | 33 ++++ .../initEndpoint.html} | 104 ++++++---- .../initEndpointController.js} | 73 +++---- app/services/api/userService.js | 21 ++ assets/css/app.css | 4 +- index.html | 7 +- 12 files changed, 385 insertions(+), 276 deletions(-) create mode 100644 app/components/initAdmin/initAdmin.html create mode 100644 app/components/initAdmin/initAdminController.js rename app/components/{endpointInit/endpointInit.html => initEndpoint/initEndpoint.html} (59%) rename app/components/{endpointInit/endpointInitController.js => initEndpoint/initEndpointController.js} (51%) diff --git a/api/errors.go b/api/errors.go index bf5f5517a..aefd967b7 100644 --- a/api/errors.go +++ b/api/errors.go @@ -13,8 +13,10 @@ const ( const ( ErrUserNotFound = Error("User not found") ErrUserAlreadyExists = Error("User already exists") - ErrInvalidUsername = Error("Invalid username. White spaces are not allowed.") - ErrAdminAlreadyInitialized = Error("Admin user already initialized") + ErrInvalidUsername = Error("Invalid username. White spaces are not allowed") + ErrAdminAlreadyInitialized = Error("An administrator user already exists") + ErrCannotRemoveAdmin = Error("Cannot remove the default administrator account") + ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator") ) // Team errors. diff --git a/api/http/handler/user.go b/api/http/handler/user.go index 7aa4e11c9..72952737d 100644 --- a/api/http/handler/user.go +++ b/api/http/handler/user.go @@ -82,6 +82,7 @@ type ( } postAdminInitRequest struct { + Username string `valid:"required"` Password string `valid:"required"` } ) @@ -358,10 +359,14 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R return } - user, err := handler.UserService.UserByUsername("admin") - if err == portainer.ErrUserNotFound { + users, err := handler.UserService.UsersByRole(portainer.AdministratorRole) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + if len(users) == 0 { user := &portainer.User{ - Username: "admin", + Username: req.Username, Role: portainer.AdministratorRole, } user.Password, err = handler.CryptoService.Hash(req.Password) @@ -375,11 +380,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } - } else if err != nil { - httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) - return - } - if user != nil { + } else { httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger) return } @@ -396,6 +397,22 @@ func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Requ return } + if userID == 1 { + httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveAdmin, http.StatusForbidden, handler.Logger) + return + } + + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + if tokenData.ID == portainer.UserID(userID) { + httperror.WriteErrorResponse(w, portainer.ErrAdminCannotRemoveSelf, http.StatusForbidden, handler.Logger) + return + } + _, err = handler.UserService.User(portainer.UserID(userID)) if err == portainer.ErrUserNotFound { diff --git a/app/app.js b/app/app.js index d7774719f..059b41344 100644 --- a/app/app.js +++ b/app/app.js @@ -34,11 +34,12 @@ angular.module('portainer', [ 'docker', 'endpoint', 'endpointAccess', - 'endpointInit', 'endpoints', 'events', 'image', 'images', + 'initAdmin', + 'initEndpoint', 'main', 'network', 'networks', @@ -321,6 +322,33 @@ angular.module('portainer', [ } } }) + .state('init', { + abstract: true, + url: '/init', + views: { + 'content@': { + template: '
' + } + } + }) + .state('init.endpoint', { + url: '/endpoint', + views: { + 'content@': { + templateUrl: 'app/components/initEndpoint/initEndpoint.html', + controller: 'InitEndpointController' + } + } + }) + .state('init.admin', { + url: '/admin', + views: { + 'content@': { + templateUrl: 'app/components/initAdmin/initAdmin.html', + controller: 'InitAdminController' + } + } + }) .state('docker', { url: '/docker/', views: { @@ -373,15 +401,6 @@ angular.module('portainer', [ } } }) - .state('endpointInit', { - url: '/init/endpoint', - views: { - 'content@': { - templateUrl: 'app/components/endpointInit/endpointInit.html', - controller: 'EndpointInitController' - } - } - }) .state('events', { url: '/events/', views: { diff --git a/app/components/auth/auth.html b/app/components/auth/auth.html index 8668d328a..fe53cc0b5 100644 --- a/app/components/auth/auth.html +++ b/app/components/auth/auth.html @@ -1,92 +1,38 @@
-
+
- +
- -
-
- - - -
-
- -
+
-