Compare commits
19 Commits
2.34.0
...
feat/EE-27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
344bf1082a | ||
|
|
de9bdf1cfa | ||
|
|
6afb359af6 | ||
|
|
043b341e22 | ||
|
|
a210791414 | ||
|
|
1719810ae7 | ||
|
|
75297d0ed6 | ||
|
|
b6483a9e36 | ||
|
|
0f8f7b2051 | ||
|
|
efd9391496 | ||
|
|
7e05935131 | ||
|
|
76318fbb54 | ||
|
|
68a7852295 | ||
|
|
8b330176ab | ||
|
|
fd28b66fc5 | ||
|
|
5f37097ba2 | ||
|
|
2c6360521f | ||
|
|
618e35bbb5 | ||
|
|
33bfa87b83 |
@@ -1,3 +1,5 @@
|
||||
*
|
||||
!dist
|
||||
!build
|
||||
!metadata.json
|
||||
!docker-extension/build
|
||||
|
||||
@@ -53,11 +53,12 @@ To do so, you can use the `/endpoints/{id}/docker` Portainer API environment(end
|
||||
# Private Registry
|
||||
|
||||
Using private registry, you will need to pass a based64 encoded JSON string ‘{"registryId":\<registryID value\>}’ inside the Request Header. The parameter name is "X-Registry-Auth".
|
||||
\<registryID value\> - The registry ID where the repository was created.
|
||||
\<registryID value\> - The registry ID where the repository was created.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
eyJyZWdpc3RyeUlkIjoxfQ==
|
||||
```
|
||||
|
||||
**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://documentation.portainer.io/api/api-examples/).
|
||||
|
||||
@@ -51,8 +51,9 @@ 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(),
|
||||
GenerateAdminPassword: kingpin.Flag("generate-admin-password", "Generate a password for the admin user").Bool(),
|
||||
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(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/password"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
@@ -647,6 +648,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
adminPasswordHash = *flags.AdminPassword
|
||||
}
|
||||
|
||||
if *flags.GenerateAdminPassword {
|
||||
adminPasswordHash, err = password.GeneratePassword(cryptoService, fileService)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed generating admin password: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if adminPasswordHash != "" {
|
||||
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
|
||||
@@ -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.
|
||||
|
||||
53
api/crypto/hash_test.go
Normal file
53
api/crypto/hash_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,12 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// WriteToFile create a file in the filesystem storage
|
||||
func (service *Service) WriteToFile(path string, data []byte) error {
|
||||
return WriteToFile(path, data)
|
||||
}
|
||||
|
||||
// WriteToFile create 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)
|
||||
|
||||
@@ -35,6 +35,7 @@ require (
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sethvargo/go-password v0.2.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/viney-shih/go-lock v1.1.1
|
||||
|
||||
@@ -670,6 +670,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
|
||||
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
|
||||
77
api/internal/password/generator.go
Normal file
77
api/internal/password/generator.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
const passwordFile = ".pwd"
|
||||
|
||||
type service struct {
|
||||
cryptoService portainer.CryptoService
|
||||
fileService portainer.FileService
|
||||
}
|
||||
|
||||
// GeneratePassword generates a password and stores it in the filesystem, return a hash of the password
|
||||
func GeneratePassword(cryptoService portainer.CryptoService, fileService portainer.FileService) (string, error) {
|
||||
pw := service{
|
||||
cryptoService: cryptoService,
|
||||
fileService: fileService,
|
||||
}
|
||||
|
||||
hash, err := pw.getHashFromPasswordFile()
|
||||
if err == nil {
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
return pw.generatePassword()
|
||||
}
|
||||
|
||||
func (pw *service) getHashFromPasswordFile() (string, error) {
|
||||
|
||||
if !pw.checkPasswordFile() {
|
||||
return "", errors.New("password file does not exist")
|
||||
}
|
||||
|
||||
pwd, err := pw.loadPassword()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to load password")
|
||||
}
|
||||
|
||||
return pw.cryptoService.Hash(string(pwd))
|
||||
}
|
||||
|
||||
func (pw *service) generatePassword() (string, error) {
|
||||
pwd, err := password.Generate(16, 4, 4, false, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash, err := pw.cryptoService.Hash(pwd)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to hash password")
|
||||
}
|
||||
|
||||
pw.savePassword(pwd)
|
||||
|
||||
pw.fileService.WriteToFile(passwordFile, []byte(hash))
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func (pw *service) savePassword(pwd string) error {
|
||||
return pw.fileService.WriteToFile(passwordFile, []byte(pwd))
|
||||
}
|
||||
|
||||
func (pw *service) loadPassword() ([]byte, error) {
|
||||
return pw.fileService.GetFileContent("", passwordFile)
|
||||
}
|
||||
|
||||
func (pw *service) checkPasswordFile() bool {
|
||||
_, err := pw.fileService.FileExists(passwordFile)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
142
api/internal/password/generator_test.go
Normal file
142
api/internal/password/generator_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func TestGeneratePassword(t *testing.T) {
|
||||
type args struct {
|
||||
cryptoService portainer.CryptoService
|
||||
fileService portainer.FileService
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GeneratePassword(tt.args.cryptoService, tt.args.fileService)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GeneratePassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GeneratePassword() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_service_getHashFromPasswordFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pw *service
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.pw.getHashFromPasswordFile()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("service.getHashFromPasswordFile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("service.getHashFromPasswordFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_service_generatePassword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pw *service
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.pw.generatePassword()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("service.generatePassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("service.generatePassword() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_service_savePassword(t *testing.T) {
|
||||
type args struct {
|
||||
pwd string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
pw *service
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.pw.savePassword(tt.args.pwd); (err != nil) != tt.wantErr {
|
||||
t.Errorf("service.savePassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_service_loadPassword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pw *service
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.pw.loadPassword()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("service.loadPassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("service.loadPassword() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_service_checkPasswordFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pw *service
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.pw.checkPasswordFile(); got != tt.want {
|
||||
t.Errorf("service.checkPasswordFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,7 @@ type (
|
||||
TunnelPort *string
|
||||
AdminPassword *string
|
||||
AdminPasswordFile *string
|
||||
GenerateAdminPassword *bool
|
||||
Assets *string
|
||||
Data *string
|
||||
FeatureFlags *[]Pair
|
||||
@@ -1240,6 +1241,7 @@ type (
|
||||
CopySSLCertPair(certPath, keyPath string) (string, string, error)
|
||||
CopySSLCACert(caCertPath string) (string, error)
|
||||
StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error)
|
||||
WriteToFile(path string, data []byte) error
|
||||
}
|
||||
|
||||
// GitService represents a service for managing Git
|
||||
@@ -1339,7 +1341,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
|
||||
|
||||
@@ -14,6 +14,7 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService
|
||||
tokenGetter: /* @ngInject */ function tokenGetter(LocalStorage) {
|
||||
return LocalStorage.getJWT();
|
||||
},
|
||||
whiteListedDomains: ['localhost'],
|
||||
});
|
||||
|
||||
$httpProvider.interceptors.push('jwtInterceptor');
|
||||
|
||||
4
app/global.d.ts
vendored
4
app/global.d.ts
vendored
@@ -18,3 +18,7 @@ declare module 'axios-progress-bar' {
|
||||
instance?: AxiosInstance
|
||||
): void;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
ddExtension: boolean;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,16 @@
|
||||
<meta name="author" content="<%= author %>" />
|
||||
<base id="base" />
|
||||
<script>
|
||||
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
||||
var basePath = path ? '/' + path + '/' : '/';
|
||||
document.getElementById('base').href = basePath;
|
||||
if (window.origin == 'file://') {
|
||||
// we are loading the app from a local file as in docker extension
|
||||
document.getElementById('base').href = 'http://localhost:9000/';
|
||||
|
||||
window.ddExtension = true;
|
||||
} else {
|
||||
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
||||
var basePath = path ? '/' + path + '/' : '/';
|
||||
document.getElementById('base').href = basePath;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
@@ -62,9 +69,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div id="view" ui-view="content" ng-if="!applicationState.loading"></div> </div
|
||||
><!-- End Page Content --> </div
|
||||
><!-- End Content Wrapper --> </div
|
||||
><!-- End Page Wrapper -->
|
||||
</body></html
|
||||
>
|
||||
<div id="view" ui-view="content" ng-if="!applicationState.loading"></div>
|
||||
</div>
|
||||
<!-- End Page Content -->
|
||||
</div>
|
||||
<!-- End Content Wrapper -->
|
||||
</div>
|
||||
<!-- End Page Wrapper -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -2,7 +2,7 @@ export default class HeaderContentController {
|
||||
/* @ngInject */
|
||||
constructor(Authentication) {
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.display = !window.ddExtension;
|
||||
this.username = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div class="breadcrumb-links">
|
||||
<div class="pull-left" ng-transclude></div>
|
||||
<div class="pull-right" ng-if="$ctrl.username">
|
||||
<div class="pull-right" ng-if="$ctrl.username && $ctrl.display">
|
||||
<a ui-sref="portainer.account" style="margin-right: 5px">
|
||||
<u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u>
|
||||
<u> <i class="fa fa-wrench" aria-hidden="true"></i> my account </u>
|
||||
</a>
|
||||
<a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px" data-cy="template-logoutButton">
|
||||
<u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u>
|
||||
<u> <i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function HeaderContent({ children }: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<div className="breadcrumb-links">
|
||||
<div className="pull-left">{children}</div>
|
||||
{user && (
|
||||
{user && !window.ddExtension && (
|
||||
<div className={clsx('pull-right', styles.userLinks)}>
|
||||
<Link to="portainer.account" className={styles.link}>
|
||||
<i
|
||||
|
||||
@@ -2,7 +2,7 @@ export default class HeaderTitle {
|
||||
/* @ngInject */
|
||||
constructor(Authentication) {
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.display = !window.ddExtension;
|
||||
this.username = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<div class="page white-space-normal">
|
||||
{{ $ctrl.titleText }}
|
||||
<span class="header_title_content" ng-transclude></span>
|
||||
<span class="pull-right user-box" ng-if="$ctrl.username"> <i class="fa fa-user-circle" aria-hidden="true"></i> {{ $ctrl.username }} </span>
|
||||
<span class="pull-right user-box" ng-if="$ctrl.username && $ctrl.display">
|
||||
<i class="fa fa-user-circle" aria-hidden="true"></i>
|
||||
{{ $ctrl.username }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,10 @@ export function HeaderTitle({ title, children }: PropsWithChildren<Props>) {
|
||||
<div className="page white-space-normal">
|
||||
{title}
|
||||
<span className="header_title_content">{children}</span>
|
||||
{user && (
|
||||
{user && !window.ddExtension && (
|
||||
<span className="pull-right user-box">
|
||||
<i className="fa fa-user-circle" aria-hidden="true" /> {user.Username}
|
||||
<i className="fa fa-user-circle" aria-hidden="true" />
|
||||
{user.Username}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -31,9 +31,11 @@ angular.module('portainer.app').factory('EndpointStatusInterceptor', [
|
||||
}
|
||||
|
||||
function responseErrorInterceptor(rejection) {
|
||||
var url = rejection.config.url;
|
||||
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
|
||||
EndpointProvider.setOfflineMode(true);
|
||||
if (rejection.config) {
|
||||
var url = rejection.config.url;
|
||||
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
|
||||
EndpointProvider.setOfflineMode(true);
|
||||
}
|
||||
}
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { clear as clearSessionStorage } from './session-storage';
|
||||
|
||||
const DEFAULT_USER = 'admin';
|
||||
const DEFAULT_PASSWORD = 'Passw0rd;';
|
||||
|
||||
angular.module('portainer.app').factory('Authentication', [
|
||||
'$async',
|
||||
'$state',
|
||||
@@ -28,12 +31,14 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
async function initAsync() {
|
||||
try {
|
||||
const jwt = LocalStorage.getJWT();
|
||||
if (jwt) {
|
||||
await setUser(jwt);
|
||||
if (!jwt || jwtHelper.isTokenExpired(jwt)) {
|
||||
return tryAutoLoginExtension();
|
||||
}
|
||||
return !!jwt;
|
||||
await setUser(jwt);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
console.log('Unable to initialize authentication service', error);
|
||||
return tryAutoLoginExtension();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +52,7 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
EndpointProvider.clean();
|
||||
LocalStorage.cleanAuthData();
|
||||
LocalStorage.storeLoginStateUUID('');
|
||||
tryAutoLoginExtension();
|
||||
}
|
||||
|
||||
function logout(performApiLogout) {
|
||||
@@ -59,7 +65,15 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
|
||||
async function OAuthLoginAsync(code) {
|
||||
const response = await OAuth.validate({ code: code }).$promise;
|
||||
await setUser(response.jwt);
|
||||
const jwt = setJWTFromResponse(response);
|
||||
await setUser(jwt);
|
||||
}
|
||||
|
||||
function setJWTFromResponse(response) {
|
||||
const jwt = response.jwt;
|
||||
LocalStorage.storeJWT(jwt);
|
||||
|
||||
return response.jwt;
|
||||
}
|
||||
|
||||
function OAuthLogin(code) {
|
||||
@@ -68,7 +82,8 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
|
||||
async function loginAsync(username, password) {
|
||||
const response = await Auth.login({ username: username, password: password }).$promise;
|
||||
await setUser(response.jwt);
|
||||
const jwt = setJWTFromResponse(response);
|
||||
await setUser(jwt);
|
||||
}
|
||||
|
||||
function login(username, password) {
|
||||
@@ -77,7 +92,7 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
|
||||
function isAuthenticated() {
|
||||
var jwt = LocalStorage.getJWT();
|
||||
return jwt && !jwtHelper.isTokenExpired(jwt);
|
||||
return !!jwt && !jwtHelper.isTokenExpired(jwt);
|
||||
}
|
||||
|
||||
function getUserDetails() {
|
||||
@@ -96,7 +111,6 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
}
|
||||
|
||||
async function setUser(jwt) {
|
||||
LocalStorage.storeJWT(jwt);
|
||||
var tokenPayload = jwtHelper.decodeToken(jwt);
|
||||
user.username = tokenPayload.username;
|
||||
user.ID = tokenPayload.id;
|
||||
@@ -104,11 +118,16 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
await setUserTheme();
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
if (user.role === 1) {
|
||||
return true;
|
||||
function tryAutoLoginExtension() {
|
||||
if (!window.ddExtension) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
return login(DEFAULT_USER, DEFAULT_PASSWORD);
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
return !!user && user.role === 1;
|
||||
}
|
||||
|
||||
return service;
|
||||
|
||||
@@ -19,7 +19,7 @@ angular.module('portainer.app').controller('MainController', [
|
||||
$scope.$watch($scope.getWidth, function (newValue) {
|
||||
if (newValue >= mobileView) {
|
||||
const toggleValue = LocalStorage.getToolbarToggle();
|
||||
$scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : true;
|
||||
$scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : !window.ddExtension;
|
||||
} else {
|
||||
$scope.toggle = false;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_logo" class="control-label text-left"> Use custom logo </label>
|
||||
<label class="switch" style="margin-left: 20px"> <input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" /><i></i> </label>
|
||||
<label class="switch" style="margin-left: 20px">
|
||||
<input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="formValues.customLogo">
|
||||
@@ -40,7 +43,10 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_enableTelemetry" class="control-label text-left"> Allow the collection of anonymous statistics </label>
|
||||
<label class="switch" style="margin-left: 20px"> <input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" /><i></i> </label>
|
||||
<label class="switch" style="margin-left: 20px">
|
||||
<input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-12 text-muted small" style="margin-top: 10px">
|
||||
You can find more information about this in our
|
||||
@@ -128,7 +134,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ssl-certificate-settings></ssl-certificate-settings>
|
||||
<ssl-certificate-settings ng-show="$ctrl.showHTTPS"></ssl-certificate-settings>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
@@ -149,7 +155,7 @@
|
||||
<input type="text" class="form-control" id="header_value" ng-model="formValues.labelValue" placeholder="e.g. bar" />
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-2 margin-sm-top">
|
||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.labelName"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add filter</button>
|
||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.labelName"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add filter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -166,11 +172,11 @@
|
||||
<tr ng-repeat="label in settings.BlackListedLabels">
|
||||
<td>{{ label.name }}</td>
|
||||
<td>{{ label.value }}</td>
|
||||
<td
|
||||
><button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)"
|
||||
><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button
|
||||
></td
|
||||
>
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="settings.BlackListedLabels.length === 0">
|
||||
<td colspan="3" class="text-center text-muted">No filter available.</td>
|
||||
@@ -208,8 +214,8 @@
|
||||
checked="formValues.scheduleAutomaticBackups"
|
||||
label-class="'col-sm-2'"
|
||||
on-change="(onToggleAutoBackups)"
|
||||
></por-switch-field
|
||||
></div>
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Schedule automatic backups -->
|
||||
<!-- Cron rule -->
|
||||
@@ -311,7 +317,8 @@
|
||||
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
|
||||
<div class="col-sm-1">
|
||||
<label class="switch" data-cy="settings-s3PasswordToggle">
|
||||
<input type="checkbox" id="password_protect_s3" name="password_protect_s3" ng-model="formValues.passwordProtectS3" disabled /><i></i>
|
||||
<input type="checkbox" id="password_protect_s3" name="password_protect_s3" ng-model="formValues.passwordProtectS3" disabled />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -342,7 +349,7 @@
|
||||
limited-feature-disabled
|
||||
limited-feature-class="limited-be"
|
||||
>
|
||||
<span><i class="fa fa-upload" aria-hidden="true"></i> Export backup</span>
|
||||
<span> <i class="fa fa-upload" aria-hidden="true"></i> Export backup</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -370,7 +377,8 @@
|
||||
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
|
||||
<div class="col-sm-1">
|
||||
<label class="switch" data-cy="settings-passwordProtectLocal">
|
||||
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i>
|
||||
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
],
|
||||
backupInProgress: false,
|
||||
featureLimited: false,
|
||||
showHTTPS: !window.ddExtension,
|
||||
};
|
||||
|
||||
$scope.BACKUP_FORM_TYPES = { S3: 's3', FILE: 'file' };
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
|
||||
<sidebar-section ng-if="isAdmin || isTeamLeader" title="Settings">
|
||||
<sidebar-menu
|
||||
ng-show="display"
|
||||
icon-class="fa-users fa-fw"
|
||||
label="Users"
|
||||
path="portainer.users"
|
||||
@@ -87,7 +88,9 @@
|
||||
is-sidebar-open="toggle"
|
||||
children-paths="['portainer.settings.authentication', 'portainer.settings.edgeCompute']"
|
||||
>
|
||||
<sidebar-menu-item path="portainer.settings.authentication" class-name="sidebar-sublist" data-cy="portainerSidebar-authentication">Authentication</sidebar-menu-item>
|
||||
<sidebar-menu-item path="portainer.settings.authentication" class-name="sidebar-sublist" data-cy="portainerSidebar-authentication" ng-show="display"
|
||||
>Authentication</sidebar-menu-item
|
||||
>
|
||||
<sidebar-menu-item path="portainer.settings.edgeCompute" class-name="sidebar-sublist" data-cy="portainerSidebar-edge-compute">Edge Compute</sidebar-menu-item>
|
||||
|
||||
<div class="sidebar-sublist">
|
||||
|
||||
@@ -3,6 +3,7 @@ angular.module('portainer.app').controller('SidebarController', SidebarControlle
|
||||
function SidebarController($rootScope, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) {
|
||||
$scope.applicationState = StateManager.getState();
|
||||
$scope.endpointState = EndpointProvider.endpoint();
|
||||
$scope.display = !window.ddExtension;
|
||||
|
||||
function checkPermissions(memberships) {
|
||||
var isLeader = false;
|
||||
|
||||
32
build/docker-extension/Makefile
Normal file
32
build/docker-extension/Makefile
Normal file
@@ -0,0 +1,32 @@
|
||||
# Makefile for development purpose
|
||||
|
||||
.PHONY: all build
|
||||
all: clean build-local install-local
|
||||
|
||||
ORG=portainer
|
||||
VERSION=2.11.12
|
||||
IMAGE_NAME=$(ORG)/portainer-docker-extension
|
||||
TAGGED_IMAGE_NAME=$(IMAGE_NAME):$(VERSION)
|
||||
|
||||
clean:
|
||||
-docker extension remove $(IMAGE_NAME)
|
||||
-docker rmi $(PORTAINER_IMAGE_NAME):$(VERSION)
|
||||
-docker rmi $(IMAGE_NAME)
|
||||
|
||||
build-local:
|
||||
docker buildx build -f build/linux/Dockerfile --load --build-arg TAG=$(VERSION) --build-arg PORTAINER_IMAGE_NAME=$(IMAGE_NAME) --tag=$(IMAGE_NAME):local-development .
|
||||
|
||||
install-local:
|
||||
docker extension install portainer/portainer-docker-extension:local-development
|
||||
|
||||
build-remote:
|
||||
docker buildx build -f build/linux/Dockerfile --push --builder=buildx-multi-arch --platform=windows/amd64,linux/amd64,linux/arm64 --build-arg TAG=$(VERSION) --build-arg PORTAINER_IMAGE_NAME=$(IMAGE_NAME) --tag=$(TAGGED_IMAGE_NAME) .
|
||||
|
||||
install-remote:
|
||||
docker extension install $(TAGGED_IMAGE_NAME)
|
||||
|
||||
multiarch:
|
||||
docker buildx create --name=buildx-multi-arch --driver=docker-container --driver-opt=network=host
|
||||
|
||||
portainer:
|
||||
yarn build
|
||||
20
build/docker-extension/docker-compose.yml
Normal file
20
build/docker-extension/docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
portainer:
|
||||
image: ${DESKTOP_PLUGIN_IMAGE}
|
||||
command: ["--admin-password", "$$$$2a$$$$10$$$$HX4qSZhtQcvKUNmAsPXuPe9POkM7gdaFPcSnRjokgb8EkMI.1gkSa"]
|
||||
# command: ["--generate-password"]
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- portainer_data:/data
|
||||
ports:
|
||||
- 127.0.0.1:8000:8000
|
||||
- 127.0.0.1:9000:9000
|
||||
- 127.0.0.1:9443:9443
|
||||
volumes:
|
||||
portainer_data:
|
||||
|
||||
17
build/docker-extension/metadata.json
Normal file
17
build/docker-extension/metadata.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Portainer",
|
||||
"provider": "Portainer.io",
|
||||
"icon": "portainer.svg",
|
||||
"vm": {
|
||||
"composefile": "docker-compose.yml",
|
||||
"exposes": { "socket": "docker.sock" }
|
||||
},
|
||||
"ui": {
|
||||
"dashboard-tab": {
|
||||
"title": "Portainer",
|
||||
"root": "/public",
|
||||
"src": "index.html",
|
||||
"backend": { "tcp": "9000" }
|
||||
}
|
||||
}
|
||||
}
|
||||
1
build/docker-extension/portainer.svg
Normal file
1
build/docker-extension/portainer.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="2500" viewBox=".16 0 571.71 800" width="1788" xmlns="http://www.w3.org/2000/svg"><g fill="#13bef9"><path d="m190.83 175.88h-12.2v63.2h12.2zm52.47 0h-12.2v63.2h12.2zm71.69-120.61-12.5-21.68-208.67 120.61 12.5 21.68z"/><path d="m313.77 55.27 12.51-21.68 208.67 120.61-12.51 21.68z"/><path d="m571.87 176.18v-25.03h-571.71v25.03z"/><path d="m345.5 529.77v-370.99h25.02v389.01c-6.71-7.64-15.26-13.13-25.02-18.02zm-42.71-6.41v-523.36h25.02v526.41c-7.02-3.36-24.1-3.05-25.02-3.05zm-237.04 52.21c-30.51-22.59-50.64-58.62-50.64-99.54 0-21.68 5.79-43.05 16.47-61.68h213.55c10.98 18.63 16.48 40 16.48 61.68 0 18.93-2.44 36.64-10.07 52.52-16.17-15.57-39.97-22.29-64.07-22.29-42.71 0-79.32 26.56-88.77 66.26-3.36-.31-5.49-.61-8.85-.61-8.24.3-16.17 1.53-24.1 3.66z" fill-rule="evenodd"/><path d="m170.69 267.18h-64.67v65.03h64.67zm-72.91 0h-64.67v65.03h64.67zm0 72.36h-64.67v65.04h64.67zm72.91 0h-64.67v65.04h64.67zm72.61 0h-64.67v65.04h64.67zm0-107.17h-64.67v65.03h64.67z"/><path d="m109.37 585.34c8.85-37.55 42.71-65.65 82.98-65.65 25.94 0 49.12 11.61 64.99 29.93 13.72-9.47 30.2-14.96 48.2-14.96 46.98 0 85.11 38.16 85.11 85.19 0 9.77-1.52 18.93-4.57 27.78 10.37 14.05 16.78 31.76 16.78 50.69 0 47.02-38.14 85.19-85.12 85.19-20.75 0-39.66-7.33-54.3-19.54-15.56 21.68-40.88 36.03-69.56 36.03-32.95 0-61.63-18.93-75.96-46.41-5.8 1.22-11.6 1.83-17.7 1.83-46.98 0-85.42-38.17-85.42-85.19s38.14-85.19 85.42-85.19c3.05-.31 6.1-.31 9.15.3z" fill-rule="evenodd"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
build/docker-extension/pwd
Executable file
BIN
build/docker-extension/pwd
Executable file
Binary file not shown.
15
build/docker-extension/pwd.go
Normal file
15
build/docker-extension/pwd.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read file '.pwd'
|
||||
pwd, err := os.ReadFile(".pwd")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Print(pwd)
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
FROM portainer/base
|
||||
|
||||
LABEL org.opencontainers.image.title="Portainer" \
|
||||
org.opencontainers.image.description="Rich container management experience using Portainer." \
|
||||
org.opencontainers.image.vendor="Portainer.io" \
|
||||
com.docker.desktop.extension.api.version=">= 0.2.0" \
|
||||
com.docker.desktop.extension.icon=https://portainer-io-assets.sfo2.cdn.digitaloceanspaces.com/logos/portainer.png
|
||||
|
||||
COPY dist /
|
||||
COPY build/docker-extension /
|
||||
|
||||
VOLUME /data
|
||||
WORKDIR /
|
||||
|
||||
@@ -41,6 +41,8 @@ module.exports = function (grunt) {
|
||||
|
||||
grunt.registerTask('build', ['build:server', 'build:client']);
|
||||
|
||||
grunt.registerTask('install:extension', ['shell:install_extension']);
|
||||
|
||||
grunt.registerTask('start:server', ['build:server', 'shell:run_container']);
|
||||
|
||||
grunt.registerTask('start:localserver', [`shell:build_binary:linux:${arch}`, 'shell:run_localserver']);
|
||||
@@ -108,6 +110,7 @@ gruntConfig.clean = {
|
||||
gruntConfig.shell = {
|
||||
build_binary: { command: shell_build_binary },
|
||||
build_binary_azuredevops: { command: shell_build_binary_azuredevops },
|
||||
install_extension: { command: shell_install_extension },
|
||||
download_docker_binary: { command: shell_download_docker_binary },
|
||||
download_kompose_binary: { command: shell_download_kompose_binary },
|
||||
download_kubectl_binary: { command: shell_download_kubectl_binary },
|
||||
@@ -155,6 +158,10 @@ function shell_build_binary_azuredevops(platform, arch) {
|
||||
return `build/build_binary_azuredevops.sh ${platform} ${arch};`;
|
||||
}
|
||||
|
||||
function shell_install_extension() {
|
||||
return `make all -f build/docker-extension/Makefile`;
|
||||
}
|
||||
|
||||
function shell_run_container() {
|
||||
const portainerData = '${PORTAINER_DATA:-/tmp/portainer}';
|
||||
const portainerRoot = process.env.PORTAINER_PROJECT ? process.env.PORTAINER_PROJECT : process.env.PWD;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js",
|
||||
"dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js",
|
||||
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt start:client",
|
||||
"dev:extension": "grunt build && grunt install:extension",
|
||||
"start:toolkit": "grunt start:toolkit",
|
||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||
"clean:all": "grunt clean:all",
|
||||
|
||||
Reference in New Issue
Block a user