Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
179df06267 | ||
|
|
0f5407da40 | ||
|
|
2fd95d87eb | ||
|
|
33b428eb7f | ||
|
|
c6b770d697 | ||
|
|
d48f6bd02c | ||
|
|
340805f880 | ||
|
|
f6c5c552aa | ||
|
|
90a472c08b | ||
|
|
8b80eb1731 | ||
|
|
d2404458ea | ||
|
|
6a39a5cf44 |
1
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -4,6 +4,7 @@ about: Create a bug report
|
||||
title: ''
|
||||
labels: bug/need-confirmation, kind/bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/Custom.md
vendored
2
.github/ISSUE_TEMPLATE/Custom.md
vendored
@@ -4,8 +4,8 @@ about: Ask us a question about Portainer usage or deployment
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
---
|
||||
Before you start, we need a little bit more information from you:
|
||||
|
||||
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
|
||||
|
||||
@@ -169,6 +169,7 @@ func (store *Store) MigrateData(force bool) error {
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
@@ -55,22 +55,6 @@ func (store *Store) Init() error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = store.DockerHubService.DockerHub()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
defaultDockerHub := &portainer.DockerHub{
|
||||
Authentication: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
|
||||
err := store.DockerHubService.UpdateDockerHub(defaultDockerHub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groups, err := store.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
41
api/bolt/log/log.go
Normal file
41
api/bolt/log/log.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
const (
|
||||
INFO = "INFO"
|
||||
ERROR = "ERROR"
|
||||
DEBUG = "DEBUG"
|
||||
FATAL = "FATAL"
|
||||
)
|
||||
|
||||
type ScopedLog struct {
|
||||
scope string
|
||||
}
|
||||
|
||||
func NewScopedLog(scope string) *ScopedLog {
|
||||
return &ScopedLog{scope: scope}
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) print(kind string, message string) {
|
||||
log.Printf("[%s] [%s] %s", kind, slog.scope, message)
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) Debug(message string) {
|
||||
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) Info(message string) {
|
||||
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) Error(message string, err error) {
|
||||
slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) NotImplemented(method string) {
|
||||
log.Fatalf("[%s] [%s] [%s]", FATAL, slog.scope, fmt.Sprintf("%s is not yet implemented", method))
|
||||
}
|
||||
1
api/bolt/log/log.test.go
Normal file
1
api/bolt/log/log.test.go
Normal file
@@ -0,0 +1 @@
|
||||
package log
|
||||
124
api/bolt/migrator/migrate_dbversion32.go
Normal file
124
api/bolt/migrator/migrate_dbversion32.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionTo32() error {
|
||||
err := m.updateRegistriesToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateDockerhubToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateRegistriesToDB32() error {
|
||||
registries, err := m.registryService.Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
|
||||
registry.RegistryAccesses = portainer.RegistryAccesses{}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
|
||||
filteredUserAccessPolicies := portainer.UserAccessPolicies{}
|
||||
for userId, registryPolicy := range registry.UserAccessPolicies {
|
||||
if _, found := endpoint.UserAccessPolicies[userId]; found {
|
||||
filteredUserAccessPolicies[userId] = registryPolicy
|
||||
}
|
||||
}
|
||||
|
||||
filteredTeamAccessPolicies := portainer.TeamAccessPolicies{}
|
||||
for teamId, registryPolicy := range registry.TeamAccessPolicies {
|
||||
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
|
||||
filteredTeamAccessPolicies[teamId] = registryPolicy
|
||||
}
|
||||
}
|
||||
|
||||
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
|
||||
UserAccessPolicies: filteredUserAccessPolicies,
|
||||
TeamAccessPolicies: filteredTeamAccessPolicies,
|
||||
Namespaces: []string{},
|
||||
}
|
||||
}
|
||||
m.registryService.UpdateRegistry(registry.ID, ®istry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateDockerhubToDB32() error {
|
||||
dockerhub, err := m.dockerhubService.DockerHub()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !dockerhub.Authentication {
|
||||
return nil
|
||||
}
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.DockerHubRegistry,
|
||||
Name: "Dockerhub (authenticated - migrated)",
|
||||
URL: "docker.io",
|
||||
Authentication: true,
|
||||
Username: dockerhub.Username,
|
||||
Password: dockerhub.Password,
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
}
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
|
||||
if endpoint.Type != portainer.KubernetesLocalEnvironment &&
|
||||
endpoint.Type != portainer.AgentOnKubernetesEnvironment &&
|
||||
endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
|
||||
userAccessPolicies := portainer.UserAccessPolicies{}
|
||||
for userId := range endpoint.UserAccessPolicies {
|
||||
if _, found := endpoint.UserAccessPolicies[userId]; found {
|
||||
userAccessPolicies[userId] = portainer.AccessPolicy{
|
||||
RoleID: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
teamAccessPolicies := portainer.TeamAccessPolicies{}
|
||||
for teamId := range endpoint.TeamAccessPolicies {
|
||||
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
|
||||
teamAccessPolicies[teamId] = portainer.AccessPolicy{
|
||||
RoleID: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
|
||||
UserAccessPolicies: userAccessPolicies,
|
||||
TeamAccessPolicies: teamAccessPolicies,
|
||||
Namespaces: []string{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m.registryService.CreateRegistry(registry)
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package migrator
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/dockerhub"
|
||||
"github.com/portainer/portainer/api/bolt/endpoint"
|
||||
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
||||
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
||||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
||||
"github.com/portainer/portainer/api/bolt/role"
|
||||
@@ -20,6 +22,8 @@ import (
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
|
||||
type (
|
||||
// Migrator defines a service to migrate data after a Portainer version update.
|
||||
Migrator struct {
|
||||
@@ -41,6 +45,7 @@ type (
|
||||
versionService *version.Service
|
||||
fileService portainer.FileService
|
||||
authorizationService *authorization.Service
|
||||
dockerhubService *dockerhub.Service
|
||||
}
|
||||
|
||||
// Parameters represents the required parameters to create a new Migrator instance.
|
||||
@@ -63,6 +68,7 @@ type (
|
||||
VersionService *version.Service
|
||||
FileService portainer.FileService
|
||||
AuthorizationService *authorization.Service
|
||||
DockerhubService *dockerhub.Service
|
||||
}
|
||||
)
|
||||
|
||||
@@ -87,6 +93,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||
versionService: parameters.VersionService,
|
||||
fileService: parameters.FileService,
|
||||
authorizationService: parameters.AuthorizationService,
|
||||
dockerhubService: parameters.DockerhubService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,5 +373,13 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.0
|
||||
if m.currentDBVersion < 32 {
|
||||
err := m.migrateDBVersionTo32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -167,11 +167,6 @@ func (store *Store) CustomTemplate() portainer.CustomTemplateService {
|
||||
return store.CustomTemplateService
|
||||
}
|
||||
|
||||
// DockerHub gives access to the DockerHub data management layer
|
||||
func (store *Store) DockerHub() portainer.DockerHubService {
|
||||
return store.DockerHubService
|
||||
}
|
||||
|
||||
// EdgeGroup gives access to the EdgeGroup data management layer
|
||||
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
|
||||
return store.EdgeGroupService
|
||||
|
||||
@@ -89,8 +89,8 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer
|
||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
|
||||
func initKubernetesDeployer(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(dataStore, reverseTunnelService, signatureService, assetsPath)
|
||||
}
|
||||
|
||||
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
|
||||
@@ -134,8 +134,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
|
||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *kubecli.ClientFactory {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID)
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore portainer.DataStore) *kubecli.ClientFactory {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
|
||||
}
|
||||
|
||||
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
|
||||
@@ -382,7 +382,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID)
|
||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
|
||||
if err != nil {
|
||||
@@ -402,7 +402,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
|
||||
kubernetesDeployer := initKubernetesDeployer(dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
|
||||
|
||||
if dataStore.IsNew() {
|
||||
err = updateSettingsFromFlags(dataStore, flags)
|
||||
|
||||
@@ -5,9 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -23,64 +20,27 @@ import (
|
||||
|
||||
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
|
||||
type KubernetesDeployer struct {
|
||||
binaryPath string
|
||||
dataStore portainer.DataStore
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
kubernetesClientFactory *cli.ClientFactory
|
||||
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
binaryPath string
|
||||
dataStore portainer.DataStore
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
}
|
||||
|
||||
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
|
||||
func NewKubernetesDeployer(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
|
||||
return &KubernetesDeployer{
|
||||
binaryPath: binaryPath,
|
||||
dataStore: datastore,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
signatureService: signatureService,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
binaryPath: binaryPath,
|
||||
dataStore: datastore,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
signatureService: signatureService,
|
||||
}
|
||||
}
|
||||
|
||||
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
|
||||
|
||||
tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return tokenManager.GetAdminServiceAccountToken(), nil
|
||||
}
|
||||
|
||||
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return "", fmt.Errorf("can not get a valid user service account token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
|
||||
// Otherwise it will use kubectl to deploy the manifest.
|
||||
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
||||
func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
||||
if endpoint.Type == portainer.KubernetesLocalEnvironment {
|
||||
token, err := deployer.getToken(request, endpoint, true);
|
||||
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -93,7 +53,7 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port
|
||||
args := make([]string, 0)
|
||||
args = append(args, "--server", endpoint.URL)
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
args = append(args, "--token", token)
|
||||
args = append(args, "--token", string(token))
|
||||
args = append(args, "--namespace", namespace)
|
||||
args = append(args, "apply", "-f", "-")
|
||||
|
||||
@@ -179,14 +139,8 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := deployer.getToken(request, endpoint, false);
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
|
||||
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||
|
||||
resp, err := httpCli.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// SwarmStackManager represents a service for managing stacks.
|
||||
@@ -42,7 +42,7 @@ func NewSwarmStackManager(binaryPath, dataPath string, signatureService portaine
|
||||
}
|
||||
|
||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
@@ -50,11 +50,6 @@ func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registri
|
||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
if dockerhub.Authentication {
|
||||
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
|
||||
runCommandAndCaptureStdErr(command, dockerhubArgs, nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
// Logout executes the docker logout command.
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package gittypes
|
||||
|
||||
type RepoConfig struct {
|
||||
// The repo url
|
||||
URL string `example:"https://github.com/portainer/portainer-ee.git"`
|
||||
// The reference name
|
||||
ReferenceName string `example:"refs/heads/branch_name"`
|
||||
// Path to where the config file is in this url/refName
|
||||
ConfigFilePath string `example:"docker-compose.yml"`
|
||||
URL string
|
||||
ReferenceName string
|
||||
ConfigFilePath string
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/portainer/portainer/api
|
||||
|
||||
go 1.13
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.16
|
||||
@@ -34,6 +34,7 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
|
||||
k8s.io/api v0.17.2
|
||||
k8s.io/apimachinery v0.17.2
|
||||
k8s.io/client-go v0.17.2
|
||||
|
||||
@@ -80,7 +80,6 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
@@ -154,7 +153,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
@@ -221,10 +219,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
@@ -370,11 +366,9 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
@@ -401,7 +395,6 @@ k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUc
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
|
||||
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
|
||||
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -133,6 +134,14 @@ func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User)
|
||||
return handler.persistAndWriteToken(w, composeTokenData(user))
|
||||
}
|
||||
|
||||
func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time) *httperror.HandlerError {
|
||||
token, err := handler.JWTService.GenerateTokenForOAuth(composeTokenData(user), expiryTime)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err}
|
||||
}
|
||||
return response.JSON(w, &authenticateResponse{JWT: token})
|
||||
}
|
||||
|
||||
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -25,21 +26,21 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
|
||||
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, *time.Time, error) {
|
||||
if code == "" {
|
||||
return "", errors.New("Invalid OAuth authorization code")
|
||||
return "", nil, errors.New("Invalid OAuth authorization code")
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
return "", errors.New("Invalid OAuth configuration")
|
||||
return "", nil, errors.New("Invalid OAuth configuration")
|
||||
}
|
||||
|
||||
username, err := handler.OAuthService.Authenticate(code, settings)
|
||||
username, expiryTime, err := handler.OAuthService.Authenticate(code, settings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return username, nil
|
||||
return username, expiryTime, nil
|
||||
}
|
||||
|
||||
// @id ValidateOAuth
|
||||
@@ -69,7 +70,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "OAuth authentication is not enabled", Err: errors.New("OAuth authentication is not enabled")}
|
||||
}
|
||||
|
||||
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
|
||||
username, expiryTime, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to authenticate through OAuth", Err: httperrors.ErrUnauthorized}
|
||||
@@ -110,5 +111,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
return handler.writeTokenForOAuth(w, user, expiryTime)
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package dockerhub
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id DockerHubInspect
|
||||
// @summary Retrieve DockerHub information
|
||||
// @description Use this endpoint to retrieve the information used to connect to the DockerHub
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags dockerhub
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {object} portainer.DockerHub
|
||||
// @failure 500 "Server error"
|
||||
// @router /dockerhub [get]
|
||||
func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
|
||||
}
|
||||
|
||||
hideFields(dockerhub)
|
||||
return response.JSON(w, dockerhub)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package dockerhub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type dockerhubUpdatePayload struct {
|
||||
// Enable authentication against DockerHub
|
||||
Authentication bool `validate:"required" example:"false"`
|
||||
// Username used to authenticate against the DockerHub
|
||||
Username string `validate:"required" example:"hub_user"`
|
||||
// Password used to authenticate against the DockerHub
|
||||
Password string `validate:"required" example:"hub_password"`
|
||||
}
|
||||
|
||||
func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
||||
return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id DockerHubUpdate
|
||||
// @summary Update DockerHub information
|
||||
// @description Use this endpoint to update the information used to connect to the DockerHub
|
||||
// @description **Access policy**: administrator
|
||||
// @tags dockerhub
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body dockerhubUpdatePayload true "DockerHub information"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /dockerhub [put]
|
||||
func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload dockerhubUpdatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
dockerhub := &portainer.DockerHub{
|
||||
Authentication: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
|
||||
if payload.Authentication {
|
||||
dockerhub.Authentication = true
|
||||
dockerhub.Username = payload.Username
|
||||
dockerhub.Password = payload.Password
|
||||
}
|
||||
|
||||
err = handler.DataStore.DockerHub().UpdateDockerHub(dockerhub)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package dockerhub
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
func hideFields(dockerHub *portainer.DockerHub) {
|
||||
dockerHub.Password = ""
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle DockerHub operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage Dockerhub operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -322,8 +322,8 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
|
||||
@@ -103,6 +103,22 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
|
||||
for idx := range registries {
|
||||
registry := ®istries[idx]
|
||||
if _, ok := registry.RegistryAccesses[endpoint.ID]; ok {
|
||||
delete(registry.RegistryAccesses, endpoint.ID)
|
||||
err = handler.DataStore.Registry().UpdateRegistry(registry.ID, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to update registry accesses", Err: err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ type dockerhubStatusResponse struct {
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// GET request on /api/endpoints/{id}/dockerhub/status
|
||||
// GET request on /api/endpoints/{id}/dockerhub/{registryId}
|
||||
func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
@@ -40,13 +40,30 @@ func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.R
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment type", errors.New("Invalid environment type")}
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
}
|
||||
|
||||
var registry *portainer.Registry
|
||||
|
||||
if registryID == 0 {
|
||||
registry = &portainer.Registry{}
|
||||
} else {
|
||||
registry, err = handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if registry.Type != portainer.DockerHubRegistry {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry type", errors.New("Invalid registry type")}
|
||||
}
|
||||
}
|
||||
|
||||
httpClient := client.NewHTTPClient()
|
||||
token, err := getDockerHubToken(httpClient, dockerhub)
|
||||
token, err := getDockerHubToken(httpClient, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub token from DockerHub", err}
|
||||
}
|
||||
@@ -59,7 +76,7 @@ func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.R
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
|
||||
func getDockerHubToken(httpClient *client.HTTPClient, dockerhub *portainer.DockerHub) (string, error) {
|
||||
func getDockerHubToken(httpClient *client.HTTPClient, registry *portainer.Registry) (string, error) {
|
||||
type dockerhubTokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
@@ -71,8 +88,8 @@ func getDockerHubToken(httpClient *client.HTTPClient, dockerhub *portainer.Docke
|
||||
return "", err
|
||||
}
|
||||
|
||||
if dockerhub.Authentication {
|
||||
req.SetBasicAuth(dockerhub.Username, dockerhub.Password)
|
||||
if registry.Authentication {
|
||||
req.SetBasicAuth(registry.Username, registry.Password)
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
@@ -115,18 +132,8 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub
|
||||
return nil, errors.New("failed fetching dockerhub limits")
|
||||
}
|
||||
|
||||
// An error with rateLimit-Limit or RateLimit-Remaining is likely for dockerhub pro accounts where there is no rate limit.
|
||||
// In that specific case the headers will not be present. Don't bubble up the error as its normal
|
||||
// See: https://docs.docker.com/docker-hub/download-rate-limit/
|
||||
rateLimit, err := parseRateLimitHeader(resp.Header, "RateLimit-Limit")
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rateLimitRemaining, err := parseRateLimitHeader(resp.Header, "RateLimit-Remaining")
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &dockerhubStatusResponse{
|
||||
Limit: rateLimit,
|
||||
|
||||
50
api/http/handler/endpoints/endpoint_registries_inspect.go
Normal file
50
api/http/handler/endpoints/endpoint_registries_inspect.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// GET request on /endpoints/{id}/registries/{registryId}
|
||||
func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid registry identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a registry with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a registry with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user from the database", Err: err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedRegistryAccess(registry, user, securityContext.UserMemberships, portainer.EndpointID(endpointID)) {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
hideRegistryFields(registry, !securityContext.IsAdmin)
|
||||
return response.JSON(w, registry)
|
||||
}
|
||||
130
api/http/handler/endpoints/endpoint_registries_list.go
Normal file
130
api/http/handler/endpoints/endpoint_registries_list.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
endpointutils "github.com/portainer/portainer/api/internal/endpoint"
|
||||
)
|
||||
|
||||
// GET request on /endpoints/{id}/registries?namespace
|
||||
func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user from the database", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
isAdmin := securityContext.IsAdmin
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
|
||||
if endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
|
||||
|
||||
if namespace == "" && !isAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Missing namespace query parameter", Err: errors.New("missing namespace query parameter")}
|
||||
}
|
||||
|
||||
if namespace != "" {
|
||||
|
||||
authorized, err := handler.isNamespaceAuthorized(endpoint, namespace, user.ID, securityContext.UserMemberships, isAdmin)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to check for namespace authorization", err}
|
||||
}
|
||||
|
||||
if !authorized {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized to use namespace", Err: errors.New("user is not authorized to use namespace")}
|
||||
}
|
||||
|
||||
registries = filterRegistriesByNamespace(registries, endpoint.ID, namespace)
|
||||
}
|
||||
|
||||
} else if !isAdmin {
|
||||
registries = security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
}
|
||||
|
||||
for idx := range registries {
|
||||
hideRegistryFields(®istries[idx], !isAdmin)
|
||||
}
|
||||
|
||||
return response.JSON(w, registries)
|
||||
}
|
||||
|
||||
func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, namespace string, userId portainer.UserID, memberships []portainer.TeamMembership, isAdmin bool) (bool, error) {
|
||||
if isAdmin || namespace == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if namespace == "default" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "unable to retrieve kubernetes client")
|
||||
}
|
||||
|
||||
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "unable to retrieve endpoint's namespaces policies")
|
||||
}
|
||||
|
||||
namespacePolicy, ok := accessPolicies[namespace]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return security.AuthorizedAccess(userId, memberships, namespacePolicy.UserAccessPolicies, namespacePolicy.TeamAccessPolicies), nil
|
||||
}
|
||||
|
||||
func filterRegistriesByNamespace(registries []portainer.Registry, endpointId portainer.EndpointID, namespace string) []portainer.Registry {
|
||||
if namespace == "" {
|
||||
return registries
|
||||
}
|
||||
|
||||
filteredRegistries := []portainer.Registry{}
|
||||
|
||||
for _, registry := range registries {
|
||||
for _, authorizedNamespace := range registry.RegistryAccesses[endpointId].Namespaces {
|
||||
if authorizedNamespace == namespace {
|
||||
filteredRegistries = append(filteredRegistries, registry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRegistries
|
||||
}
|
||||
|
||||
func hideRegistryFields(registry *portainer.Registry, hideAccesses bool) {
|
||||
registry.Password = ""
|
||||
registry.ManagementConfiguration = nil
|
||||
if hideAccesses {
|
||||
registry.RegistryAccesses = nil
|
||||
}
|
||||
}
|
||||
149
api/http/handler/endpoints/endpoint_registry_access.go
Normal file
149
api/http/handler/endpoints/endpoint_registry_access.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type registryAccessPayload struct {
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
Namespaces []string
|
||||
}
|
||||
|
||||
func (payload *registryAccessPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /endpoints/{id}/registries/{registryId}
|
||||
func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid registry identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized", Err: err}
|
||||
}
|
||||
|
||||
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
var payload registryAccessPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
if registry.RegistryAccesses == nil {
|
||||
registry.RegistryAccesses = portainer.RegistryAccesses{}
|
||||
}
|
||||
|
||||
if _, ok := registry.RegistryAccesses[endpoint.ID]; !ok {
|
||||
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{}
|
||||
}
|
||||
|
||||
registryAccess := registry.RegistryAccesses[endpoint.ID]
|
||||
|
||||
if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
err := handler.updateKubeAccess(endpoint, registry, registryAccess.Namespaces, payload.Namespaces)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to update kube access policies", Err: err}
|
||||
}
|
||||
|
||||
registryAccess.Namespaces = payload.Namespaces
|
||||
} else {
|
||||
registryAccess.UserAccessPolicies = payload.UserAccessPolicies
|
||||
registryAccess.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
}
|
||||
|
||||
registry.RegistryAccesses[portainer.EndpointID(endpointID)] = registryAccess
|
||||
|
||||
handler.DataStore.Registry().UpdateRegistry(registry.ID, registry)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateKubeAccess(endpoint *portainer.Endpoint, registry *portainer.Registry, oldNamespaces, newNamespaces []string) error {
|
||||
oldNamespacesSet := toSet(oldNamespaces)
|
||||
newNamespacesSet := toSet(newNamespaces)
|
||||
|
||||
namespacesToRemove := setDifference(oldNamespacesSet, newNamespacesSet)
|
||||
namespacesToAdd := setDifference(newNamespacesSet, oldNamespacesSet)
|
||||
|
||||
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for namespace := range namespacesToRemove {
|
||||
err := cli.DeleteRegistrySecret(registry, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for namespace := range namespacesToAdd {
|
||||
err := cli.CreateRegistrySecret(registry, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type stringSet map[string]bool
|
||||
|
||||
func toSet(list []string) stringSet {
|
||||
set := stringSet{}
|
||||
for _, el := range list {
|
||||
set[el] = true
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// setDifference returns the set difference tagsA - tagsB
|
||||
func setDifference(setA stringSet, setB stringSet) stringSet {
|
||||
set := stringSet{}
|
||||
|
||||
for el := range setA {
|
||||
if !setB[el] {
|
||||
set[el] = true
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
"net/http"
|
||||
|
||||
@@ -28,6 +29,7 @@ type Handler struct {
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
SnapshotService portainer.SnapshotService
|
||||
K8sClientFactory *cli.ClientFactory
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
AuthorizationService *authorization.Service
|
||||
}
|
||||
@@ -53,7 +55,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/dockerhub",
|
||||
h.Handle("/endpoints/{id}/dockerhub/{registryId}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}/extensions",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
@@ -63,5 +65,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}/registries",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistriesList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}/registries/{registryId}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistryInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}/registries/{registryId}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistryAccess))).Methods(http.MethodPut)
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/backup"
|
||||
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
||||
"github.com/portainer/portainer/api/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||
@@ -39,7 +38,6 @@ type Handler struct {
|
||||
AuthHandler *auth.Handler
|
||||
BackupHandler *backup.Handler
|
||||
CustomTemplatesHandler *customtemplates.Handler
|
||||
DockerHubHandler *dockerhub.Handler
|
||||
EdgeGroupsHandler *edgegroups.Handler
|
||||
EdgeJobsHandler *edgejobs.Handler
|
||||
EdgeStacksHandler *edgestacks.Handler
|
||||
@@ -67,7 +65,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.6.3
|
||||
// @version 2.1.1
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -88,8 +86,6 @@ type Handler struct {
|
||||
// @tag.description Authenticate against Portainer HTTP API
|
||||
// @tag.name custom_templates
|
||||
// @tag.description Manage Custom Templates
|
||||
// @tag.name dockerhub
|
||||
// @tag.description Manage how Portainer connects to the DockerHub
|
||||
// @tag.name edge_groups
|
||||
// @tag.description Manage Edge Groups
|
||||
// @tag.name edge_jobs
|
||||
@@ -146,8 +142,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.BackupHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/restore"):
|
||||
http.StripPrefix("/api", h.BackupHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
||||
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/custom_templates"):
|
||||
http.StripPrefix("/api", h.CustomTemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"):
|
||||
|
||||
@@ -5,32 +5,46 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
func hideFields(registry *portainer.Registry) {
|
||||
func hideFields(registry *portainer.Registry, hideAccesses bool) {
|
||||
registry.Password = ""
|
||||
registry.ManagementConfiguration = nil
|
||||
if hideAccesses {
|
||||
registry.RegistryAccesses = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle registry operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
K8sClientFactory *cli.ClientFactory
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage registry operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
h := newHandler(bouncer)
|
||||
h.initRouter(bouncer)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func newHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
return &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) initRouter(bouncer accessGuard) {
|
||||
h.Handle("/registries",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/registries",
|
||||
@@ -45,5 +59,21 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||
h.PathPrefix("/registries/proxies/gitlab").Handler(
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithoutRegistry)))
|
||||
return h
|
||||
}
|
||||
|
||||
type accessGuard interface {
|
||||
AdminAccess(h http.Handler) http.Handler
|
||||
RestrictedAccess(h http.Handler) http.Handler
|
||||
AuthenticatedAccess(h http.Handler) http.Handler
|
||||
}
|
||||
|
||||
func (handler *Handler) registriesHaveSameURLAndCredentials(r1, r2 *portainer.Registry) bool {
|
||||
hasSameUrl := r1.URL == r2.URL
|
||||
hasSameCredentials := r1.Authentication == r2.Authentication && (!r1.Authentication || (r1.Authentication && r1.Username == r2.Username))
|
||||
|
||||
if r1.Type != portainer.GitlabRegistry || r2.Type != portainer.GitlabRegistry {
|
||||
return hasSameUrl && hasSameCredentials
|
||||
}
|
||||
|
||||
return hasSameUrl && hasSameCredentials && r1.Gitlab.ProjectPath == r2.Gitlab.ProjectPath
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type registryConfigurePayload struct {
|
||||
@@ -93,9 +95,12 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
|
||||
// @failure 500 "Server error"
|
||||
// @router /registries/{id}/configure [post]
|
||||
func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to configure registry", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
payload := ®istryConfigurePayload{}
|
||||
@@ -104,6 +109,11 @@ func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
}
|
||||
|
||||
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
|
||||
@@ -2,6 +2,7 @@ package registries
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
@@ -9,15 +10,19 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type registryCreatePayload struct {
|
||||
// Name that will be used to identify this registry
|
||||
Name string `example:"my-registry" validate:"required"`
|
||||
// Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)
|
||||
Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4"`
|
||||
// Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry), 5 (ProGet registry) or 6 (DockerHub)
|
||||
Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4,5,6"`
|
||||
// URL or IP address of the Docker registry
|
||||
URL string `example:"registry.mydomain.tld:2375" validate:"required"`
|
||||
URL string `example:"registry.mydomain.tld:2375/feed" validate:"required"`
|
||||
// BaseURL required for ProGet registry
|
||||
BaseURL string `example:"registry.mydomain.tld:2375"`
|
||||
// Is authentication against this registry enabled
|
||||
Authentication bool `example:"false" validate:"required"`
|
||||
// Username used to authenticate against this registry. Required when Authentication is true
|
||||
@@ -30,7 +35,7 @@ type registryCreatePayload struct {
|
||||
Quay portainer.QuayRegistryData
|
||||
}
|
||||
|
||||
func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||
func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("Invalid registry name")
|
||||
}
|
||||
@@ -40,9 +45,17 @@ func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
||||
return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if payload.Type != portainer.QuayRegistry && payload.Type != portainer.AzureRegistry && payload.Type != portainer.CustomRegistry && payload.Type != portainer.GitlabRegistry {
|
||||
return errors.New("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)")
|
||||
|
||||
switch payload.Type {
|
||||
case portainer.QuayRegistry, portainer.AzureRegistry, portainer.CustomRegistry, portainer.GitlabRegistry, portainer.ProGetRegistry, portainer.DockerHubRegistry:
|
||||
default:
|
||||
return errors.New("invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry), 5 (ProGet registry) or 6 (DockerHub)")
|
||||
}
|
||||
|
||||
if payload.Type == portainer.ProGetRegistry && payload.BaseURL == "" {
|
||||
return fmt.Errorf("BaseURL is required for registry type %d (ProGet)", portainer.ProGetRegistry)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -60,23 +73,41 @@ func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||
// @failure 500 "Server error"
|
||||
// @router /registries [post]
|
||||
func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create registry", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
var payload registryCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.RegistryType(payload.Type),
|
||||
Name: payload.Name,
|
||||
URL: payload.URL,
|
||||
Authentication: payload.Authentication,
|
||||
Username: payload.Username,
|
||||
Password: payload.Password,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Gitlab: payload.Gitlab,
|
||||
Quay: payload.Quay,
|
||||
Type: portainer.RegistryType(payload.Type),
|
||||
Name: payload.Name,
|
||||
URL: payload.URL,
|
||||
BaseURL: payload.BaseURL,
|
||||
Authentication: payload.Authentication,
|
||||
Username: payload.Username,
|
||||
Password: payload.Password,
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
Gitlab: payload.Gitlab,
|
||||
Quay: payload.Quay,
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
for _, r := range registries {
|
||||
if handler.registriesHaveSameURLAndCredentials(&r, registry) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials")}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Registry().CreateRegistry(registry)
|
||||
@@ -84,6 +115,6 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the registry inside the database", err}
|
||||
}
|
||||
|
||||
hideFields(registry)
|
||||
hideFields(registry, true)
|
||||
return response.JSON(w, registry)
|
||||
}
|
||||
|
||||
113
api/http/handler/registries/registry_create_test.go
Normal file
113
api/http/handler/registries/registry_create_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_registryCreatePayload_Validate(t *testing.T) {
|
||||
basePayload := registryCreatePayload{Name: "Test registry", URL: "http://example.com"}
|
||||
t.Run("Can't create a ProGet registry if BaseURL is empty", func(t *testing.T) {
|
||||
payload := basePayload
|
||||
payload.Type = portainer.ProGetRegistry
|
||||
err := payload.Validate(nil)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("Can create a GitLab registry if BaseURL is empty", func(t *testing.T) {
|
||||
payload := basePayload
|
||||
payload.Type = portainer.GitlabRegistry
|
||||
err := payload.Validate(nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("Can create a ProGet registry if BaseURL is not empty", func(t *testing.T) {
|
||||
payload := basePayload
|
||||
payload.Type = portainer.ProGetRegistry
|
||||
payload.BaseURL = "http://example.com"
|
||||
err := payload.Validate(nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
type testRegistryService struct {
|
||||
portainer.RegistryService
|
||||
createRegistry func(r *portainer.Registry) error
|
||||
updateRegistry func(ID portainer.RegistryID, r *portainer.Registry) error
|
||||
getRegistry func(ID portainer.RegistryID) (*portainer.Registry, error)
|
||||
}
|
||||
|
||||
type testDataStore struct {
|
||||
portainer.DataStore
|
||||
registry *testRegistryService
|
||||
}
|
||||
|
||||
func (t testDataStore) Registry() portainer.RegistryService {
|
||||
return t.registry
|
||||
}
|
||||
|
||||
func (t testRegistryService) CreateRegistry(r *portainer.Registry) error {
|
||||
return t.createRegistry(r)
|
||||
}
|
||||
|
||||
func (t testRegistryService) UpdateRegistry(ID portainer.RegistryID, r *portainer.Registry) error {
|
||||
return t.updateRegistry(ID, r)
|
||||
}
|
||||
|
||||
func (t testRegistryService) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
return t.getRegistry(ID)
|
||||
}
|
||||
|
||||
func (t testRegistryService) Registries() ([]portainer.Registry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestHandler_registryCreate(t *testing.T) {
|
||||
payload := registryCreatePayload{
|
||||
Name: "Test registry",
|
||||
Type: portainer.ProGetRegistry,
|
||||
URL: "http://example.com",
|
||||
BaseURL: "http://example.com",
|
||||
Authentication: false,
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
Gitlab: portainer.GitlabRegistryData{},
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
assert.NoError(t, err)
|
||||
r := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payloadBytes))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
restrictedContext := &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
UserID: portainer.UserID(1),
|
||||
}
|
||||
|
||||
ctx := security.StoreRestrictedRequestContext(r, restrictedContext)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
registry := portainer.Registry{}
|
||||
handler := Handler{}
|
||||
handler.DataStore = testDataStore{
|
||||
registry: &testRegistryService{
|
||||
createRegistry: func(r *portainer.Registry) error {
|
||||
registry = *r
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
handlerError := handler.registryCreate(w, r)
|
||||
assert.Nil(t, handlerError)
|
||||
assert.Equal(t, payload.Name, registry.Name)
|
||||
assert.Equal(t, payload.Type, registry.Type)
|
||||
assert.Equal(t, payload.URL, registry.URL)
|
||||
assert.Equal(t, payload.BaseURL, registry.BaseURL)
|
||||
assert.Equal(t, payload.Authentication, registry.Authentication)
|
||||
assert.Equal(t, payload.Username, registry.Username)
|
||||
assert.Equal(t, payload.Password, registry.Password)
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// @id RegistryDelete
|
||||
@@ -23,6 +25,14 @@ import (
|
||||
// @failure 500 "Server error"
|
||||
// @router /registries/{id} [delete]
|
||||
func (handler *Handler) registryDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete registry", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -27,6 +26,7 @@ import (
|
||||
// @failure 500 "Server error"
|
||||
// @router /registries/{id} [get]
|
||||
func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
@@ -39,11 +39,6 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied}
|
||||
}
|
||||
|
||||
hideFields(registry)
|
||||
hideFields(registry, false)
|
||||
return response.JSON(w, registry)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -21,21 +22,18 @@ import (
|
||||
// @failure 500 "Server error"
|
||||
// @router /registries [get]
|
||||
func (handler *Handler) registryList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to list registries, use /endpoints/:endpointId/registries route instead", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||
|
||||
for idx := range filteredRegistries {
|
||||
hideFields(&filteredRegistries[idx])
|
||||
}
|
||||
|
||||
return response.JSON(w, filteredRegistries)
|
||||
return response.JSON(w, registries)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type registryUpdatePayload struct {
|
||||
@@ -16,15 +18,16 @@ type registryUpdatePayload struct {
|
||||
Name *string `validate:"required" example:"my-registry"`
|
||||
// URL or IP address of the Docker registry
|
||||
URL *string `validate:"required" example:"registry.mydomain.tld:2375"`
|
||||
// BaseURL is used for quay registry
|
||||
BaseURL *string `json:",omitempty" example:"registry.mydomain.tld:2375"`
|
||||
// Is authentication against this registry enabled
|
||||
Authentication *bool `example:"false" validate:"required"`
|
||||
// Username used to authenticate against this registry. Required when Authentication is true
|
||||
Username *string `example:"registry_user"`
|
||||
// Password used to authenticate against this registry. required when Authentication is true
|
||||
Password *string `example:"registry_password"`
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
Quay *portainer.QuayRegistryData
|
||||
Password *string `example:"registry_password"`
|
||||
RegistryAccesses *portainer.RegistryAccesses
|
||||
Quay *portainer.QuayRegistryData
|
||||
}
|
||||
|
||||
func (payload *registryUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -48,17 +51,19 @@ func (payload *registryUpdatePayload) Validate(r *http.Request) error {
|
||||
// @failure 500 "Server error"
|
||||
// @router /registries/{id} [put]
|
||||
func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update registry", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload registryUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
@@ -66,27 +71,26 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var payload registryUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
if payload.Name != nil {
|
||||
registry.Name = *payload.Name
|
||||
}
|
||||
|
||||
if payload.URL != nil {
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
for _, r := range registries {
|
||||
if r.ID != registry.ID && hasSameURL(&r, registry) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", errors.New("A registry is already defined for this URL")}
|
||||
}
|
||||
}
|
||||
shouldUpdateSecrets := false
|
||||
|
||||
registry.URL = *payload.URL
|
||||
if registry.Type == portainer.ProGetRegistry && payload.BaseURL != nil {
|
||||
registry.BaseURL = *payload.BaseURL
|
||||
}
|
||||
|
||||
if payload.Authentication != nil {
|
||||
if *payload.Authentication {
|
||||
registry.Authentication = true
|
||||
shouldUpdateSecrets = shouldUpdateSecrets || (payload.Username != nil && *payload.Username != registry.Username) || (payload.Password != nil && *payload.Password != registry.Password)
|
||||
|
||||
if payload.Username != nil {
|
||||
registry.Username = *payload.Username
|
||||
@@ -103,12 +107,35 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
}
|
||||
|
||||
if payload.UserAccessPolicies != nil {
|
||||
registry.UserAccessPolicies = payload.UserAccessPolicies
|
||||
if payload.URL != nil {
|
||||
shouldUpdateSecrets = shouldUpdateSecrets || (*payload.URL != registry.URL)
|
||||
|
||||
registry.URL = *payload.URL
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
for _, r := range registries {
|
||||
if r.ID != registry.ID && handler.registriesHaveSameURLAndCredentials(&r, registry) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials")}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if payload.TeamAccessPolicies != nil {
|
||||
registry.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
if shouldUpdateSecrets {
|
||||
for endpointID, endpointAccess := range registry.RegistryAccesses {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
err = handler.updateEndpointRegistryAccess(endpoint, registry, endpointAccess)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Quay != nil {
|
||||
@@ -123,10 +150,24 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
return response.JSON(w, registry)
|
||||
}
|
||||
|
||||
func hasSameURL(r1, r2 *portainer.Registry) bool {
|
||||
if r1.Type != portainer.GitlabRegistry || r2.Type != portainer.GitlabRegistry {
|
||||
return r1.URL == r2.URL
|
||||
func (handler *Handler) updateEndpointRegistryAccess(endpoint *portainer.Endpoint, registry *portainer.Registry, endpointAccess portainer.RegistryAccessPolicies) error {
|
||||
|
||||
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r1.URL == r2.URL && r1.Gitlab.ProjectPath == r2.Gitlab.ProjectPath
|
||||
for _, namespace := range endpointAccess.Namespaces {
|
||||
err := cli.DeleteRegistrySecret(registry, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cli.CreateRegistrySecret(registry, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
88
api/http/handler/registries/registry_update_test.go
Normal file
88
api/http/handler/registries/registry_update_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ps(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func pb(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
type TestBouncer struct{}
|
||||
|
||||
func (t TestBouncer) AdminAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (t TestBouncer) RestrictedAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (t TestBouncer) AuthenticatedAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func TestHandler_registryUpdate(t *testing.T) {
|
||||
payload := registryUpdatePayload{
|
||||
Name: ps("Updated test registry"),
|
||||
URL: ps("http://example.org/feed"),
|
||||
BaseURL: ps("http://example.org"),
|
||||
Authentication: pb(true),
|
||||
Username: ps("username"),
|
||||
Password: ps("password"),
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
assert.NoError(t, err)
|
||||
registry := portainer.Registry{Type: portainer.ProGetRegistry, ID: 5}
|
||||
r := httptest.NewRequest(http.MethodPut, "/registries/5", bytes.NewReader(payloadBytes))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
restrictedContext := &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
UserID: portainer.UserID(1),
|
||||
}
|
||||
|
||||
ctx := security.StoreRestrictedRequestContext(r, restrictedContext)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
updatedRegistry := portainer.Registry{}
|
||||
handler := newHandler(nil)
|
||||
handler.initRouter(TestBouncer{})
|
||||
handler.DataStore = testDataStore{
|
||||
registry: &testRegistryService{
|
||||
getRegistry: func(_ portainer.RegistryID) (*portainer.Registry, error) {
|
||||
return ®istry, nil
|
||||
},
|
||||
updateRegistry: func(ID portainer.RegistryID, r *portainer.Registry) error {
|
||||
assert.Equal(t, ID, r.ID)
|
||||
updatedRegistry = *r
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handler.Router.ServeHTTP(w, r)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
// Registry type should remain intact
|
||||
assert.Equal(t, registry.Type, updatedRegistry.Type)
|
||||
|
||||
assert.Equal(t, *payload.Name, updatedRegistry.Name)
|
||||
assert.Equal(t, *payload.URL, updatedRegistry.URL)
|
||||
assert.Equal(t, *payload.BaseURL, updatedRegistry.BaseURL)
|
||||
assert.Equal(t, *payload.Authentication, updatedRegistry.Authentication)
|
||||
assert.Equal(t, *payload.Username, updatedRegistry.Username)
|
||||
assert.Equal(t, *payload.Password, updatedRegistry.Password)
|
||||
|
||||
}
|
||||
@@ -290,7 +290,6 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
type composeStackDeploymentConfig struct {
|
||||
stack *portainer.Stack
|
||||
endpoint *portainer.Endpoint
|
||||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
isAdmin bool
|
||||
user *portainer.User
|
||||
@@ -302,26 +301,20 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve DockerHub details from the database", Err: err}
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve registries from the database", Err: err}
|
||||
}
|
||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
||||
}
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
config := &composeStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
@@ -366,7 +359,7 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
|
||||
handler.SwarmStackManager.Login(config.registries, config.endpoint)
|
||||
|
||||
err = handler.ComposeStackManager.Up(config.stack, config.endpoint)
|
||||
if err != nil {
|
||||
|
||||
@@ -95,7 +95,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
output, err := handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace)
|
||||
output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
|
||||
}
|
||||
|
||||
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
|
||||
output, err := handler.deployKubernetesStack(endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
|
||||
func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) {
|
||||
func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) {
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
@@ -167,7 +167,7 @@ func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *p
|
||||
stackConfig = string(convertedConfig)
|
||||
}
|
||||
|
||||
return handler.KubernetesDeployer.Deploy(request, endpoint, stackConfig, namespace)
|
||||
return handler.KubernetesDeployer.Deploy(endpoint, stackConfig, namespace)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -300,7 +300,6 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
type swarmStackDeploymentConfig struct {
|
||||
stack *portainer.Stack
|
||||
endpoint *portainer.Endpoint
|
||||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
prune bool
|
||||
isAdmin bool
|
||||
@@ -313,26 +312,20 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
config := &swarmStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: prune,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
@@ -367,7 +360,7 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
|
||||
handler.SwarmStackManager.Login(config.registries, config.endpoint)
|
||||
|
||||
err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 404 " not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id} [delete]
|
||||
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 404 " not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id}/start [post]
|
||||
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 404 " not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id}/stop [post]
|
||||
func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -33,23 +33,7 @@ func (payload *updateStackGitPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id StackUpdateGit
|
||||
// @summary Redeploy a stack
|
||||
// @description Pull and redeploy a stack via Git
|
||||
// @description **Access policy**: restricted
|
||||
// @tags stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Stack identifier"
|
||||
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
|
||||
// @param body body updateStackGitPayload true "Git configs for pull and redeploy a stack"
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/{id}/git [put]
|
||||
// PUT request on /api/stacks/:id/git?endpointId=<endpointId>
|
||||
func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -3,11 +3,12 @@ package teams
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
// @id TeamDelete
|
||||
@@ -29,7 +30,7 @@ func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *http
|
||||
}
|
||||
|
||||
_, err = handler.DataStore.Team().Team(portainer.TeamID(teamID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team with the specified identifier inside the database", err}
|
||||
@@ -45,5 +46,27 @@ func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *http
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete associated team memberships from the database", err}
|
||||
}
|
||||
|
||||
// update default team if deleted team was default
|
||||
err = handler.updateDefaultTeamIfDeleted(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to reset default team", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
// updateDefaultTeamIfDeleted resets the default team to nil if default team was the deleted team
|
||||
func (handler *Handler) updateDefaultTeamIfDeleted(teamID portainer.TeamID) error {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch settings")
|
||||
}
|
||||
|
||||
if teamID != settings.OAuthSettings.DefaultTeamID {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings.OAuthSettings.DefaultTeamID = 0
|
||||
err = handler.DataStore.Settings().UpdateSettings(settings)
|
||||
return errors.Wrap(err, "failed to update settings")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
@@ -13,22 +12,20 @@ import (
|
||||
// Handler is the HTTP handler used to handle websocket operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
KubernetesClientFactory *cli.ClientFactory
|
||||
requestBouncer *security.RequestBouncer
|
||||
connectionUpgrader websocket.Upgrader
|
||||
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
DataStore portainer.DataStore
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
KubernetesClientFactory *cli.ClientFactory
|
||||
requestBouncer *security.RequestBouncer
|
||||
connectionUpgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage websocket operations.
|
||||
func NewHandler(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, bouncer *security.RequestBouncer) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
connectionUpgrader: websocket.Upgrader{},
|
||||
requestBouncer: bouncer,
|
||||
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
}
|
||||
h.PathPrefix("/websocket/exec").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -13,7 +11,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
)
|
||||
|
||||
// @summary Execute a websocket on pod
|
||||
@@ -73,14 +70,8 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
token, useAdminToken, err := handler.getToken(r, endpoint, false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get user service account token", err}
|
||||
}
|
||||
|
||||
params := &webSocketRequestParams{
|
||||
endpoint: endpoint,
|
||||
token: token,
|
||||
}
|
||||
|
||||
r.Header.Del("Origin")
|
||||
@@ -121,7 +112,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
|
||||
}
|
||||
|
||||
err = cli.StartExecProcess(token, useAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
|
||||
err = cli.StartExecProcess(namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
|
||||
}
|
||||
@@ -133,37 +124,3 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, bool, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
kubecli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
tokenCache := handler.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
|
||||
|
||||
tokenManager, err := kubernetes.NewTokenManager(kubecli, handler.DataStore, tokenCache, setLocalAdminToken)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return tokenManager.GetAdminServiceAccountToken(), true, nil
|
||||
}
|
||||
|
||||
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return "", false, fmt.Errorf("can not get a valid user service account token")
|
||||
}
|
||||
|
||||
return token, false, nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
|
||||
|
||||
proxy.Director = func(incoming *http.Request, out http.Header) {
|
||||
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
|
||||
@@ -65,7 +64,6 @@ func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *htt
|
||||
out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey())
|
||||
out.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
@@ -8,5 +8,4 @@ type webSocketRequestParams struct {
|
||||
ID string
|
||||
nodeName string
|
||||
endpoint *portainer.Endpoint
|
||||
token string
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
)
|
||||
|
||||
// proxy for /subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/*
|
||||
@@ -49,7 +49,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request)
|
||||
errObj := map[string]string{
|
||||
"message": "A container instance with the same name already exists inside the selected resource group",
|
||||
}
|
||||
err = responseutils.RewriteResponse(resp, errObj, http.StatusConflict)
|
||||
err = utils.RewriteResponse(resp, errObj, http.StatusConflict)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request)
|
||||
return response, err
|
||||
}
|
||||
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request)
|
||||
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
|
||||
err = responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
err = utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func (transport *Transport) proxyContainerGroupGetRequest(request *http.Request)
|
||||
return response, err
|
||||
}
|
||||
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -106,7 +106,7 @@ func (transport *Transport) proxyContainerGroupGetRequest(request *http.Request)
|
||||
|
||||
responseObject = transport.decorateContainerGroup(responseObject, context)
|
||||
|
||||
responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func (transport *Transport) proxyContainerGroupDeleteRequest(request *http.Reque
|
||||
}
|
||||
|
||||
if !transport.userCanDeleteContainerGroup(request, context) {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
|
||||
response, err := http.DefaultTransport.RoundTrip(request)
|
||||
@@ -126,14 +126,14 @@ func (transport *Transport) proxyContainerGroupDeleteRequest(request *http.Reque
|
||||
return response, err
|
||||
}
|
||||
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport.removeResourceControl(responseObject, context)
|
||||
|
||||
responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
)
|
||||
|
||||
// proxy for /subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups
|
||||
@@ -23,7 +23,7 @@ func (transport *Transport) proxyContainerGroupsGetRequest(request *http.Request
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,10 +39,10 @@ func (transport *Transport) proxyContainerGroupsGetRequest(request *http.Request
|
||||
filteredValue := transport.filterContainerGroups(decoratedValue, context)
|
||||
responseObject["value"] = filteredValue
|
||||
|
||||
responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
} else {
|
||||
return nil, fmt.Errorf("The container groups response has no value property")
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -162,7 +162,7 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
|
||||
systemResourceControl := findSystemNetworkResourceControl(responseObject)
|
||||
if systemResourceControl != nil {
|
||||
responseObject = decorateObject(responseObject, systemResourceControl)
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,15 +175,15 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
|
||||
}
|
||||
|
||||
if resourceControl == nil && (executor.operationContext.isAdmin) {
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin || (resourceControl != nil && authorization.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
return responseutils.RewriteAccessDeniedResponse(response)
|
||||
return utils.RewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
func (transport *Transport) applyAccessControlOnResourceList(parameters *resourceOperationParameters, resourceData []interface{}, executor *operationExecutor) ([]interface{}, error) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, en
|
||||
func (transport *Transport) configListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ConfigList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
responseArray, err := utils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (transport *Transport) configListOperation(response *http.Response, executo
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// configInspectOperation extracts the response as a JSON object, verify that the user
|
||||
@@ -58,7 +58,7 @@ func (transport *Transport) configListOperation(response *http.Response, executo
|
||||
func (transport *Transport) configInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ConfigInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,9 +78,9 @@ func (transport *Transport) configInspectOperation(response *http.Response, exec
|
||||
// https://docs.docker.com/engine/api/v1.37/#operation/ConfigList
|
||||
// https://docs.docker.com/engine/api/v1.37/#operation/ConfigInspect
|
||||
func selectorConfigLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
|
||||
secretSpec := utils.GetJSONObject(responseObject, "Spec")
|
||||
if secretSpec != nil {
|
||||
secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels")
|
||||
secretLabelsObject := utils.GetJSONObject(secretSpec, "Labels")
|
||||
return secretLabelsObject
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -46,7 +46,7 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client,
|
||||
func (transport *Transport) containerListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ContainerList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
responseArray, err := utils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func (transport *Transport) containerListOperation(response *http.Response, exec
|
||||
}
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// containerInspectOperation extracts the response as a JSON object, verify that the user
|
||||
@@ -77,7 +77,7 @@ func (transport *Transport) containerListOperation(response *http.Response, exec
|
||||
func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
//ContainerInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,9 +96,9 @@ func (transport *Transport) containerInspectOperation(response *http.Response, e
|
||||
// Labels are available under the "Config.Labels" property.
|
||||
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
func selectorContainerLabelsFromContainerInspectOperation(responseObject map[string]interface{}) map[string]interface{} {
|
||||
containerConfigObject := responseutils.GetJSONObject(responseObject, "Config")
|
||||
containerConfigObject := utils.GetJSONObject(responseObject, "Config")
|
||||
if containerConfigObject != nil {
|
||||
containerLabelsObject := responseutils.GetJSONObject(containerConfigObject, "Labels")
|
||||
containerLabelsObject := utils.GetJSONObject(containerConfigObject, "Labels")
|
||||
return containerLabelsObject
|
||||
}
|
||||
return nil
|
||||
@@ -109,7 +109,7 @@ func selectorContainerLabelsFromContainerInspectOperation(responseObject map[str
|
||||
// Labels are available under the "Labels" property.
|
||||
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
func selectorContainerLabelsFromContainerListOperation(responseObject map[string]interface{}) map[string]interface{} {
|
||||
containerLabelsObject := responseutils.GetJSONObject(responseObject, "Labels")
|
||||
containerLabelsObject := utils.GetJSONObject(responseObject, "Labels")
|
||||
return containerLabelsObject
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, e
|
||||
func (transport *Transport) networkListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// NetworkList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
responseArray, err := utils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (transport *Transport) networkListOperation(response *http.Response, execut
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// networkInspectOperation extracts the response as a JSON object, verify that the user
|
||||
@@ -62,7 +62,7 @@ func (transport *Transport) networkListOperation(response *http.Response, execut
|
||||
func (transport *Transport) networkInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// NetworkInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -99,5 +99,5 @@ func findSystemNetworkResourceControl(networkObject map[string]interface{}) *por
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
func selectorNetworkLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
return responseutils.GetJSONObject(responseObject, "Labels")
|
||||
return utils.GetJSONObject(responseObject, "Labels")
|
||||
}
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type (
|
||||
registryAccessContext struct {
|
||||
isAdmin bool
|
||||
userID portainer.UserID
|
||||
user *portainer.User
|
||||
endpointID portainer.EndpointID
|
||||
teamMemberships []portainer.TeamMembership
|
||||
registries []portainer.Registry
|
||||
dockerHub *portainer.DockerHub
|
||||
}
|
||||
|
||||
registryAuthenticationHeader struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Serveraddress string `json:"serveraddress"`
|
||||
}
|
||||
|
||||
portainerRegistryAuthenticationHeader struct {
|
||||
RegistryId portainer.RegistryID `json:"registryId"`
|
||||
}
|
||||
)
|
||||
|
||||
func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
||||
func createRegistryAuthenticationHeader(registryId portainer.RegistryID, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
||||
var authenticationHeader *registryAuthenticationHeader
|
||||
|
||||
if serverAddress == "" {
|
||||
if registryId == 0 { // dockerhub (anonymous)
|
||||
authenticationHeader = ®istryAuthenticationHeader{
|
||||
Username: accessContext.dockerHub.Username,
|
||||
Password: accessContext.dockerHub.Password,
|
||||
Serveraddress: "docker.io",
|
||||
}
|
||||
} else {
|
||||
} else { // any "custom" registry
|
||||
var matchingRegistry *portainer.Registry
|
||||
for _, registry := range accessContext.registries {
|
||||
if registry.URL == serverAddress &&
|
||||
(accessContext.isAdmin || (!accessContext.isAdmin && security.AuthorizedRegistryAccess(®istry, accessContext.userID, accessContext.teamMemberships))) {
|
||||
if registry.ID == registryId &&
|
||||
(accessContext.isAdmin ||
|
||||
security.AuthorizedRegistryAccess(®istry, accessContext.user, accessContext.teamMemberships, accessContext.endpointID)) {
|
||||
matchingRegistry = ®istry
|
||||
break
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, en
|
||||
func (transport *Transport) secretListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// SecretList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
responseArray, err := utils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (transport *Transport) secretListOperation(response *http.Response, executo
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// secretInspectOperation extracts the response as a JSON object, verify that the user
|
||||
@@ -58,7 +58,7 @@ func (transport *Transport) secretListOperation(response *http.Response, executo
|
||||
func (transport *Transport) secretInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// SecretInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,9 +78,9 @@ func (transport *Transport) secretInspectOperation(response *http.Response, exec
|
||||
// https://docs.docker.com/engine/api/v1.37/#operation/SecretList
|
||||
// https://docs.docker.com/engine/api/v1.37/#operation/SecretInspect
|
||||
func selectorSecretLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
|
||||
secretSpec := utils.GetJSONObject(responseObject, "Spec")
|
||||
if secretSpec != nil {
|
||||
secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels")
|
||||
secretLabelsObject := utils.GetJSONObject(secretSpec, "Labels")
|
||||
return secretLabelsObject
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, e
|
||||
func (transport *Transport) serviceListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ServiceList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
responseArray, err := utils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func (transport *Transport) serviceListOperation(response *http.Response, execut
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// serviceInspectOperation extracts the response as a JSON object, verify that the user
|
||||
@@ -63,7 +63,7 @@ func (transport *Transport) serviceListOperation(response *http.Response, execut
|
||||
func (transport *Transport) serviceInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
//ServiceInspect response is a JSON object
|
||||
//https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -83,9 +83,9 @@ func (transport *Transport) serviceInspectOperation(response *http.Response, exe
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
func selectorServiceLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
serviceSpecObject := responseutils.GetJSONObject(responseObject, "Spec")
|
||||
serviceSpecObject := utils.GetJSONObject(responseObject, "Spec")
|
||||
if serviceSpecObject != nil {
|
||||
return responseutils.GetJSONObject(serviceSpecObject, "Labels")
|
||||
return utils.GetJSONObject(serviceSpecObject, "Labels")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package docker
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
)
|
||||
|
||||
// swarmInspectOperation extracts the response as a JSON object and rewrites the response based
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func swarmInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// SwarmInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -21,5 +21,5 @@ func swarmInspectOperation(response *http.Response, executor *operationExecutor)
|
||||
delete(responseObject, "TLSInfo")
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
func (transport *Transport) taskListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// TaskList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
responseArray, err := utils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -32,18 +32,18 @@ func (transport *Transport) taskListOperation(response *http.Response, executor
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// selectorServiceLabels retrieve the labels object associated to the task object.
|
||||
// Labels are available under the "Spec.ContainerSpec.Labels" property.
|
||||
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
func selectorTaskLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
taskSpecObject := responseutils.GetJSONObject(responseObject, "Spec")
|
||||
taskSpecObject := utils.GetJSONObject(responseObject, "Spec")
|
||||
if taskSpecObject != nil {
|
||||
containerSpecObject := responseutils.GetJSONObject(taskSpecObject, "ContainerSpec")
|
||||
containerSpecObject := utils.GetJSONObject(taskSpecObject, "ContainerSpec")
|
||||
if containerSpecObject != nil {
|
||||
return responseutils.GetJSONObject(containerSpecObject, "Labels")
|
||||
return utils.GetJSONObject(containerSpecObject, "Labels")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -5,17 +5,19 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -157,21 +159,43 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
|
||||
return transport.administratorOperation(r)
|
||||
}
|
||||
|
||||
volumeName := volumeIDParameter[0]
|
||||
|
||||
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, volumeIDParameter[0])
|
||||
|
||||
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, volumeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// volume browser request
|
||||
return transport.restrictedResourceOperation(r, resourceID, portainer.VolumeResourceControl, true)
|
||||
return transport.restrictedResourceOperation(r, resourceID, volumeName, portainer.VolumeResourceControl, true)
|
||||
case strings.HasPrefix(requestPath, "/dockerhub"):
|
||||
dockerhub, err := transport.dataStore.DockerHub().DockerHub()
|
||||
requestPath, registryIdString := path.Split(r.URL.Path)
|
||||
|
||||
registryID, err := strconv.Atoi(registryIdString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("missing registry id: %w", err)
|
||||
}
|
||||
|
||||
newBody, err := json.Marshal(dockerhub)
|
||||
r.URL.Path = strings.TrimSuffix(requestPath, "/")
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.DockerHubRegistry,
|
||||
}
|
||||
|
||||
if registryID != 0 {
|
||||
registry, err = transport.dataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching registry: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if registry.Type != portainer.DockerHubRegistry {
|
||||
return nil, errors.New("Invalid registry type")
|
||||
}
|
||||
|
||||
newBody, err := json.Marshal(registry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -200,10 +224,10 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res
|
||||
if request.Method == http.MethodGet {
|
||||
return transport.rewriteOperation(request, transport.configInspectOperation)
|
||||
} else if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, configID, portainer.ConfigResourceControl)
|
||||
return transport.executeGenericResourceDeletionOperation(request, configID, configID, portainer.ConfigResourceControl)
|
||||
}
|
||||
|
||||
return transport.restrictedResourceOperation(request, configID, portainer.ConfigResourceControl, false)
|
||||
return transport.restrictedResourceOperation(request, configID, configID, portainer.ConfigResourceControl, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,16 +252,16 @@ func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.
|
||||
if action == "json" {
|
||||
return transport.rewriteOperation(request, transport.containerInspectOperation)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, containerID, portainer.ContainerResourceControl, false)
|
||||
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
|
||||
} else if match, _ := path.Match("/containers/*", requestPath); match {
|
||||
// Handle /containers/{id} requests
|
||||
containerID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, containerID, portainer.ContainerResourceControl)
|
||||
return transport.executeGenericResourceDeletionOperation(request, containerID, containerID, portainer.ContainerResourceControl)
|
||||
}
|
||||
|
||||
return transport.restrictedResourceOperation(request, containerID, portainer.ContainerResourceControl, false)
|
||||
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
|
||||
}
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
@@ -256,7 +280,7 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
|
||||
if match, _ := path.Match("/services/*/*", requestPath); match {
|
||||
// Handle /services/{id}/{action} requests
|
||||
serviceID := path.Base(path.Dir(requestPath))
|
||||
return transport.restrictedResourceOperation(request, serviceID, portainer.ServiceResourceControl, false)
|
||||
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
|
||||
} else if match, _ := path.Match("/services/*", requestPath); match {
|
||||
// Handle /services/{id} requests
|
||||
serviceID := path.Base(requestPath)
|
||||
@@ -265,9 +289,9 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
|
||||
case http.MethodGet:
|
||||
return transport.rewriteOperation(request, transport.serviceInspectOperation)
|
||||
case http.MethodDelete:
|
||||
return transport.executeGenericResourceDeletionOperation(request, serviceID, portainer.ServiceResourceControl)
|
||||
return transport.executeGenericResourceDeletionOperation(request, serviceID, serviceID, portainer.ServiceResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, serviceID, portainer.ServiceResourceControl, false)
|
||||
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
|
||||
}
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
@@ -305,9 +329,9 @@ func (transport *Transport) proxyNetworkRequest(request *http.Request) (*http.Re
|
||||
if request.Method == http.MethodGet {
|
||||
return transport.rewriteOperation(request, transport.networkInspectOperation)
|
||||
} else if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, networkID, portainer.NetworkResourceControl)
|
||||
return transport.executeGenericResourceDeletionOperation(request, networkID, networkID, portainer.NetworkResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, networkID, portainer.NetworkResourceControl, false)
|
||||
return transport.restrictedResourceOperation(request, networkID, networkID, portainer.NetworkResourceControl, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,9 +350,9 @@ func (transport *Transport) proxySecretRequest(request *http.Request) (*http.Res
|
||||
if request.Method == http.MethodGet {
|
||||
return transport.rewriteOperation(request, transport.secretInspectOperation)
|
||||
} else if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, secretID, portainer.SecretResourceControl)
|
||||
return transport.executeGenericResourceDeletionOperation(request, secretID, secretID, portainer.SecretResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, secretID, portainer.SecretResourceControl, false)
|
||||
return transport.restrictedResourceOperation(request, secretID, secretID, portainer.SecretResourceControl, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,13 +418,13 @@ func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Re
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var originalHeaderData registryAuthenticationHeader
|
||||
var originalHeaderData portainerRegistryAuthenticationHeader
|
||||
err = json.Unmarshal(decodedHeaderData, &originalHeaderData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.Serveraddress, accessContext)
|
||||
authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.RegistryId, accessContext)
|
||||
|
||||
headerData, err := json.Marshal(authenticationHeader)
|
||||
if err != nil {
|
||||
@@ -415,7 +439,7 @@ func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Re
|
||||
return transport.decorateGenericResourceCreationOperation(request, serviceObjectIdentifier, portainer.ServiceResourceControl)
|
||||
}
|
||||
|
||||
func (transport *Transport) restrictedResourceOperation(request *http.Request, resourceID string, resourceType portainer.ResourceControlType, volumeBrowseRestrictionCheck bool) (*http.Response, error) {
|
||||
func (transport *Transport) restrictedResourceOperation(request *http.Request, resourceID string, dockerResourceID string, resourceType portainer.ResourceControlType, volumeBrowseRestrictionCheck bool) (*http.Response, error) {
|
||||
var err error
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
@@ -430,7 +454,7 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
||||
}
|
||||
|
||||
if !securitySettings.AllowVolumeBrowserForRegularUsers {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,20 +477,24 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
||||
if resourceControl == nil {
|
||||
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
|
||||
if dockerResourceID == "" {
|
||||
dockerResourceID = resourceID
|
||||
}
|
||||
|
||||
// This resource was created outside of portainer,
|
||||
// is part of a Docker service or part of a Docker Swarm/Compose stack.
|
||||
inheritedResourceControl, err := transport.getInheritedResourceControlFromServiceOrStack(resourceID, agentTargetHeader, resourceType, resourceControls)
|
||||
inheritedResourceControl, err := transport.getInheritedResourceControlFromServiceOrStack(dockerResourceID, agentTargetHeader, resourceType, resourceControls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if inheritedResourceControl == nil || !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
|
||||
if resourceControl != nil && !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +558,7 @@ func (transport *Transport) interceptAndRewriteRequest(request *http.Request, op
|
||||
// https://docs.docker.com/engine/api/v1.37/#operation/SecretCreate
|
||||
// https://docs.docker.com/engine/api/v1.37/#operation/ConfigCreate
|
||||
func (transport *Transport) decorateGenericResourceCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -549,7 +577,7 @@ func (transport *Transport) decorateGenericResourceCreationResponse(response *ht
|
||||
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateGenericResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
@@ -570,8 +598,8 @@ func (transport *Transport) decorateGenericResourceCreationOperation(request *ht
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (transport *Transport) executeGenericResourceDeletionOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
response, err := transport.restrictedResourceOperation(request, resourceIdentifierAttribute, resourceType, false)
|
||||
func (transport *Transport) executeGenericResourceDeletionOperation(request *http.Request, resourceIdentifierAttribute string, volumeName string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
response, err := transport.restrictedResourceOperation(request, resourceIdentifierAttribute, volumeName, resourceType, false)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
@@ -612,7 +640,7 @@ func (transport *Transport) administratorOperation(request *http.Request) (*http
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
@@ -625,15 +653,15 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) (
|
||||
}
|
||||
|
||||
accessContext := ®istryAccessContext{
|
||||
isAdmin: true,
|
||||
userID: tokenData.ID,
|
||||
isAdmin: true,
|
||||
endpointID: transport.endpoint.ID,
|
||||
}
|
||||
|
||||
hub, err := transport.dataStore.DockerHub().DockerHub()
|
||||
user, err := transport.dataStore.User().User(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessContext.dockerHub = hub
|
||||
accessContext.user = user
|
||||
|
||||
registries, err := transport.dataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
@@ -641,7 +669,7 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) (
|
||||
}
|
||||
accessContext.registries = registries
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
if user.Role != portainer.AdministratorRole {
|
||||
accessContext.isAdmin = false
|
||||
|
||||
teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -37,7 +37,7 @@ func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, en
|
||||
func (transport *Transport) volumeListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// VolumeList response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo
|
||||
responseObject["Volumes"] = volumeData
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
// volumeInspectOperation extracts the response as a JSON object, verify that the user
|
||||
@@ -76,7 +76,7 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo
|
||||
func (transport *Transport) volumeInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// VolumeInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
return responseutils.GetJSONObject(responseObject, "Labels")
|
||||
return utils.GetJSONObject(responseObject, "Labels")
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateVolumeResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
@@ -142,7 +142,7 @@ func (transport *Transport) decorateVolumeResourceCreationOperation(request *htt
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateVolumeCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
|
||||
responseObject, err := responseutils.GetResponseAsJSONObject(response)
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -159,7 +159,7 @@ func (transport *Transport) decorateVolumeCreationResponse(response *http.Respon
|
||||
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
return utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
func (transport *Transport) restrictedVolumeOperation(requestPath string, request *http.Request) (*http.Response, error) {
|
||||
@@ -168,17 +168,18 @@ func (transport *Transport) restrictedVolumeOperation(requestPath string, reques
|
||||
return transport.rewriteOperation(request, transport.volumeInspectOperation)
|
||||
}
|
||||
|
||||
volumeName := path.Base(requestPath)
|
||||
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
|
||||
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, path.Base(requestPath))
|
||||
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, volumeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, resourceID, portainer.VolumeResourceControl)
|
||||
return transport.executeGenericResourceDeletionOperation(request, resourceID, volumeName, portainer.VolumeResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, resourceID, portainer.VolumeResourceControl, false)
|
||||
return transport.restrictedResourceOperation(request, resourceID, volumeName, portainer.VolumeResourceControl, false)
|
||||
}
|
||||
|
||||
func (transport *Transport) getVolumeResourceID(nodename, volumeID string) (string, error) {
|
||||
|
||||
@@ -39,7 +39,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.dataStore)
|
||||
transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
|
||||
|
||||
endpointURL.Scheme = "http"
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||
proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint, tokenManager)
|
||||
proxy.Transport = kubernetes.NewEdgeTransport(factory.reverseTunnelService, endpoint, tokenManager, factory.kubernetesClientFactory, factory.dataStore)
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
|
||||
}
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
||||
proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager, endpoint)
|
||||
proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
type agentTransport struct {
|
||||
@@ -14,17 +15,18 @@ type agentTransport struct {
|
||||
}
|
||||
|
||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
||||
func NewAgentTransport(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint) *agentTransport {
|
||||
func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) *agentTransport {
|
||||
transport := &agentTransport{
|
||||
signatureService: signatureService,
|
||||
baseTransport: newBaseTransport(
|
||||
&http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
},
|
||||
tokenManager,
|
||||
endpoint,
|
||||
k8sClientFactory,
|
||||
dataStore,
|
||||
),
|
||||
signatureService: signatureService,
|
||||
}
|
||||
|
||||
return transport
|
||||
@@ -32,7 +34,7 @@ func NewAgentTransport(dataStore portainer.DataStore, signatureService portainer
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID)
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,7 +42,10 @@ func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Respons
|
||||
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||
|
||||
if strings.HasPrefix(request.URL.Path, "/v2") {
|
||||
decorateAgentRequest(request, transport.dataStore)
|
||||
err := decorateAgentRequest(request, transport.dataStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
type edgeTransport struct {
|
||||
@@ -13,15 +14,16 @@ type edgeTransport struct {
|
||||
}
|
||||
|
||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
|
||||
func NewEdgeTransport(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager) *edgeTransport {
|
||||
func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) *edgeTransport {
|
||||
transport := &edgeTransport{
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
baseTransport: newBaseTransport(
|
||||
&http.Transport{},
|
||||
tokenManager,
|
||||
endpoint,
|
||||
k8sClientFactory,
|
||||
dataStore,
|
||||
),
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
}
|
||||
|
||||
return transport
|
||||
@@ -29,7 +31,7 @@ func NewEdgeTransport(dataStore portainer.DataStore, reverseTunnelService portai
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID)
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -37,7 +39,10 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response
|
||||
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||
|
||||
if strings.HasPrefix(request.URL.Path, "/v2") {
|
||||
decorateAgentRequest(request, transport.dataStore)
|
||||
err := decorateAgentRequest(request, transport.dataStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
response, err := transport.baseTransport.RoundTrip(request)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
type localTransport struct {
|
||||
@@ -13,7 +13,7 @@ type localTransport struct {
|
||||
}
|
||||
|
||||
// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
|
||||
func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, dataStore portainer.DataStore) (*localTransport, error) {
|
||||
func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) (*localTransport, error) {
|
||||
config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -26,6 +26,7 @@ func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint,
|
||||
},
|
||||
tokenManager,
|
||||
endpoint,
|
||||
k8sClientFactory,
|
||||
dataStore,
|
||||
),
|
||||
}
|
||||
@@ -35,12 +36,10 @@ func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint,
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID)
|
||||
_, err := transport.prepareRoundTrip(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
return transport.baseTransport.RoundTrip(request)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,43 @@ package kubernetes
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (transport *baseTransport) deleteNamespaceRequest(request *http.Request, namespace string) (*http.Response, error) {
|
||||
if err := transport.tokenManager.kubecli.NamespaceAccessPoliciesDeleteNamespace(namespace); err != nil {
|
||||
return nil, errors.WithMessagef(err, "failed to delete a namespace [%s] from portainer config", namespace)
|
||||
func (transport *baseTransport) proxyNamespaceDeleteOperation(request *http.Request, namespace string) (*http.Response, error) {
|
||||
registries, err := transport.dataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport.executeKubernetesRequest(request, true)
|
||||
for _, registry := range registries {
|
||||
for endpointID, registryAccessPolicies := range registry.RegistryAccesses {
|
||||
if endpointID != transport.endpoint.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
namespaces := []string{}
|
||||
for _, ns := range registryAccessPolicies.Namespaces {
|
||||
if ns == namespace {
|
||||
continue
|
||||
}
|
||||
namespaces = append(namespaces, ns)
|
||||
}
|
||||
|
||||
if len(namespaces) != len(registryAccessPolicies.Namespaces) {
|
||||
updatedAccessPolicies := portainer.RegistryAccessPolicies{
|
||||
Namespaces: namespaces,
|
||||
UserAccessPolicies: registryAccessPolicies.UserAccessPolicies,
|
||||
TeamAccessPolicies: registryAccessPolicies.TeamAccessPolicies,
|
||||
}
|
||||
|
||||
registry.RegistryAccesses[endpointID] = updatedAccessPolicies
|
||||
err := transport.dataStore.Registry().UpdateRegistry(registry.ID, ®istry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
|
||||
170
api/http/proxy/factory/kubernetes/secrets.go
Normal file
170
api/http/proxy/factory/kubernetes/secrets.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/privateregistries"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func (transport *baseTransport) proxySecretRequest(request *http.Request, namespace, requestPath string) (*http.Response, error) {
|
||||
switch request.Method {
|
||||
case "POST":
|
||||
return transport.proxySecretCreationOperation(request)
|
||||
case "GET":
|
||||
if path.Base(requestPath) == "secrets" {
|
||||
return transport.proxySecretListOperation(request)
|
||||
}
|
||||
return transport.proxySecretInspectOperation(request)
|
||||
case "PUT":
|
||||
return transport.proxySecretUpdateOperation(request)
|
||||
case "DELETE":
|
||||
return transport.proxySecretDeleteOperation(request, namespace)
|
||||
default:
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *baseTransport) proxySecretCreationOperation(request *http.Request) (*http.Response, error) {
|
||||
body, err := utils.GetRequestAsMap(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isSecretRepresentPrivateRegistry(body) {
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
|
||||
err = utils.RewriteRequest(request, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
|
||||
func (transport *baseTransport) proxySecretListOperation(request *http.Request) (*http.Response, error) {
|
||||
response, err := transport.executeKubernetesRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isAdmin, err := security.IsAdmin(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
body, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := utils.GetArrayObject(body, "items")
|
||||
|
||||
if items == nil {
|
||||
utils.RewriteResponse(response, body, response.StatusCode)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
filteredItems := []interface{}{}
|
||||
for _, item := range items {
|
||||
itemObj := item.(map[string]interface{})
|
||||
if !isSecretRepresentPrivateRegistry(itemObj) {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
|
||||
body["items"] = filteredItems
|
||||
|
||||
utils.RewriteResponse(response, body, response.StatusCode)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (transport *baseTransport) proxySecretInspectOperation(request *http.Request) (*http.Response, error) {
|
||||
response, err := transport.executeKubernetesRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isAdmin, err := security.IsAdmin(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
body, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isSecretRepresentPrivateRegistry(body) {
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
|
||||
err = utils.RewriteResponse(response, body, response.StatusCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (transport *baseTransport) proxySecretUpdateOperation(request *http.Request) (*http.Response, error) {
|
||||
body, err := utils.GetRequestAsMap(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isSecretRepresentPrivateRegistry(body) {
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
|
||||
err = utils.RewriteRequest(request, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
|
||||
func (transport *baseTransport) proxySecretDeleteOperation(request *http.Request, namespace string) (*http.Response, error) {
|
||||
kcl, err := transport.k8sClientFactory.GetKubeClient(transport.endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secretName := path.Base(request.RequestURI)
|
||||
|
||||
isRegistry, err := kcl.IsRegistrySecret(namespace, secretName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isRegistry {
|
||||
return utils.WriteAccessDeniedResponse()
|
||||
}
|
||||
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
|
||||
func isSecretRepresentPrivateRegistry(secret map[string]interface{}) bool {
|
||||
if secret["type"].(string) != string(v1.SecretTypeDockerConfigJson) {
|
||||
return false
|
||||
}
|
||||
|
||||
metadata := utils.GetJSONObject(secret, "metadata")
|
||||
annotations := utils.GetJSONObject(metadata, "annotations")
|
||||
_, ok := annotations[privateregistries.RegistryIDLabel]
|
||||
|
||||
return ok
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
@@ -11,6 +13,7 @@ type tokenManager struct {
|
||||
tokenCache *tokenCache
|
||||
kubecli portainer.KubeClient
|
||||
dataStore portainer.DataStore
|
||||
mutex sync.Mutex
|
||||
adminToken string
|
||||
}
|
||||
|
||||
@@ -22,6 +25,7 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore
|
||||
tokenCache: cache,
|
||||
kubecli: kubecli,
|
||||
dataStore: dataStore,
|
||||
mutex: sync.Mutex{},
|
||||
adminToken: "",
|
||||
}
|
||||
|
||||
@@ -37,13 +41,13 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore
|
||||
return tokenManager, nil
|
||||
}
|
||||
|
||||
func (manager *tokenManager) GetAdminServiceAccountToken() string {
|
||||
func (manager *tokenManager) getAdminServiceAccountToken() string {
|
||||
return manager.adminToken
|
||||
}
|
||||
|
||||
func (manager *tokenManager) GetUserServiceAccountToken(userID int) (string, error) {
|
||||
manager.tokenCache.mutex.Lock()
|
||||
defer manager.tokenCache.mutex.Unlock()
|
||||
func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) {
|
||||
manager.mutex.Lock()
|
||||
defer manager.mutex.Unlock()
|
||||
|
||||
token, ok := manager.tokenCache.getToken(userID)
|
||||
if !ok {
|
||||
|
||||
@@ -2,7 +2,6 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/orcaman/concurrent-map"
|
||||
)
|
||||
@@ -15,7 +14,6 @@ type (
|
||||
|
||||
tokenCache struct {
|
||||
userTokenCache cmap.ConcurrentMap
|
||||
mutex sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
@@ -37,18 +35,6 @@ func (manager *TokenCacheManager) CreateTokenCache(endpointID int) *tokenCache {
|
||||
return tokenCache
|
||||
}
|
||||
|
||||
// GetOrCreateTokenCache will get the tokenCache from the manager map of caches if it exists,
|
||||
// otherwise it will create a new tokenCache object, associate it to the manager map of caches
|
||||
// and return a pointer to that tokenCache instance.
|
||||
func (manager *TokenCacheManager) GetOrCreateTokenCache(endpointID int) *tokenCache {
|
||||
key := strconv.Itoa(endpointID)
|
||||
if epCache, ok := manager.tokenCaches.Get(key); ok {
|
||||
return epCache.(*tokenCache)
|
||||
}
|
||||
|
||||
return manager.CreateTokenCache(endpointID)
|
||||
}
|
||||
|
||||
// RemoveUserFromCache will ensure that the specific userID is removed from all registered caches.
|
||||
func (manager *TokenCacheManager) RemoveUserFromCache(userID int) {
|
||||
for cache := range manager.tokenCaches.IterBuffered() {
|
||||
@@ -59,7 +45,6 @@ func (manager *TokenCacheManager) RemoveUserFromCache(userID int) {
|
||||
func newTokenCache() *tokenCache {
|
||||
return &tokenCache{
|
||||
userTokenCache: cmap.New(),
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,44 +3,55 @@ package kubernetes
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type baseTransport struct {
|
||||
httpTransport *http.Transport
|
||||
tokenManager *tokenManager
|
||||
endpoint *portainer.Endpoint
|
||||
dataStore portainer.DataStore
|
||||
httpTransport *http.Transport
|
||||
tokenManager *tokenManager
|
||||
endpoint *portainer.Endpoint
|
||||
k8sClientFactory *cli.ClientFactory
|
||||
dataStore portainer.DataStore
|
||||
}
|
||||
|
||||
func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, dataStore portainer.DataStore) *baseTransport {
|
||||
func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) *baseTransport {
|
||||
return &baseTransport{
|
||||
httpTransport: httpTransport,
|
||||
tokenManager: tokenManager,
|
||||
endpoint: endpoint,
|
||||
dataStore: dataStore,
|
||||
httpTransport: httpTransport,
|
||||
tokenManager: tokenManager,
|
||||
endpoint: endpoint,
|
||||
k8sClientFactory: k8sClientFactory,
|
||||
dataStore: dataStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
// #region KUBERNETES PROXY
|
||||
|
||||
// proxyKubernetesRequest intercepts a Kubernetes API request and apply logic based
|
||||
// on the requested operation.
|
||||
func (transport *baseTransport) proxyKubernetesRequest(request *http.Request) (*http.Response, error) {
|
||||
apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/api/v[0-9](\.[0-9])?`)
|
||||
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
||||
|
||||
switch {
|
||||
case strings.EqualFold(requestPath, "/namespaces"):
|
||||
return transport.executeKubernetesRequest(request, true)
|
||||
return transport.executeKubernetesRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/namespaces"):
|
||||
return transport.proxyNamespacedRequest(request, requestPath)
|
||||
default:
|
||||
return transport.executeKubernetesRequest(request, true)
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,26 +66,43 @@ func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fu
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(requestPath, "secrets"):
|
||||
return transport.proxySecretRequest(request, namespace, requestPath)
|
||||
case requestPath == "" && request.Method == "DELETE":
|
||||
return transport.deleteNamespaceRequest(request, namespace)
|
||||
return transport.proxyNamespaceDeleteOperation(request, namespace)
|
||||
default:
|
||||
return transport.executeKubernetesRequest(request, true)
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *baseTransport) executeKubernetesRequest(request *http.Request, shouldLog bool) (*http.Response, error) {
|
||||
return transport.httpTransport.RoundTrip(request)
|
||||
func (transport *baseTransport) executeKubernetesRequest(request *http.Request) (*http.Response, error) {
|
||||
|
||||
resp, err := transport.httpTransport.RoundTrip(request)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
var (
|
||||
namespaceRegex = regexp.MustCompile(`^/namespaces/([^/]*)$`)
|
||||
)
|
||||
// #endregion
|
||||
|
||||
func getRoundTripToken(
|
||||
request *http.Request,
|
||||
tokenManager *tokenManager,
|
||||
endpointIdentifier portainer.EndpointID,
|
||||
) (string, error) {
|
||||
// #region ROUND TRIP
|
||||
|
||||
func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return transport.proxyKubernetesRequest(request)
|
||||
}
|
||||
|
||||
func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -82,9 +110,9 @@ func getRoundTripToken(
|
||||
|
||||
var token string
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
token = tokenManager.GetAdminServiceAccountToken()
|
||||
token = tokenManager.getAdminServiceAccountToken()
|
||||
} else {
|
||||
token, err = tokenManager.GetUserServiceAccountToken(int(tokenData.ID))
|
||||
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
||||
if err != nil {
|
||||
log.Printf("Failed retrieving service account token: %v", err)
|
||||
return "", err
|
||||
@@ -94,32 +122,56 @@ func getRoundTripToken(
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region DECORATE FUNCTIONS
|
||||
|
||||
func decorateAgentRequest(r *http.Request, dataStore portainer.DataStore) error {
|
||||
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(requestPath, "/dockerhub"):
|
||||
decorateAgentDockerHubRequest(r, dataStore)
|
||||
return decorateAgentDockerHubRequest(r, dataStore)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decorateAgentDockerHubRequest(r *http.Request, dataStore portainer.DataStore) error {
|
||||
dockerhub, err := dataStore.DockerHub().DockerHub()
|
||||
requestPath, registryIdString := path.Split(r.URL.Path)
|
||||
|
||||
registryID, err := strconv.Atoi(registryIdString)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("missing registry id: %w", err)
|
||||
}
|
||||
|
||||
newBody, err := json.Marshal(dockerhub)
|
||||
r.URL.Path = strings.TrimSuffix(requestPath, "/")
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.DockerHubRegistry,
|
||||
}
|
||||
|
||||
if registryID != 0 {
|
||||
registry, err = dataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching registry: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if registry.Type != portainer.DockerHubRegistry {
|
||||
return errors.New("invalid registry type")
|
||||
}
|
||||
|
||||
newBody, err := json.Marshal(registry)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed marshaling registry: %w", err)
|
||||
}
|
||||
|
||||
r.Method = http.MethodPost
|
||||
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(newBody))
|
||||
r.ContentLength = int64(len(newBody))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package responseutils
|
||||
|
||||
// GetJSONObject will extract an object from a specific property of another JSON object.
|
||||
// Returns nil if nothing is associated to the specified key.
|
||||
func GetJSONObject(jsonObject map[string]interface{}, property string) map[string]interface{} {
|
||||
object := jsonObject[property]
|
||||
if object != nil {
|
||||
return object.(map[string]interface{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
91
api/http/proxy/factory/utils/json.go
Normal file
91
api/http/proxy/factory/utils/json.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// GetJSONObject will extract an object from a specific property of another JSON object.
|
||||
// Returns nil if nothing is associated to the specified key.
|
||||
func GetJSONObject(jsonObject map[string]interface{}, property string) map[string]interface{} {
|
||||
object := jsonObject[property]
|
||||
if object != nil {
|
||||
return object.(map[string]interface{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetArrayObject will extract an array from a specific property of another JSON object.
|
||||
// Returns nil if nothing is associated to the specified key.
|
||||
func GetArrayObject(jsonObject map[string]interface{}, property string) []interface{} {
|
||||
object := jsonObject[property]
|
||||
if object != nil {
|
||||
return object.([]interface{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBody(body io.ReadCloser, contentType string, isGzip bool) (interface{}, error) {
|
||||
if body == nil {
|
||||
return nil, errors.New("unable to parse response: empty response body")
|
||||
}
|
||||
|
||||
reader := body
|
||||
|
||||
if isGzip {
|
||||
gzipReader, err := gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader = gzipReader
|
||||
}
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
bodyBytes, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
err = unmarshal(contentType, bodyBytes, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func marshal(contentType string, data interface{}) ([]byte, error) {
|
||||
switch contentType {
|
||||
case "application/yaml":
|
||||
return yaml.Marshal(data)
|
||||
case "application/json", "":
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("content type is not supported for marshaling: %s", contentType)
|
||||
}
|
||||
|
||||
func unmarshal(contentType string, body []byte, returnBody interface{}) error {
|
||||
switch contentType {
|
||||
case "application/yaml":
|
||||
return yaml.Unmarshal(body, returnBody)
|
||||
case "application/json", "":
|
||||
return json.Unmarshal(body, returnBody)
|
||||
}
|
||||
|
||||
return fmt.Errorf("content type is not supported for unmarshaling: %s", contentType)
|
||||
}
|
||||
45
api/http/proxy/factory/utils/request.go
Normal file
45
api/http/proxy/factory/utils/request.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GetRequestAsMap returns the response content as a generic JSON object
|
||||
func GetRequestAsMap(request *http.Request) (map[string]interface{}, error) {
|
||||
data, err := getRequestBody(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data.(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
// RewriteRequest will replace the existing request body with the one specified
|
||||
// in parameters
|
||||
func RewriteRequest(request *http.Request, newData interface{}) error {
|
||||
data, err := marshal(getContentType(request.Header), newData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := ioutil.NopCloser(bytes.NewReader(data))
|
||||
|
||||
request.Body = body
|
||||
request.ContentLength = int64(len(data))
|
||||
|
||||
if request.Header == nil {
|
||||
request.Header = make(http.Header)
|
||||
}
|
||||
request.Header.Set("Content-Length", strconv.Itoa(len(data)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRequestBody(request *http.Request) (interface{}, error) {
|
||||
isGzip := request.Header.Get("Content-Encoding") == "gzip"
|
||||
|
||||
return getBody(request.Body, getContentType(request.Header), isGzip)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package responseutils
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -13,7 +11,7 @@ import (
|
||||
|
||||
// GetResponseAsJSONObject returns the response content as a generic JSON object
|
||||
func GetResponseAsJSONObject(response *http.Response) (map[string]interface{}, error) {
|
||||
responseData, err := getResponseBodyAsGenericJSON(response)
|
||||
responseData, err := getResponseBody(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -24,7 +22,7 @@ func GetResponseAsJSONObject(response *http.Response) (map[string]interface{}, e
|
||||
|
||||
// GetResponseAsJSONArray returns the response content as an array of generic JSON object
|
||||
func GetResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
|
||||
responseData, err := getResponseBodyAsGenericJSON(response)
|
||||
responseData, err := getResponseBody(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -44,72 +42,54 @@ func GetResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) {
|
||||
if response.Body == nil {
|
||||
return nil, errors.New("unable to parse response: empty response body")
|
||||
}
|
||||
|
||||
reader := response.Body
|
||||
|
||||
if response.Header.Get("Content-Encoding") == "gzip" {
|
||||
response.Header.Del("Content-Encoding")
|
||||
gzipReader, err := gzip.NewReader(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = gzipReader
|
||||
}
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
var data interface{}
|
||||
body, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
type dockerErrorResponse struct {
|
||||
type errorResponse struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// WriteAccessDeniedResponse will create a new access denied response
|
||||
func WriteAccessDeniedResponse() (*http.Response, error) {
|
||||
response := &http.Response{}
|
||||
err := RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden)
|
||||
err := RewriteResponse(response, errorResponse{Message: "access denied to resource"}, http.StatusForbidden)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// RewriteAccessDeniedResponse will overwrite the existing response with an access denied response
|
||||
func RewriteAccessDeniedResponse(response *http.Response) error {
|
||||
return RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden)
|
||||
return RewriteResponse(response, errorResponse{Message: "access denied to resource"}, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// RewriteResponse will replace the existing response body and status code with the one specified
|
||||
// in parameters
|
||||
func RewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error {
|
||||
jsonData, err := json.Marshal(newResponseData)
|
||||
data, err := marshal(getContentType(response.Header), newResponseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := ioutil.NopCloser(bytes.NewReader(jsonData))
|
||||
body := ioutil.NopCloser(bytes.NewReader(data))
|
||||
|
||||
response.StatusCode = statusCode
|
||||
response.Body = body
|
||||
response.ContentLength = int64(len(jsonData))
|
||||
response.ContentLength = int64(len(data))
|
||||
|
||||
if response.Header == nil {
|
||||
response.Header = make(http.Header)
|
||||
}
|
||||
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
|
||||
response.Header.Set("Content-Length", strconv.Itoa(len(data)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getResponseBody(response *http.Response) (interface{}, error) {
|
||||
isGzip := response.Header.Get("Content-Encoding") == "gzip"
|
||||
|
||||
if isGzip {
|
||||
response.Header.Del("Content-Encoding")
|
||||
}
|
||||
|
||||
return getBody(response.Body, getContentType(response.Header), isGzip)
|
||||
}
|
||||
|
||||
func getContentType(headers http.Header) string {
|
||||
return headers.Get("Content-type")
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// IsAdmin returns true if the logged-in user is an admin
|
||||
func IsAdmin(request *http.Request) (bool, error) {
|
||||
tokenData, err := RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return tokenData.Role == portainer.AdministratorRole, nil
|
||||
}
|
||||
|
||||
// AuthorizedResourceControlAccess checks whether the user can alter an existing resource control.
|
||||
func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin || resourceControl.Public {
|
||||
@@ -95,9 +107,9 @@ func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedReques
|
||||
// It will check if the user is part of the authorized users or part of a team that is
|
||||
// listed in the authorized teams of the endpoint and the associated group.
|
||||
func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
groupAccess := authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
|
||||
groupAccess := AuthorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
|
||||
if !groupAccess {
|
||||
return authorizedAccess(userID, memberships, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies)
|
||||
return AuthorizedAccess(userID, memberships, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -106,17 +118,24 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta
|
||||
// It will check if the user is part of the authorized users or part of a team that is
|
||||
// listed in the authorized teams.
|
||||
func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
return authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
|
||||
return AuthorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
|
||||
}
|
||||
|
||||
// AuthorizedRegistryAccess ensure that the user can access the specified registry.
|
||||
// It will check if the user is part of the authorized users or part of a team that is
|
||||
// listed in the authorized teams.
|
||||
func AuthorizedRegistryAccess(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
return authorizedAccess(userID, memberships, registry.UserAccessPolicies, registry.TeamAccessPolicies)
|
||||
// listed in the authorized teams for a specified endpoint,
|
||||
func AuthorizedRegistryAccess(registry *portainer.Registry, user *portainer.User, teamMemberships []portainer.TeamMembership, endpointID portainer.EndpointID) bool {
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return true
|
||||
}
|
||||
|
||||
registryEndpointAccesses := registry.RegistryAccesses[endpointID]
|
||||
|
||||
return AuthorizedAccess(user.ID, teamMemberships, registryEndpointAccesses.UserAccessPolicies, registryEndpointAccesses.TeamAccessPolicies)
|
||||
}
|
||||
|
||||
func authorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, userAccessPolicies portainer.UserAccessPolicies, teamAccessPolicies portainer.TeamAccessPolicies) bool {
|
||||
// AuthorizedAccess verifies the userID or memberships are authorized to use an object per the supplied access policies
|
||||
func AuthorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, userAccessPolicies portainer.UserAccessPolicies, teamAccessPolicies portainer.TeamAccessPolicies) bool {
|
||||
_, userAccess := userAccessPolicies[userID]
|
||||
if userAccess {
|
||||
return true
|
||||
|
||||
@@ -128,31 +128,6 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegistryAccess retrieves the JWT token from the request context and verifies
|
||||
// that the user can access the specified registry.
|
||||
// An error is returned when access is denied.
|
||||
func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portainer.Registry) error {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return nil
|
||||
}
|
||||
|
||||
memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !AuthorizedRegistryAccess(registry, tokenData.ID, memberships) {
|
||||
return httperrors.ErrEndpointAccessDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handlers are applied backwards to the incoming request:
|
||||
// - add secure handlers to the response
|
||||
// - parse the JWT token and put it into the http context.
|
||||
@@ -213,7 +188,7 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
|
||||
return
|
||||
}
|
||||
|
||||
ctx := storeRestrictedRequestContext(r, requestContext)
|
||||
ctx := StoreRestrictedRequestContext(r, requestContext)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -33,9 +33,9 @@ func RetrieveTokenData(request *http.Request) (*portainer.TokenData, error) {
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
// storeRestrictedRequestContext stores a RestrictedRequestContext object inside the request context
|
||||
// StoreRestrictedRequestContext stores a RestrictedRequestContext object inside the request context
|
||||
// and returns the enhanced context.
|
||||
func storeRestrictedRequestContext(request *http.Request, requestContext *RestrictedRequestContext) context.Context {
|
||||
func StoreRestrictedRequestContext(request *http.Request, requestContext *RestrictedRequestContext) context.Context {
|
||||
return context.WithValue(request.Context(), contextRestrictedRequest, requestContext)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// FilterUserTeams filters teams based on user role.
|
||||
@@ -64,15 +64,16 @@ func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []po
|
||||
|
||||
// FilterRegistries filters registries based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized registries.
|
||||
func FilterRegistries(registries []portainer.Registry, context *RestrictedRequestContext) []portainer.Registry {
|
||||
filteredRegistries := registries
|
||||
if !context.IsAdmin {
|
||||
filteredRegistries = make([]portainer.Registry, 0)
|
||||
func FilterRegistries(registries []portainer.Registry, user *portainer.User, teamMemberships []portainer.TeamMembership, endpointID portainer.EndpointID) []portainer.Registry {
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return registries
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
if AuthorizedRegistryAccess(®istry, context.UserID, context.UserMemberships) {
|
||||
filteredRegistries = append(filteredRegistries, registry)
|
||||
}
|
||||
filteredRegistries := []portainer.Registry{}
|
||||
|
||||
for _, registry := range registries {
|
||||
if AuthorizedRegistryAccess(®istry, user, teamMemberships, endpointID) {
|
||||
filteredRegistries = append(filteredRegistries, registry)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/backup"
|
||||
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
||||
"github.com/portainer/portainer/api/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||
@@ -111,9 +110,6 @@ func (server *Server) Start() error {
|
||||
customTemplatesHandler.FileService = server.FileService
|
||||
customTemplatesHandler.GitService = server.GitService
|
||||
|
||||
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
|
||||
dockerHubHandler.DataStore = server.DataStore
|
||||
|
||||
var edgeGroupsHandler = edgegroups.NewHandler(requestBouncer)
|
||||
edgeGroupsHandler.DataStore = server.DataStore
|
||||
|
||||
@@ -135,6 +131,7 @@ func (server *Server) Start() error {
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = server.ProxyManager
|
||||
endpointHandler.SnapshotService = server.SnapshotService
|
||||
endpointHandler.K8sClientFactory = server.KubernetesClientFactory
|
||||
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
endpointHandler.ComposeStackManager = server.ComposeStackManager
|
||||
endpointHandler.AuthorizationService = server.AuthorizationService
|
||||
@@ -161,6 +158,7 @@ func (server *Server) Start() error {
|
||||
registryHandler.DataStore = server.DataStore
|
||||
registryHandler.FileService = server.FileService
|
||||
registryHandler.ProxyManager = server.ProxyManager
|
||||
registryHandler.K8sClientFactory = server.KubernetesClientFactory
|
||||
|
||||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||
resourceControlHandler.DataStore = server.DataStore
|
||||
@@ -204,7 +202,7 @@ func (server *Server) Start() error {
|
||||
userHandler.DataStore = server.DataStore
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
|
||||
var websocketHandler = websocket.NewHandler(server.KubernetesTokenCacheManager, requestBouncer)
|
||||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||
websocketHandler.DataStore = server.DataStore
|
||||
websocketHandler.SignatureService = server.SignatureService
|
||||
websocketHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
@@ -219,7 +217,6 @@ func (server *Server) Start() error {
|
||||
AuthHandler: authHandler,
|
||||
BackupHandler: backupHandler,
|
||||
CustomTemplatesHandler: customTemplatesHandler,
|
||||
DockerHubHandler: dockerHubHandler,
|
||||
EdgeGroupsHandler: edgeGroupsHandler,
|
||||
EdgeJobsHandler: edgeJobsHandler,
|
||||
EdgeStacksHandler: edgeStacksHandler,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package authorization
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
// Service represents a service used to
|
||||
// update authorizations associated to a user or team.
|
||||
type Service struct {
|
||||
dataStore portainer.DataStore
|
||||
K8sClientFactory *cli.ClientFactory
|
||||
dataStore portainer.DataStore
|
||||
K8sClientFactory *cli.ClientFactory
|
||||
}
|
||||
|
||||
// NewService returns a point to a new Service instance.
|
||||
@@ -140,6 +140,7 @@ func DefaultEndpointAuthorizationsForEndpointAdministratorRole() portainer.Autho
|
||||
portainer.OperationDockerAgentUndefined: true,
|
||||
portainer.OperationPortainerResourceControlCreate: true,
|
||||
portainer.OperationPortainerResourceControlUpdate: true,
|
||||
portainer.OperationPortainerRegistryUpdateAccess: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
|
||||
@@ -9,8 +9,8 @@ func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
|
||||
}
|
||||
|
||||
// IsDocketEndpoint returns true if this is a docker endpoint
|
||||
func IsDocketEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
// IsDockerEndpoint returns true if this is a docker endpoint
|
||||
func IsDockerEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
return endpoint.Type == portainer.DockerEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
)
|
||||
|
||||
type datastore struct {
|
||||
dockerHub portainer.DockerHubService
|
||||
customTemplate portainer.CustomTemplateService
|
||||
edgeGroup portainer.EdgeGroupService
|
||||
edgeJob portainer.EdgeJobService
|
||||
@@ -37,7 +36,6 @@ func (d *datastore) CheckCurrentEdition() error { retur
|
||||
func (d *datastore) IsNew() bool { return false }
|
||||
func (d *datastore) MigrateData(force bool) error { return nil }
|
||||
func (d *datastore) RollbackToCE() error { return nil }
|
||||
func (d *datastore) DockerHub() portainer.DockerHubService { return d.dockerHub }
|
||||
func (d *datastore) CustomTemplate() portainer.CustomTemplateService { return d.customTemplate }
|
||||
func (d *datastore) EdgeGroup() portainer.EdgeGroupService { return d.edgeGroup }
|
||||
func (d *datastore) EdgeJob() portainer.EdgeJobService { return d.edgeJob }
|
||||
|
||||
@@ -3,7 +3,6 @@ package cli
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -13,6 +12,28 @@ type (
|
||||
namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy
|
||||
)
|
||||
|
||||
// GetNamespaceAccessPolicies gets the namespace access policies
|
||||
// from config maps in the portainer namespace
|
||||
func (kcl *KubeClient) GetNamespaceAccessPolicies() (
|
||||
map[string]portainer.K8sNamespaceAccessPolicy, error,
|
||||
) {
|
||||
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
|
||||
|
||||
var policies map[string]portainer.K8sNamespaceAccessPolicy
|
||||
err = json.Unmarshal([]byte(accessData), &policies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error {
|
||||
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
|
||||
if k8serrors.IsNotFound(err) {
|
||||
@@ -81,43 +102,6 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sN
|
||||
return false
|
||||
}
|
||||
|
||||
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
|
||||
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
||||
kcl.lock.Lock()
|
||||
defer kcl.lock.Unlock()
|
||||
|
||||
policies, err := kcl.GetNamespaceAccessPolicies()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to fetch access policies")
|
||||
}
|
||||
|
||||
delete(policies, ns)
|
||||
|
||||
return kcl.UpdateNamespaceAccessPolicies(policies)
|
||||
}
|
||||
|
||||
// GetNamespaceAccessPolicies gets the namespace access policies
|
||||
// from config maps in the portainer namespace
|
||||
func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {
|
||||
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
|
||||
|
||||
var policies map[string]portainer.K8sNamespaceAccessPolicy
|
||||
err = json.Unmarshal([]byte(accessData), &policies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// UpdateNamespaceAccessPolicies updates the namespace access policies
|
||||
func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]portainer.K8sNamespaceAccessPolicy) error {
|
||||
data, err := json.Marshal(accessPolicies)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
ktypes "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kfake "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func Test_NamespaceAccessPoliciesDeleteNamespace_updatesPortainerConfig_whenConfigExists(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
namespaceToDelete string
|
||||
expectedConfig map[string]portainer.K8sNamespaceAccessPolicy
|
||||
}{
|
||||
{
|
||||
name: "doesn't change config, when designated namespace absent",
|
||||
namespaceToDelete: "missing-namespace",
|
||||
expectedConfig: map[string]portainer.K8sNamespaceAccessPolicy{
|
||||
"ns1": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}},
|
||||
"ns2": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "removes designated namespace from config, when namespace is present",
|
||||
namespaceToDelete: "ns2",
|
||||
expectedConfig: map[string]portainer.K8sNamespaceAccessPolicy{
|
||||
"ns1": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
config := &ktypes.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: portainerConfigMapName,
|
||||
Namespace: portainerNamespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"NamespaceAccessPolicies": `{"ns1":{"UserAccessPolicies":{"2":{"RoleId":0}}}, "ns2":{"UserAccessPolicies":{"2":{"RoleId":0}}}}`,
|
||||
},
|
||||
}
|
||||
_, err := k.cli.CoreV1().ConfigMaps(portainerNamespace).Create(config)
|
||||
assert.NoError(t, err, "failed to create a portainer config")
|
||||
defer func() {
|
||||
k.cli.CoreV1().ConfigMaps(portainerNamespace).Delete(portainerConfigMapName, nil)
|
||||
}()
|
||||
|
||||
err = k.NamespaceAccessPoliciesDeleteNamespace(test.namespaceToDelete)
|
||||
assert.NoError(t, err, "failed to delete namespace")
|
||||
|
||||
policies, err := k.GetNamespaceAccessPolicies()
|
||||
assert.NoError(t, err, "failed to fetch policies")
|
||||
assert.Equal(t, test.expectedConfig, policies)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
type (
|
||||
// ClientFactory is used to create Kubernetes clients
|
||||
ClientFactory struct {
|
||||
dataStore portainer.DataStore
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
instanceID string
|
||||
@@ -26,15 +27,15 @@ type (
|
||||
|
||||
// KubeClient represent a service used to execute Kubernetes operations
|
||||
KubeClient struct {
|
||||
cli kubernetes.Interface
|
||||
cli *kubernetes.Clientset
|
||||
instanceID string
|
||||
lock *sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
// NewClientFactory returns a new instance of a ClientFactory
|
||||
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *ClientFactory {
|
||||
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore portainer.DataStore) *ClientFactory {
|
||||
return &ClientFactory{
|
||||
dataStore: dataStore,
|
||||
signatureService: signatureService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
instanceID: instanceID,
|
||||
@@ -74,7 +75,6 @@ func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (po
|
||||
kubecli := &KubeClient{
|
||||
cli: cli,
|
||||
instanceID: factory.instanceID,
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
return kubecli, nil
|
||||
@@ -136,7 +136,29 @@ func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*k
|
||||
|
||||
func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
|
||||
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
endpointURL := fmt.Sprintf("http://localhost:%d/kubernetes", tunnel.Port)
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
err := factory.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
|
||||
}
|
||||
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
settings, err := factory.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
|
||||
}
|
||||
|
||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
|
||||
tunnel = factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
}
|
||||
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags(endpointURL, "")
|
||||
if err != nil {
|
||||
|
||||
@@ -14,18 +14,13 @@ import (
|
||||
// StartExecProcess will start an exec process inside a container located inside a pod inside a specific namespace
|
||||
// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write
|
||||
// to the stdout parameter.
|
||||
// This function only works against a local endpoint using an in-cluster config with the user's SA token.
|
||||
func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error {
|
||||
// This function only works against a local endpoint using an in-cluster config.
|
||||
func (kcl *KubeClient) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !useAdminToken {
|
||||
config.BearerToken = token
|
||||
config.BearerTokenFile = ""
|
||||
}
|
||||
|
||||
req := kcl.cli.CoreV1().RESTClient().
|
||||
Post().
|
||||
Resource("pods").
|
||||
|
||||
96
api/kubernetes/cli/registries.go
Normal file
96
api/kubernetes/cli/registries.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
secretDockerConfigKey = ".dockerconfigjson"
|
||||
)
|
||||
|
||||
type (
|
||||
dockerConfig struct {
|
||||
Auths map[string]registryDockerConfig `json:"auths"`
|
||||
}
|
||||
|
||||
registryDockerConfig struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
)
|
||||
|
||||
func (kcl *KubeClient) DeleteRegistrySecret(registry *portainer.Registry, namespace string) error {
|
||||
err := kcl.cli.CoreV1().Secrets(namespace).Delete(registrySecretName(registry), &metav1.DeleteOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return errors.Wrap(err, "failed removing secret")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) CreateRegistrySecret(registry *portainer.Registry, namespace string) error {
|
||||
config := dockerConfig{
|
||||
Auths: map[string]registryDockerConfig{
|
||||
registry.URL: {
|
||||
Username: registry.Username,
|
||||
Password: registry.Password,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
configByte, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed marshal config")
|
||||
}
|
||||
|
||||
secret := &v1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: registrySecretName(registry),
|
||||
Annotations: map[string]string{
|
||||
"portainer.io/registry.id": strconv.Itoa(int(registry.ID)),
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
secretDockerConfigKey: configByte,
|
||||
},
|
||||
Type: v1.SecretTypeDockerConfigJson,
|
||||
}
|
||||
|
||||
_, err = kcl.cli.CoreV1().Secrets(namespace).Create(secret)
|
||||
if err != nil && !k8serrors.IsAlreadyExists(err) {
|
||||
return errors.Wrap(err, "failed saving secret")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (cli *KubeClient) IsRegistrySecret(namespace, secretName string) (bool, error) {
|
||||
secret, err := cli.cli.CoreV1().Secrets(namespace).Get(secretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
isSecret := secret.Type == v1.SecretTypeDockerConfigJson
|
||||
|
||||
return isSecret, nil
|
||||
|
||||
}
|
||||
|
||||
func registrySecretName(registry *portainer.Registry) string {
|
||||
return fmt.Sprintf("registry-%d", registry.ID)
|
||||
}
|
||||
5
api/kubernetes/privateregistries/labels.go
Normal file
5
api/kubernetes/privateregistries/labels.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package privateregistries
|
||||
|
||||
const (
|
||||
RegistryIDLabel = "portainer.io/registry.id"
|
||||
)
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
@@ -26,18 +27,18 @@ func NewService() *Service {
|
||||
// Authenticate takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint.
|
||||
// On success, it will then return the username and token expiry time associated to authenticated user by fetching this information
|
||||
// from the resource server and matching it with the user identifier setting.
|
||||
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
|
||||
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, *time.Time, error) {
|
||||
token, err := getOAuthToken(code, configuration)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
username, err := getUsername(token.AccessToken, configuration)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err)
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
return username, nil
|
||||
return username, &token.Expiry, nil
|
||||
}
|
||||
|
||||
func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package portainer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
@@ -512,12 +511,14 @@ type (
|
||||
Registry struct {
|
||||
// Registry Identifier
|
||||
ID RegistryID `json:"Id" example:"1"`
|
||||
// Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab)
|
||||
Type RegistryType `json:"Type" enums:"1,2,3,4"`
|
||||
// Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab, 5 - ProGet, 6 - DockerHub)
|
||||
Type RegistryType `json:"Type" enums:"1,2,3,4,5,6"`
|
||||
// Registry Name
|
||||
Name string `json:"Name" example:"my-registry"`
|
||||
// URL or IP address of the Docker registry
|
||||
URL string `json:"URL" example:"registry.mydomain.tld:2375"`
|
||||
// Base URL, introduced for ProGet registry
|
||||
BaseURL string `json:"BaseURL" example:"registry.mydomain.tld:2375"`
|
||||
// Is authentication against this registry enabled
|
||||
Authentication bool `json:"Authentication" example:"true"`
|
||||
// Username used to authenticate against this registry
|
||||
@@ -527,15 +528,28 @@ type (
|
||||
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
||||
Gitlab GitlabRegistryData `json:"Gitlab"`
|
||||
Quay QuayRegistryData `json:"Quay"`
|
||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||
RegistryAccesses RegistryAccesses `json:"RegistryAccesses"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 31
|
||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||
// Deprecated in DBVersion == 31
|
||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||
|
||||
// Deprecated in DBVersion == 18
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
// Deprecated in DBVersion == 18
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
}
|
||||
|
||||
RegistryAccesses map[EndpointID]RegistryAccessPolicies
|
||||
|
||||
RegistryAccessPolicies struct {
|
||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||
Namespaces []string `json:"Namespaces"`
|
||||
}
|
||||
|
||||
// RegistryID represents a registry identifier
|
||||
RegistryID int
|
||||
|
||||
@@ -1018,7 +1032,6 @@ type (
|
||||
CheckCurrentEdition() error
|
||||
BackupTo(w io.Writer) error
|
||||
|
||||
DockerHub() DockerHubService
|
||||
CustomTemplate() CustomTemplateService
|
||||
EdgeGroup() EdgeGroupService
|
||||
EdgeJob() EdgeJobService
|
||||
@@ -1049,12 +1062,6 @@ type (
|
||||
CreateSignature(message string) (string, error)
|
||||
}
|
||||
|
||||
// DockerHubService represents a service for managing the DockerHub object
|
||||
DockerHubService interface {
|
||||
DockerHub() (*DockerHub, error)
|
||||
UpdateDockerHub(registry *DockerHub) error
|
||||
}
|
||||
|
||||
// DockerSnapshotter represents a service used to create Docker endpoint snapshots
|
||||
DockerSnapshotter interface {
|
||||
CreateSnapshot(endpoint *Endpoint) (*DockerSnapshot, error)
|
||||
@@ -1165,15 +1172,17 @@ type (
|
||||
KubeClient interface {
|
||||
SetupUserServiceAccount(userID int, teamIDs []int) error
|
||||
GetServiceAccountBearerToken(userID int) (string, error)
|
||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
||||
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
||||
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
|
||||
DeleteRegistrySecret(registry *Registry, namespace string) error
|
||||
CreateRegistrySecret(registry *Registry, namespace string) error
|
||||
IsRegistrySecret(namespace, secretName string) (bool, error)
|
||||
}
|
||||
|
||||
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
|
||||
KubernetesDeployer interface {
|
||||
Deploy(request *http.Request, endpoint *Endpoint, data string, namespace string) (string, error)
|
||||
Deploy(endpoint *Endpoint, data string, namespace string) (string, error)
|
||||
ConvertCompose(data string) ([]byte, error)
|
||||
}
|
||||
|
||||
@@ -1191,7 +1200,7 @@ type (
|
||||
|
||||
// OAuthService represents a service used to authenticate users using OAuth
|
||||
OAuthService interface {
|
||||
Authenticate(code string, configuration *OAuthSettings) (string, error)
|
||||
Authenticate(code string, configuration *OAuthSettings) (string, *time.Time, error)
|
||||
}
|
||||
|
||||
// RegistryService represents a service for managing registry data
|
||||
@@ -1266,7 +1275,7 @@ type (
|
||||
|
||||
// SwarmStackManager represents a service to manage Swarm stacks
|
||||
SwarmStackManager interface {
|
||||
Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint)
|
||||
Login(registries []Registry, endpoint *Endpoint)
|
||||
Logout(endpoint *Endpoint) error
|
||||
Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
|
||||
Remove(stack *Stack, endpoint *Endpoint) error
|
||||
@@ -1343,9 +1352,9 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.6.3"
|
||||
APIVersion = "2.6.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 30
|
||||
DBVersion = 32
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
@@ -1491,6 +1500,10 @@ const (
|
||||
CustomRegistry
|
||||
// GitlabRegistry represents a gitlab registry
|
||||
GitlabRegistry
|
||||
// ProGetRegistry represents a proget registry
|
||||
ProGetRegistry
|
||||
// DockerHubRegistry represents a dockerhub registry
|
||||
DockerHubRegistry
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1187,17 +1187,22 @@ definitions:
|
||||
TeamAccessPolicies:
|
||||
$ref: '#/definitions/portainer.TeamAccessPolicies'
|
||||
Type:
|
||||
description: Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab)
|
||||
description: Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab, 5 - ProGet)
|
||||
enum:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
type: integer
|
||||
URL:
|
||||
description: URL or IP address of the Docker registry
|
||||
example: registry.mydomain.tld:2375
|
||||
type: string
|
||||
BaseURL:
|
||||
description: Base URL or IP address of the ProGet registry
|
||||
example: registry.mydomain.tld:2375
|
||||
type: string
|
||||
UserAccessPolicies:
|
||||
$ref: '#/definitions/portainer.UserAccessPolicies'
|
||||
Username:
|
||||
@@ -1827,18 +1832,23 @@ definitions:
|
||||
type: string
|
||||
type:
|
||||
description: 'Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container
|
||||
registry), 3 (custom registry) or 4 (Gitlab registry)'
|
||||
registry), 3 (custom registry), 4 (Gitlab registry) or 5 (ProGet registry)'
|
||||
enum:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
example: 1
|
||||
type: integer
|
||||
url:
|
||||
description: URL or IP address of the Docker registry
|
||||
example: registry.mydomain.tld:2375
|
||||
type: string
|
||||
baseUrl:
|
||||
description: Base URL or IP address of the ProGet registry
|
||||
example: registry.mydomain.tld:2375
|
||||
type: string
|
||||
username:
|
||||
description: Username used to authenticate against this registry. Required
|
||||
when Authentication is true
|
||||
@@ -1871,6 +1881,10 @@ definitions:
|
||||
description: URL or IP address of the Docker registry
|
||||
example: registry.mydomain.tld:2375
|
||||
type: string
|
||||
baseUrl:
|
||||
description: Base URL or IP address of the ProGet registry
|
||||
example: registry.mydomain.tld:2375
|
||||
type: string
|
||||
userAccessPolicies:
|
||||
$ref: '#/definitions/portainer.UserAccessPolicies'
|
||||
username:
|
||||
|
||||
@@ -4,10 +4,10 @@ angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub);
|
||||
|
||||
function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) {
|
||||
return $resource(
|
||||
`${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub`,
|
||||
`${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub/:registryId`,
|
||||
{},
|
||||
{
|
||||
limits: { method: 'GET' },
|
||||
limits: { method: 'GET', params: { registryId: '@registryId' } },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
angular
|
||||
.module('portainer')
|
||||
.constant('API_ENDPOINT_AUTH', 'api/auth')
|
||||
.constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub')
|
||||
.constant('API_ENDPOINT_CUSTOM_TEMPLATES', 'api/custom_templates')
|
||||
.constant('API_ENDPOINT_EDGE_GROUPS', 'api/edge_groups')
|
||||
.constant('API_ENDPOINT_EDGE_JOBS', 'api/edge_jobs')
|
||||
|
||||
@@ -591,6 +591,26 @@ angular.module('portainer.docker', ['portainer.app']).config([
|
||||
},
|
||||
};
|
||||
|
||||
const registries = {
|
||||
name: 'docker.registries',
|
||||
url: '/registries',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'endpointRegistriesView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const registryAccess = {
|
||||
name: 'docker.registries.access',
|
||||
url: '/:id/access',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'dockerRegistryAccessView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(configs);
|
||||
$stateRegistryProvider.register(config);
|
||||
$stateRegistryProvider.register(configCreation);
|
||||
@@ -641,5 +661,7 @@ angular.module('portainer.docker', ['portainer.app']).config([
|
||||
$stateRegistryProvider.register(volumeBrowse);
|
||||
$stateRegistryProvider.register(volumeCreation);
|
||||
$stateRegistryProvider.register(dockerFeaturesConfiguration);
|
||||
$stateRegistryProvider.register(registries);
|
||||
$stateRegistryProvider.register(registryAccess);
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -35,17 +35,19 @@
|
||||
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess && !$ctrl.offlineMode">
|
||||
<a ui-sref="docker.events({endpointId: $ctrl.endpointId})" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
|
||||
<a ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
|
||||
<li class="sidebar-list">
|
||||
<a ng-if="$ctrl.standaloneManagement" ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
|
||||
<a ng-if="$ctrl.swarmManagement" ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
|
||||
|
||||
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.swarm'].includes($ctrl.currentRouteName)">
|
||||
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
|
||||
<a ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
|
||||
|
||||
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.host'].includes($ctrl.currentRouteName)">
|
||||
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
<div
|
||||
ng-if="$ctrl.adminAccess && ['docker.swarm', 'docker.host', 'docker.registries', 'docker.registries.access', 'docker.featuresConfiguration'].includes($ctrl.currentRouteName)"
|
||||
>
|
||||
<div class="sidebar-sublist">
|
||||
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-sublist">
|
||||
<a ui-sref="docker.registries({endpointId: $ctrl.endpointId})" ui-sref-active="active">Registries</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
import EndpointHelper from '@/portainer/helpers/endpointHelper';
|
||||
|
||||
export default class porImageRegistryContainerController {
|
||||
/* @ngInject */
|
||||
constructor(EndpointHelper, DockerHubService, Notifications) {
|
||||
this.EndpointHelper = EndpointHelper;
|
||||
constructor(DockerHubService, Notifications) {
|
||||
this.DockerHubService = DockerHubService;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.pullRateLimits = null;
|
||||
}
|
||||
|
||||
$onChanges({ isDockerHubRegistry }) {
|
||||
if (isDockerHubRegistry && isDockerHubRegistry.currentValue) {
|
||||
$onChanges({ registryId }) {
|
||||
if (registryId) {
|
||||
this.fetchRateLimits();
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.setValidity =
|
||||
this.setValidity ||
|
||||
(() => {
|
||||
/* noop */
|
||||
});
|
||||
}
|
||||
|
||||
async fetchRateLimits() {
|
||||
this.pullRateLimits = null;
|
||||
if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) {
|
||||
try {
|
||||
this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint);
|
||||
this.setValidity(!this.pullRateLimits.limit || (this.pullRateLimits.limit && this.pullRateLimits.remaining >= 0));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed loading DockerHub pull rate limits', e);
|
||||
this.setValidity(true);
|
||||
}
|
||||
} else {
|
||||
if (!EndpointHelper.isAgentEndpoint(this.endpoint) && !EndpointHelper.isLocalEndpoint(this.endpoint)) {
|
||||
this.setValidity(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint, this.registryId || 0);
|
||||
this.setValidity(this.pullRateLimits.remaining >= 0);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed loading DockerHub pull rate limits', e);
|
||||
this.setValidity(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="form-group" ng-if="$ctrl.isDockerHubRegistry && $ctrl.pullRateLimits">
|
||||
<div class="form-group" ng-if="$ctrl.pullRateLimits">
|
||||
<div class="col-sm-12 small">
|
||||
<div ng-if="$ctrl.pullRateLimits.remaining > 0" class="text-muted">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
|
||||
@@ -5,10 +5,12 @@ import controller from './por-image-registry-rate-limits.controller';
|
||||
angular.module('portainer.docker').component('porImageRegistryRateLimits', {
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
registry: '<',
|
||||
setValidity: '<',
|
||||
isAdmin: '<',
|
||||
isDockerHubRegistry: '<',
|
||||
isAuthenticated: '<',
|
||||
registryId: '<',
|
||||
},
|
||||
controller,
|
||||
transclude: {
|
||||
|
||||
@@ -5,18 +5,21 @@ import { RegistryTypes } from '@/portainer/models/registryTypes';
|
||||
|
||||
class porImageRegistryController {
|
||||
/* @ngInject */
|
||||
constructor($async, $scope, ImageHelper, RegistryService, DockerHubService, ImageService, Notifications) {
|
||||
constructor($async, $scope, ImageHelper, RegistryService, EndpointService, ImageService, Notifications) {
|
||||
this.$async = $async;
|
||||
this.$scope = $scope;
|
||||
this.ImageHelper = ImageHelper;
|
||||
this.RegistryService = RegistryService;
|
||||
this.DockerHubService = DockerHubService;
|
||||
this.EndpointService = EndpointService;
|
||||
this.ImageService = ImageService;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.onRegistryChange = this.onRegistryChange.bind(this);
|
||||
|
||||
this.registries = [];
|
||||
this.images = [];
|
||||
this.defaultRegistry = new DockerHubViewModel();
|
||||
|
||||
this.$scope.$watch(() => this.model.Registry, this.onRegistryChange);
|
||||
}
|
||||
|
||||
@@ -40,7 +43,7 @@ class porImageRegistryController {
|
||||
const registryImages = _.filter(this.images, (image) => _.includes(image, url));
|
||||
images = _.map(registryImages, (image) => _.replace(image, new RegExp(url + '/?'), ''));
|
||||
} else {
|
||||
const registries = _.filter(this.availableRegistries, (reg) => this.isKnownRegistry(reg));
|
||||
const registries = _.filter(this.registries, (reg) => this.isKnownRegistry(reg));
|
||||
const registryImages = _.flatMap(registries, (registry) => _.filter(this.images, (image) => _.includes(image, registry.URL)));
|
||||
const imagesWithoutKnown = _.difference(this.images, registryImages);
|
||||
images = _.filter(imagesWithoutKnown, (image) => !this.ImageHelper.imageContainsURL(image));
|
||||
@@ -49,7 +52,7 @@ class porImageRegistryController {
|
||||
}
|
||||
|
||||
isDockerHubRegistry() {
|
||||
return this.model.UseRegistry && this.model.Registry.Name === 'DockerHub';
|
||||
return this.model.UseRegistry && (this.model.Registry.Type === RegistryTypes.DOCKERHUB || this.model.Registry.Type === RegistryTypes.ANONYMOUS);
|
||||
}
|
||||
|
||||
async onRegistryChange() {
|
||||
@@ -63,29 +66,49 @@ class porImageRegistryController {
|
||||
return this.getRegistryURL(this.model.Registry) || 'docker.io';
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
try {
|
||||
const [registries, dockerhub, images] = await Promise.all([
|
||||
this.RegistryService.registries(),
|
||||
this.DockerHubService.dockerhub(),
|
||||
this.autoComplete ? this.ImageService.images() : [],
|
||||
]);
|
||||
this.images = this.ImageService.getUniqueTagListFromImages(images);
|
||||
this.availableRegistries = _.concat(dockerhub, registries);
|
||||
async reloadRegistries() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
|
||||
this.registries = _.concat(this.defaultRegistry, registries);
|
||||
|
||||
const id = this.model.Registry.Id;
|
||||
if (!id) {
|
||||
this.model.Registry = dockerhub;
|
||||
} else {
|
||||
this.model.Registry = _.find(this.availableRegistries, { Id: id });
|
||||
const id = this.model.Registry.Id;
|
||||
const registry = _.find(this.registries, { Id: id });
|
||||
if (!registry) {
|
||||
this.model.Registry = this.defaultRegistry;
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
|
||||
});
|
||||
}
|
||||
|
||||
async loadImages() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
if (!this.autoComplete) {
|
||||
this.images = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const images = await this.ImageService.images();
|
||||
this.images = this.ImageService.getUniqueTagListFromImages(images);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve images');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$onChanges({ namespace, endpoint }) {
|
||||
if ((namespace || endpoint) && this.endpoint.Id) {
|
||||
this.reloadRegistries();
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
return this.$async(async () => {
|
||||
await this.loadImages();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user