From 10f7744a622de98698a53ac5df6e8d24afcd9e9a Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 1 Feb 2017 22:13:48 +1300 Subject: [PATCH] feat(authentication): add a --no-auth flag to disable authentication (#553) --- api/cli/cli.go | 1 + api/cli/defaults.go | 1 + api/cli/defaults_windows.go | 1 + api/cmd/portainer/main.go | 15 +- api/http/auth_handler.go | 9 + api/http/middleware.go | 35 ++-- api/http/server.go | 5 +- api/portainer.go | 6 +- app/app.js | 245 ++++++++++---------------- app/components/auth/authController.js | 17 ++ app/components/sidebar/sidebar.html | 2 +- app/directives/header-content.js | 9 +- app/directives/header-title.js | 6 +- app/services/authentication.js | 11 +- app/services/localStorage.js | 6 + app/services/stateManager.js | 25 ++- 16 files changed, 203 insertions(+), 191 deletions(-) diff --git a/api/cli/cli.go b/api/cli/cli.go index 10f476c9b..acaaa8ea7 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -29,6 +29,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(), + NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(), TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(), TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(), TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(), diff --git a/api/cli/defaults.go b/api/cli/defaults.go index adf8affbf..57c755ea0 100644 --- a/api/cli/defaults.go +++ b/api/cli/defaults.go @@ -7,6 +7,7 @@ const ( defaultDataDirectory = "/data" defaultAssetsDirectory = "." defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" + defaultNoAuth = "false" defaultTLSVerify = "false" defaultTLSCACertPath = "/certs/ca.pem" defaultTLSCertPath = "/certs/cert.pem" diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go index 3a4106c74..6ffc1f331 100644 --- a/api/cli/defaults_windows.go +++ b/api/cli/defaults_windows.go @@ -5,6 +5,7 @@ const ( defaultDataDirectory = "C:\\data" defaultAssetsDirectory = "." defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" + defaultNoAuth = "false" defaultTLSVerify = "false" defaultTLSCACertPath = "C:\\certs\\ca.pem" defaultTLSCertPath = "C:\\certs\\cert.pem" diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index e01c304a9..4e12d2091 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -25,8 +25,9 @@ func main() { } settings := &portainer.Settings{ - HiddenLabels: *flags.Labels, - Logo: *flags.Logo, + HiddenLabels: *flags.Labels, + Logo: *flags.Logo, + Authentication: !*flags.NoAuth, } fileService, err := file.NewService(*flags.Data, "") @@ -41,9 +42,12 @@ func main() { } defer store.Close() - jwtService, err := jwt.NewService() - if err != nil { - log.Fatal(err) + var jwtService portainer.JWTService + if !*flags.NoAuth { + jwtService, err = jwt.NewService() + if err != nil { + log.Fatal(err) + } } var cryptoService portainer.CryptoService = &crypto.Service{} @@ -76,6 +80,7 @@ func main() { AssetsPath: *flags.Assets, Settings: settings, TemplatesURL: *flags.Templates, + AuthDisabled: *flags.NoAuth, UserService: store.UserService, EndpointService: store.EndpointService, CryptoService: cryptoService, diff --git a/api/http/auth_handler.go b/api/http/auth_handler.go index 29c19201d..b4c1af789 100644 --- a/api/http/auth_handler.go +++ b/api/http/auth_handler.go @@ -16,6 +16,7 @@ import ( type AuthHandler struct { *mux.Router Logger *log.Logger + authDisabled bool UserService portainer.UserService CryptoService portainer.CryptoService JWTService portainer.JWTService @@ -26,6 +27,9 @@ const ( ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format") // ErrInvalidCredentials is an error raised when credentials for a user are invalid ErrInvalidCredentials = portainer.Error("Invalid credentials") + // ErrAuthDisabled is an error raised when trying to access the authentication endpoints + // when the server has been started with the --no-auth flag + ErrAuthDisabled = portainer.Error("Authentication is disabled") ) // NewAuthHandler returns a new instance of AuthHandler. @@ -44,6 +48,11 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques return } + if handler.authDisabled { + Error(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger) + return + } + var req postAuthRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) diff --git a/api/http/middleware.go b/api/http/middleware.go index ff38a736a..891e7c610 100644 --- a/api/http/middleware.go +++ b/api/http/middleware.go @@ -9,7 +9,8 @@ import ( // Service represents a service to manage HTTP middlewares type middleWareService struct { - jwtService portainer.JWTService + jwtService portainer.JWTService + authDisabled bool } func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler { @@ -37,24 +38,26 @@ func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handle // middleWareAuthenticate provides Authentication middleware for handlers func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var token string + if !service.authDisabled { + var token string - // Get token from the Authorization header - tokens, ok := r.Header["Authorization"] - if ok && len(tokens) >= 1 { - token = tokens[0] - token = strings.TrimPrefix(token, "Bearer ") - } + // Get token from the Authorization header + tokens, ok := r.Header["Authorization"] + if ok && len(tokens) >= 1 { + token = tokens[0] + token = strings.TrimPrefix(token, "Bearer ") + } - if token == "" { - Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil) - return - } + if token == "" { + Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil) + return + } - err := service.jwtService.VerifyToken(token) - if err != nil { - Error(w, err, http.StatusUnauthorized, nil) - return + err := service.jwtService.VerifyToken(token) + if err != nil { + Error(w, err, http.StatusUnauthorized, nil) + return + } } next.ServeHTTP(w, r) diff --git a/api/http/server.go b/api/http/server.go index 32975944b..784edc5dc 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -10,6 +10,7 @@ import ( type Server struct { BindAddress string AssetsPath string + AuthDisabled bool UserService portainer.UserService EndpointService portainer.EndpointService CryptoService portainer.CryptoService @@ -40,13 +41,15 @@ func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error { // Start starts the HTTP server func (server *Server) Start() error { middleWareService := &middleWareService{ - jwtService: server.JWTService, + jwtService: server.JWTService, + authDisabled: server.AuthDisabled, } var authHandler = NewAuthHandler() authHandler.UserService = server.UserService authHandler.CryptoService = server.CryptoService authHandler.JWTService = server.JWTService + authHandler.authDisabled = server.AuthDisabled var userHandler = NewUserHandler(middleWareService) userHandler.UserService = server.UserService userHandler.CryptoService = server.CryptoService diff --git a/api/portainer.go b/api/portainer.go index 6d928e1b2..bc5af38ac 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -20,6 +20,7 @@ type ( Labels *[]Pair Logo *string Templates *string + NoAuth *bool TLSVerify *bool TLSCacert *string TLSCert *string @@ -28,8 +29,9 @@ type ( // Settings represents Portainer settings. Settings struct { - HiddenLabels []Pair `json:"hiddenLabels"` - Logo string `json:"logo"` + HiddenLabels []Pair `json:"hiddenLabels"` + Logo string `json:"logo"` + Authentication bool `json:"authentication"` } // User represent a user account. diff --git a/app/app.js b/app/app.js index c6333b0ab..89c33f828 100644 --- a/app/app.js +++ b/app/app.js @@ -67,124 +67,121 @@ angular.module('portainer', [ $urlRouterProvider.otherwise('/auth'); $stateProvider + .state('root', { + abstract: true, + resolve: { + requiresLogin: ['StateManager', function (StateManager) { + var applicationState = StateManager.getState(); + return applicationState.application.authentication; + }] + } + }) .state('auth', { - url: '/auth', + parent: 'root', + url: '/auth', params: { logout: false, error: '' }, views: { - "content": { + "content@": { templateUrl: 'app/components/auth/auth.html', controller: 'AuthenticationController' } + }, + data: { + requiresLogin: false } }) .state('containers', { + parent: 'root', url: '/containers/', views: { - "content": { + "content@": { templateUrl: 'app/components/containers/containers.html', controller: 'ContainersController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('container', { url: "^/containers/:id", views: { - "content": { + "content@": { templateUrl: 'app/components/container/container.html', controller: 'ContainerController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('stats', { url: "^/containers/:id/stats", views: { - "content": { + "content@": { templateUrl: 'app/components/stats/stats.html', controller: 'StatsController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('logs', { url: "^/containers/:id/logs", views: { - "content": { + "content@": { templateUrl: 'app/components/containerLogs/containerlogs.html', controller: 'ContainerLogsController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('console', { url: "^/containers/:id/console", views: { - "content": { + "content@": { templateUrl: 'app/components/containerConsole/containerConsole.html', controller: 'ContainerConsoleController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('dashboard', { + parent: 'root', url: '/dashboard', views: { - "content": { + "content@": { templateUrl: 'app/components/dashboard/dashboard.html', controller: 'DashboardController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('actions', { abstract: true, url: "/actions", views: { - "content": { - template: '
' + "content@": { + template: '
' }, - "sidebar": { - template: '
' + "sidebar@": { + template: '
' } } }) @@ -192,344 +189,281 @@ angular.module('portainer', [ abstract: true, url: "/create", views: { - "content": { - template: '
' + "content@": { + template: '
' }, - "sidebar": { - template: '
' + "sidebar@": { + template: '
' } } }) .state('actions.create.container', { url: "/container", views: { - "content": { + "content@": { templateUrl: 'app/components/createContainer/createcontainer.html', controller: 'CreateContainerController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('actions.create.network', { url: "/network", views: { - "content": { + "content@": { templateUrl: 'app/components/createNetwork/createnetwork.html', controller: 'CreateNetworkController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('actions.create.service', { url: "/service", views: { - "content": { + "content@": { templateUrl: 'app/components/createService/createservice.html', controller: 'CreateServiceController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('actions.create.volume', { url: "/volume", views: { - "content": { + "content@": { templateUrl: 'app/components/createVolume/createvolume.html', controller: 'CreateVolumeController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('docker', { url: '/docker/', views: { - "content": { + "content@": { templateUrl: 'app/components/docker/docker.html', controller: 'DockerController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('endpoints', { url: '/endpoints/', views: { - "content": { + "content@": { templateUrl: 'app/components/endpoints/endpoints.html', controller: 'EndpointsController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('endpoint', { url: '^/endpoints/:id', views: { - "content": { + "content@": { templateUrl: 'app/components/endpoint/endpoint.html', controller: 'EndpointController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('endpointInit', { url: '/init/endpoint', views: { - "content": { + "content@": { templateUrl: 'app/components/endpointInit/endpointInit.html', controller: 'EndpointInitController' } - }, - data: { - requiresLogin: true } }) .state('events', { url: '/events/', views: { - "content": { + "content@": { templateUrl: 'app/components/events/events.html', controller: 'EventsController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('images', { url: '/images/', views: { - "content": { + "content@": { templateUrl: 'app/components/images/images.html', controller: 'ImagesController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('image', { url: '^/images/:id/', views: { - "content": { + "content@": { templateUrl: 'app/components/image/image.html', controller: 'ImageController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('networks', { url: '/networks/', views: { - "content": { + "content@": { templateUrl: 'app/components/networks/networks.html', controller: 'NetworksController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('network', { url: '^/networks/:id/', views: { - "content": { + "content@": { templateUrl: 'app/components/network/network.html', controller: 'NetworkController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('node', { url: '^/nodes/:id/', views: { - "content": { + "content@": { templateUrl: 'app/components/node/node.html', controller: 'NodeController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('services', { url: '/services/', views: { - "content": { + "content@": { templateUrl: 'app/components/services/services.html', controller: 'ServicesController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('service', { url: '^/service/:id/', views: { - "content": { + "content@": { templateUrl: 'app/components/service/service.html', controller: 'ServiceController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('settings', { url: '/settings/', views: { - "content": { + "content@": { templateUrl: 'app/components/settings/settings.html', controller: 'SettingsController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('task', { url: '^/task/:id', views: { - "content": { + "content@": { templateUrl: 'app/components/task/task.html', controller: 'TaskController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('templates', { url: '/templates/', views: { - "content": { + "content@": { templateUrl: 'app/components/templates/templates.html', controller: 'TemplatesController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('volumes', { url: '/volumes/', views: { - "content": { + "content@": { templateUrl: 'app/components/volumes/volumes.html', controller: 'VolumesController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }) .state('swarm', { url: '/swarm/', views: { - "content": { + "content@": { templateUrl: 'app/components/swarm/swarm.html', controller: 'SwarmController' }, - "sidebar": { + "sidebar@": { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } - }, - data: { - requiresLogin: true } }); @@ -550,18 +484,21 @@ angular.module('portainer', [ }; }); }]) - .run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', function ($rootScope, $state, Authentication, authManager, StateManager) { - authManager.checkAuthOnRefresh(); - authManager.redirectWhenUnauthenticated(); - - Authentication.init(); - StateManager.init(); + .run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'Messages', function ($rootScope, $state, Authentication, authManager, StateManager, Messages) { + StateManager.initialize().then(function success(state) { + if (state.application.authentication) { + authManager.checkAuthOnRefresh(); + authManager.redirectWhenUnauthenticated(); + Authentication.init(); + $rootScope.$on('tokenHasExpired', function($state) { + $state.go('auth', {error: 'Your session has expired'}); + }); + } + }, function error(err) { + Messages.error("Failure", err, 'Unable to retrieve application settings'); + }); $rootScope.$state = $state; - - $rootScope.$on('tokenHasExpired', function($state) { - $state.go('auth', {error: 'Your session has expired'}); - }); }]) // This is your docker url that the api will use to make requests // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 diff --git a/app/components/auth/authController.js b/app/components/auth/authController.js index 2e30f2e5d..b46e8537a 100644 --- a/app/components/auth/authController.js +++ b/app/components/auth/authController.js @@ -13,6 +13,23 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au error: false }; + if (!$scope.applicationState.application.authentication) { + EndpointService.getActive().then(function success(data) { + StateManager.updateEndpointState(true) + .then(function success() { + $state.go('dashboard'); + }, function error(err) { + Messages.error("Failure", err, 'Unable to connect to the Docker endpoint'); + }); + }, function error(err) { + if (err.status === 404) { + $state.go('endpointInit'); + } else { + Messages.error("Failure", err, 'Unable to verify Docker endpoint existence'); + } + }); + } + if ($stateParams.logout) { Authentication.logout(); } diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index d8e97c81b..1d29b8f8d 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -47,7 +47,7 @@ Docker -