From 360701e25624a2d2372708a8dba0c92700b93964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Busso?= Date: Tue, 19 Apr 2022 13:10:42 +1200 Subject: [PATCH] feat(docker-desktop-extension): Make Portainer compatible with Docker Desktop Extension EE-2747 (#6644) * Initial extension build * Add auto login fix auto auth add some message Add extension version Double attempt to login Add auto login from jwt check Add autologin on logout revert sidebar Catch error 401 to relogin cleanup login Add password generator Hide User block and collapse sidebar by default hide user box and toggle sidebar remove defailt dd Integrate extension to portainer Move extension to build remove files from ignore Move extension folder fix alpine try to copy folder try add Change base image move folder extension ignore folder build Fix relative path Move ext to root fix image name versioned index Update extension on same image Update mod * fix kubeshell baseurl * Fix kube shell * move build and remove https * Tidy mod * Remove space * Fix hash test * Password manager * change to building locally * Restore version variable and add local install command * fix local dev image + hide users & auth * Password manageListen on locahost onlyr * FIxes base path * Hide only username * Move default to constants * Update app/portainer/components/PageHeader/HeaderContent.html Co-authored-by: Chaim Lev-Ari * fix 2 failing FE tests [EE-2938] * remove password autogeneration from v1 * fix webhooks * fix docker container console and attach * fix default for portainer IP * update meta, dockerfile and makefile for new ver * fix basepath in kube and docker console * revert makefile changes * add icon back * Add remote short cut command * make local methods the default * default to 0.0.0 for version for local development * simplify make commands * small build fixes * resolve conflicts * Update api/filesystem/write.go Co-authored-by: Chaim Lev-Ari * use a more secure default pass Co-authored-by: itsconquest Co-authored-by: Chaim Lev-Ari --- .dockerignore | 2 + api/cli/cli.go | 2 +- api/crypto/hash.go | 6 +-- api/crypto/hash_test.go | 53 +++++++++++++++++++ api/filesystem/write.go | 1 + api/portainer.go | 2 +- app/config.js | 1 + .../console/containerConsoleController.js | 8 +-- app/global.d.ts | 4 ++ app/index.html | 28 ++++++---- .../kubectl-shell/kubectl-shell.controller.js | 4 +- .../applications/console/consoleController.js | 5 +- .../PageHeader/HeaderContent.controller.js | 2 +- .../components/PageHeader/HeaderContent.html | 6 +-- .../components/PageHeader/HeaderContent.tsx | 2 +- .../PageHeader/HeaderTitle.controller.js | 2 +- .../components/PageHeader/HeaderTitle.html | 5 +- .../components/PageHeader/HeaderTitle.tsx | 5 +- app/portainer/helpers/webhookHelper.js | 16 ++++-- .../interceptors/endpointStatusInterceptor.js | 8 +-- app/portainer/services/authentication.js | 43 ++++++++++----- .../create/createEndpointController.js | 12 ++++- app/portainer/views/main/mainController.js | 2 +- app/portainer/views/settings/settings.html | 36 ++++++++----- .../views/settings/settingsController.js | 1 + app/portainer/views/sidebar/sidebar.html | 8 ++- .../views/sidebar/sidebarController.js | 1 + build/docker-extension/Makefile | 29 ++++++++++ build/docker-extension/docker-compose.yml | 18 +++++++ build/docker-extension/metadata.json | 15 ++++++ build/docker-extension/portainer.svg | 1 + build/linux/Dockerfile | 7 +++ package.json | 1 + 33 files changed, 269 insertions(+), 67 deletions(-) create mode 100644 api/crypto/hash_test.go create mode 100644 build/docker-extension/Makefile create mode 100644 build/docker-extension/docker-compose.yml create mode 100644 build/docker-extension/metadata.json create mode 100644 build/docker-extension/portainer.svg diff --git a/.dockerignore b/.dockerignore index 3765648b1..8b26d6a73 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ * !dist !build +!metadata.json +!docker-extension/build diff --git a/api/cli/cli.go b/api/cli/cli.go index f04804cd3..8a2ee9c19 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -51,7 +51,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(), Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(), SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(), - AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(), + AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(), AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')), Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(), diff --git a/api/crypto/hash.go b/api/crypto/hash.go index 3e52dfbd3..d9b212bf3 100644 --- a/api/crypto/hash.go +++ b/api/crypto/hash.go @@ -9,11 +9,11 @@ type Service struct{} // Hash hashes a string using the bcrypt algorithm func (*Service) Hash(data string) (string, error) { - hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost) + bytes, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost) if err != nil { - return "", nil + return "", err } - return string(hash), nil + return string(bytes), err } // CompareHashAndData compares a hash to clear data and returns an error if the comparison fails. diff --git a/api/crypto/hash_test.go b/api/crypto/hash_test.go new file mode 100644 index 000000000..bc73be3a5 --- /dev/null +++ b/api/crypto/hash_test.go @@ -0,0 +1,53 @@ +package crypto + +import ( + "testing" +) + +func TestService_Hash(t *testing.T) { + var s = &Service{} + + type args struct { + hash string + data string + } + tests := []struct { + name string + args args + expect bool + }{ + { + name: "Empty", + args: args{ + hash: "", + data: "", + }, + expect: false, + }, + { + name: "Matching", + args: args{ + hash: "$2a$10$6BFGd94oYx8k0bFNO6f33uPUpcpAJyg8UVX.akLe9EthF/ZBTXqcy", + data: "Passw0rd!", + }, + expect: true, + }, + { + name: "Not matching", + args: args{ + hash: "$2a$10$ltKrUZ7492xyutHOb0/XweevU4jyw7QO66rP32jTVOMb3EX3JxA/a", + data: "Passw0rd!", + }, + expect: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := s.CompareHashAndData(tt.args.hash, tt.args.data) + if (err != nil) == tt.expect { + t.Errorf("Service.CompareHashAndData() = %v", err) + } + }) + } +} diff --git a/api/filesystem/write.go b/api/filesystem/write.go index 235511933..1e4b714a0 100644 --- a/api/filesystem/write.go +++ b/api/filesystem/write.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" ) +// WriteToFile creates a file in the filesystem storage func WriteToFile(dst string, content []byte) error { if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil { return errors.Wrapf(err, "failed to create filestructure for the path %q", dst) diff --git a/api/portainer.go b/api/portainer.go index 2f942ea82..ca5711247 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1344,7 +1344,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "2.11.0" + APIVersion = "2.11.1" // DBVersion is the version number of the Portainer database DBVersion = 35 // ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax diff --git a/app/config.js b/app/config.js index 2cac8c5dc..d701ee818 100644 --- a/app/config.js +++ b/app/config.js @@ -14,6 +14,7 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService tokenGetter: /* @ngInject */ function tokenGetter(LocalStorage) { return LocalStorage.getJWT(); }, + whiteListedDomains: ['localhost'], }); $httpProvider.interceptors.push('jwtInterceptor'); diff --git a/app/docker/views/containers/console/containerConsoleController.js b/app/docker/views/containers/console/containerConsoleController.js index ab2e92d6c..940bd30ba 100644 --- a/app/docker/views/containers/console/containerConsoleController.js +++ b/app/docker/views/containers/console/containerConsoleController.js @@ -69,9 +69,9 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [ id: attachId, }; + const base = window.location.origin.startsWith('http') ? `${window.location.origin}${baseHref()}` : baseHref(); var url = - window.location.origin + - baseHref() + + base + 'api/websocket/attach?' + Object.keys(params) .map((k) => k + '=' + params[k]) @@ -110,9 +110,9 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [ id: data.Id, }; + const base = window.location.origin.startsWith('http') ? `${window.location.origin}${baseHref()}` : baseHref(); var url = - window.location.origin + - baseHref() + + base + 'api/websocket/exec?' + Object.keys(params) .map((k) => k + '=' + params[k]) diff --git a/app/global.d.ts b/app/global.d.ts index a8842eefe..3ae967ca1 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -18,3 +18,7 @@ declare module 'axios-progress-bar' { instance?: AxiosInstance ): void; } + +interface Window { + ddExtension: boolean; +} diff --git a/app/index.html b/app/index.html index 920e1af72..8748fde19 100644 --- a/app/index.html +++ b/app/index.html @@ -7,9 +7,16 @@ @@ -62,9 +69,12 @@ -
- +
+ + + + + + + + diff --git a/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js b/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js index 2a96fb6d0..3b77d9ba1 100644 --- a/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js +++ b/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js @@ -93,11 +93,13 @@ export default class KubectlShellController { const wsProtocol = this.$window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const path = baseHref() + 'api/websocket/kubernetes-shell'; + const base = path.startsWith('http') ? path.replace(/^https?:\/\//i, '') : window.location.host + path; + const queryParams = Object.entries(params) .map(([k, v]) => `${k}=${v}`) .join('&'); - const url = `${wsProtocol}${window.location.host}${path}?${queryParams}`; + const url = `${wsProtocol}${base}?${queryParams}`; Terminal.applyAddon(fit); this.state.shell.socket = new WebSocket(url); this.state.shell.term = new Terminal(); diff --git a/app/kubernetes/views/applications/console/consoleController.js b/app/kubernetes/views/applications/console/consoleController.js index 590bbedcf..f0912d106 100644 --- a/app/kubernetes/views/applications/console/consoleController.js +++ b/app/kubernetes/views/applications/console/consoleController.js @@ -58,9 +58,10 @@ class KubernetesApplicationConsoleController { command: this.state.command, }; + const base = window.location.origin.startsWith('http') ? `${window.location.origin}${baseHref()}` : baseHref(); + let url = - window.location.origin + - baseHref() + + base + 'api/websocket/pod?' + Object.keys(params) .map((k) => k + '=' + params[k]) diff --git a/app/portainer/components/PageHeader/HeaderContent.controller.js b/app/portainer/components/PageHeader/HeaderContent.controller.js index f69beb9f8..7392c6be5 100644 --- a/app/portainer/components/PageHeader/HeaderContent.controller.js +++ b/app/portainer/components/PageHeader/HeaderContent.controller.js @@ -2,7 +2,7 @@ export default class HeaderContentController { /* @ngInject */ constructor(Authentication) { this.Authentication = Authentication; - + this.display = !window.ddExtension; this.username = null; } diff --git a/app/portainer/components/PageHeader/HeaderContent.html b/app/portainer/components/PageHeader/HeaderContent.html index 2b5d569e5..51b1eb69c 100644 --- a/app/portainer/components/PageHeader/HeaderContent.html +++ b/app/portainer/components/PageHeader/HeaderContent.html @@ -1,11 +1,11 @@