feat(app): rework private registries and support private registries in kubernetes EE-30 (#5131)

* feat(app): rework private registries and support private registries in kubernetes

[EE-30]

feat(api): backport private registries backend changes (#5072)

* feat(api/bolt): backport bolt changes

* feat(api/exec): backport exec changes

* feat(api/http): backport http/handler/dockerhub changes

* feat(api/http): backport http/handler/endpoints changes

* feat(api/http): backport http/handler/registries changes

* feat(api/http): backport http/handler/stacks changes

* feat(api/http): backport http/handler changes

* feat(api/http): backport http/proxy/factory/azure changes

* feat(api/http): backport http/proxy/factory/docker changes

* feat(api/http): backport http/proxy/factory/utils changes

* feat(api/http): backport http/proxy/factory/kubernetes changes

* feat(api/http): backport http/proxy/factory changes

* feat(api/http): backport http/security changes

* feat(api/http): backport http changes

* feat(api/internal): backport internal changes

* feat(api): backport api changes

* feat(api/kubernetes): backport kubernetes changes

* fix(api/http): changes on backend following backport

feat(app): backport private registries frontend changes (#5056)

* feat(app/docker): backport docker/components changes

* feat(app/docker): backport docker/helpers changes

* feat(app/docker): backport docker/views/container changes

* feat(app/docker): backport docker/views/images changes

* feat(app/docker): backport docker/views/registries changes

* feat(app/docker): backport docker/views/services changes

* feat(app/docker): backport docker changes

* feat(app/kubernetes): backport kubernetes/components changes

* feat(app/kubernetes): backport kubernetes/converters changes

* feat(app/kubernetes): backport kubernetes/models changes

* feat(app/kubernetes): backport kubernetes/registries changes

* feat(app/kubernetes): backport kubernetes/services changes

* feat(app/kubernetes): backport kubernetes/views/applications changes

* feat(app/kubernetes): backport kubernetes/views/configurations changes

* feat(app/kubernetes): backport kubernetes/views/configure changes

* feat(app/kubernetes): backport kubernetes/views/resource-pools changes

* feat(app/kubernetes): backport kubernetes/views changes

* feat(app/portainer): backport portainer/components/accessManagement changes

* feat(app/portainer): backport portainer/components/datatables changes

* feat(app/portainer): backport portainer/components/forms changes

* feat(app/portainer): backport portainer/components/registry-details changes

* feat(app/portainer): backport portainer/models changes

* feat(app/portainer): backport portainer/rest changes

* feat(app/portainer): backport portainer/services changes

* feat(app/portainer): backport portainer/views changes

* feat(app/portainer): backport portainer changes

* feat(app): backport app changes

* config(project): gitignore + jsconfig changes

gitignore all files under api/cmd/portainer but main.go and enable Code Editor autocomplete on import ... from '@/...'

fix(app): fix pull rate limit checker

fix(app/registries): sidebar menus and registry accesses users filtering

fix(api): add missing kube client factory

fix(kube): fetch dockerhub pull limits (#5133)

fix(app): pre review fixes (#5142)

* fix(app/registries): remove checkbox for endpointRegistries view

* fix(endpoints): allow access to default namespace

* fix(docker): fetch pull limits

* fix(kube/ns): show selected registries for non admin

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>

chore(webpack): ignore missing sourcemaps

fix(registries): fetch registry config from url

feat(kube/registries): ignore not found when deleting secret

feat(db): move migration to db 31

fix(registries): fix bugs in PR EE-869 (#5169)

* fix(registries): hide role

* fix(endpoints): set empty access policy to edge endpoint

* fix(registry): remove double arguments

* fix(admin): ignore warning

* feat(kube/configurations): tag registry secrets (#5157)

* feat(kube/configurations): tag registry secrets

* feat(kube/secrets): show registry secrets for admins

* fix(registries): move dockerhub to beginning

* refactor(registries): use endpoint scoped registries

feat(registries): filter by namespace if supplied

feat(access-managment): filter users for registry (#5191)

* refactor(access-manage): move users selector to component

* feat(access-managment): filter users for registry

refactor(registries): sync code with CE (#5200)

* refactor(registry): add inspect handler under endpoints

* refactor(endpoint): sync endpoint_registries_list

* refactor(endpoints): sync registry_access

* fix(db): rename migration functions

* fix(registries): show accesses for admin

* fix(kube): set token on transport

* refactor(kube): move secret help to bottom

* fix(kuberentes): remove shouldLog parameter

* style(auth): add description of security.IsAdmin

* feat(security): allow admin access to registry

* feat(edge): connect to edge endpoint when creating client

* style(portainer): change deprecation version

* refactor(sidebar): hide manage

* refactor(containers): revert changes

* style(container): remove whitespace

* fix(endpoint): add handler to registy on endpointService

* refactor(image): use endpointService.registries

* fix(kueb/namespaces): rename resource pool to namespace

* fix(kube/namespace): move selected registries

* fix(api/registries): hide accesses on registry creation

Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>

refactor(api): remove code duplication after rebase

fix(app/registries): replace last registry api usage by endpoint registry api

fix(api/endpoints): update registry access policies on endpoint deletion (#5226)

[EE-1027]

fix(db): update db version

* fix(dockerhub): fetch rate limits

* fix(registry/tests): supply restricred context

* fix(registries): show proget registry only when selected

* fix(registry): create dockerhub registry

* feat(db): move migrations to db 32

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
528
LP B 2021-07-14 11:15:21 +02:00 committed by GitHub
parent 0f5407da40
commit 179df06267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
175 changed files with 3757 additions and 2544 deletions

View File

@ -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)

View File

@ -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
View 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
View File

@ -0,0 +1 @@
package log

View 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, &registry)
}
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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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.

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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,

View File

@ -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 := &registries[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)
}

View File

@ -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)

View 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)
}

View 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(&registries[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
}
}

View 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
}

View File

@ -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
}

View File

@ -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
@ -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"):

View File

@ -8,20 +8,25 @@ import (
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.
@ -34,8 +39,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
func newHandler(bouncer *security.RequestBouncer) *Handler {
return &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
}
@ -60,4 +65,15 @@ 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
}

View File

@ -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 := &registryConfigurePayload{}
@ -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}

View File

@ -10,13 +10,15 @@ 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), 4 (Gitlab registry) or 5 (ProGet registry)
Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4,5"`
// 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/feed" validate:"required"`
// BaseURL required for ProGet registry
@ -45,9 +47,9 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
}
switch payload.Type {
case portainer.QuayRegistry, portainer.AzureRegistry, portainer.CustomRegistry, portainer.GitlabRegistry, portainer.ProGetRegistry:
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) or 5 (ProGet registry)")
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 == "" {
@ -71,24 +73,41 @@ func (payload *registryCreatePayload) Validate(_ *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,
BaseURL: payload.BaseURL,
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)
@ -96,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)
}

View File

@ -8,6 +8,7 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/stretchr/testify/assert"
)
@ -82,6 +83,14 @@ func TestHandler_registryCreate(t *testing.T) {
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{

View File

@ -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}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -9,18 +9,25 @@ 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 {
Name *string `json:",omitempty" example:"my-registry" validate:"required"`
URL *string `json:",omitempty" example:"registry.mydomain.tld:2375/feed" validate:"required"`
BaseURL *string `json:",omitempty" example:"registry.mydomain.tld:2375"`
Authentication *bool `json:",omitempty" example:"false" validate:"required"`
Username *string `json:",omitempty" example:"registry_user"`
Password *string `json:",omitempty" example:"registry_password"`
UserAccessPolicies portainer.UserAccessPolicies `json:",omitempty"`
TeamAccessPolicies portainer.TeamAccessPolicies `json:",omitempty"`
Quay *portainer.QuayRegistryData
// Name that will be used to identify this registry
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"`
RegistryAccesses *portainer.RegistryAccesses
Quay *portainer.QuayRegistryData
}
func (payload *registryUpdatePayload) Validate(r *http.Request) error {
@ -44,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}
@ -62,23 +71,17 @@ 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")}
}
}
registry.URL = *payload.URL
}
shouldUpdateSecrets := false
if registry.Type == portainer.ProGetRegistry && payload.BaseURL != nil {
registry.BaseURL = *payload.BaseURL
@ -87,6 +90,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
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
}

View File

@ -8,6 +8,7 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/stretchr/testify/assert"
)
@ -48,6 +49,14 @@ func TestHandler_registryUpdate(t *testing.T) {
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{})

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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")
}

View File

@ -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 = &registryAuthenticationHeader{
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(&registry, accessContext.userID, accessContext.teamMemberships))) {
if registry.ID == registryId &&
(accessContext.isAdmin ||
security.AuthorizedRegistryAccess(&registry, accessContext.user, accessContext.teamMemberships, accessContext.endpointID)) {
matchingRegistry = &registry
break
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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"
)
@ -169,12 +171,31 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
// volume browser request
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
}
@ -397,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 {
@ -433,7 +454,7 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
}
if !securitySettings.AllowVolumeBrowserForRegularUsers {
return responseutils.WriteAccessDeniedResponse()
return utils.WriteAccessDeniedResponse()
}
}
@ -468,12 +489,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
}
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()
}
}
@ -537,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
}
@ -556,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) {
@ -619,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)
@ -632,15 +653,15 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) (
}
accessContext := &registryAccessContext{
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 {
@ -648,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)

View File

@ -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) {

View File

@ -39,7 +39,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
return nil, err
}
transport, err := kubernetes.NewLocalTransport(tokenManager)
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.ID, 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)
proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
return proxy, nil
}

View File

@ -0,0 +1,60 @@
package kubernetes
import (
"crypto/tls"
"net/http"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/kubernetes/cli"
)
type agentTransport struct {
*baseTransport
signatureService portainer.DigitalSignatureService
}
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) *agentTransport {
transport := &agentTransport{
baseTransport: newBaseTransport(
&http.Transport{
TLSClientConfig: tlsConfig,
},
tokenManager,
endpoint,
k8sClientFactory,
dataStore,
),
signatureService: signatureService,
}
return transport
}
// 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)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
if strings.HasPrefix(request.URL.Path, "/v2") {
err := decorateAgentRequest(request, transport.dataStore)
if err != nil {
return nil, err
}
}
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
return transport.baseTransport.RoundTrip(request)
}

View File

@ -0,0 +1,57 @@
package kubernetes
import (
"net/http"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/kubernetes/cli"
)
type edgeTransport struct {
*baseTransport
reverseTunnelService portainer.ReverseTunnelService
}
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) *edgeTransport {
transport := &edgeTransport{
baseTransport: newBaseTransport(
&http.Transport{},
tokenManager,
endpoint,
k8sClientFactory,
dataStore,
),
reverseTunnelService: reverseTunnelService,
}
return transport
}
// 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)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
if strings.HasPrefix(request.URL.Path, "/v2") {
err := decorateAgentRequest(request, transport.dataStore)
if err != nil {
return nil, err
}
}
response, err := transport.baseTransport.RoundTrip(request)
if err == nil {
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID)
} else {
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID)
}
return response, err
}

View File

@ -0,0 +1,45 @@
package kubernetes
import (
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/kubernetes/cli"
)
type localTransport struct {
*baseTransport
}
// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
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
}
transport := &localTransport{
baseTransport: newBaseTransport(
&http.Transport{
TLSClientConfig: config,
},
tokenManager,
endpoint,
k8sClientFactory,
dataStore,
),
}
return transport, nil
}
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
_, err := transport.prepareRoundTrip(request)
if err != nil {
return nil, err
}
return transport.baseTransport.RoundTrip(request)
}

View File

@ -0,0 +1,45 @@
package kubernetes
import (
"net/http"
portainer "github.com/portainer/portainer/api"
)
func (transport *baseTransport) proxyNamespaceDeleteOperation(request *http.Request, namespace string) (*http.Response, error) {
registries, err := transport.dataStore.Registry().Registries()
if err != nil {
return nil, err
}
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, &registry)
if err != nil {
return nil, err
}
}
}
}
return transport.executeKubernetesRequest(request)
}

View 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
}

View File

@ -2,153 +2,107 @@ package kubernetes
import (
"bytes"
"crypto/tls"
"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"
"github.com/portainer/portainer/api/crypto"
)
type (
localTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
endpointIdentifier portainer.EndpointID
}
agentTransport struct {
dataStore portainer.DataStore
httpTransport *http.Transport
tokenManager *tokenManager
signatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
}
edgeTransport struct {
dataStore portainer.DataStore
httpTransport *http.Transport
tokenManager *tokenManager
reverseTunnelService portainer.ReverseTunnelService
endpointIdentifier portainer.EndpointID
}
)
// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) {
config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
if err != nil {
return nil, err
}
transport := &localTransport{
httpTransport: &http.Transport{
TLSClientConfig: config,
},
tokenManager: tokenManager,
}
return transport, nil
type baseTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
endpoint *portainer.Endpoint
k8sClientFactory *cli.ClientFactory
dataStore portainer.DataStore
}
// 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.endpointIdentifier)
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,
k8sClientFactory: k8sClientFactory,
dataStore: dataStore,
}
}
// #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)
case strings.HasPrefix(requestPath, "/namespaces"):
return transport.proxyNamespacedRequest(request, requestPath)
default:
return transport.executeKubernetesRequest(request)
}
}
func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fullRequestPath string) (*http.Response, error) {
requestPath := strings.TrimPrefix(fullRequestPath, "/namespaces/")
split := strings.SplitN(requestPath, "/", 2)
namespace := split[0]
requestPath = ""
if len(split) > 1 {
requestPath = split[1]
}
switch {
case strings.HasPrefix(requestPath, "secrets"):
return transport.proxySecretRequest(request, namespace, requestPath)
case requestPath == "" && request.Method == "DELETE":
return transport.proxyNamespaceDeleteOperation(request, namespace)
default:
return transport.executeKubernetesRequest(request)
}
}
func (transport *baseTransport) executeKubernetesRequest(request *http.Request) (*http.Response, error) {
resp, err := transport.httpTransport.RoundTrip(request)
return resp, err
}
// #endregion
// #region ROUND TRIP
func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string, error) {
token, err := getRoundTripToken(request, transport.tokenManager)
if err != nil {
return nil, err
return "", err
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return transport.httpTransport.RoundTrip(request)
}
// 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) *agentTransport {
transport := &agentTransport{
dataStore: datastore,
httpTransport: &http.Transport{
TLSClientConfig: tlsConfig,
},
tokenManager: tokenManager,
signatureService: signatureService,
}
return transport
return token, nil
}
// 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.endpointIdentifier)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
if strings.HasPrefix(request.URL.Path, "/v2") {
decorateAgentRequest(request, transport.dataStore)
}
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
return transport.httpTransport.RoundTrip(request)
func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response, error) {
return transport.proxyKubernetesRequest(request)
}
// NewEdgeTransport 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, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport {
transport := &edgeTransport{
dataStore: datastore,
httpTransport: &http.Transport{},
tokenManager: tokenManager,
reverseTunnelService: reverseTunnelService,
endpointIdentifier: endpointIdentifier,
}
return transport
}
// 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.endpointIdentifier)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
if strings.HasPrefix(request.URL.Path, "/v2") {
decorateAgentRequest(request, transport.dataStore)
}
response, err := transport.httpTransport.RoundTrip(request)
if err == nil {
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpointIdentifier)
} else {
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpointIdentifier)
}
return response, err
}
func getRoundTripToken(
request *http.Request,
tokenManager *tokenManager,
endpointIdentifier portainer.EndpointID,
) (string, error) {
func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return "", err
@ -168,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

View File

@ -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
}

View 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)
}

View 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)
}

View File

@ -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")
}

View File

@ -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

View File

@ -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))
})
}

View File

@ -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)
}

View File

@ -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(&registry, context.UserID, context.UserMemberships) {
filteredRegistries = append(filteredRegistries, registry)
}
filteredRegistries := []portainer.Registry{}
for _, registry := range registries {
if AuthorizedRegistryAccess(&registry, user, teamMemberships, endpointID) {
filteredRegistries = append(filteredRegistries, registry)
}
}

View File

@ -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
@ -219,7 +217,6 @@ func (server *Server) Start() error {
AuthHandler: authHandler,
BackupHandler: backupHandler,
CustomTemplatesHandler: customTemplatesHandler,
DockerHubHandler: dockerHubHandler,
EdgeGroupsHandler: edgeGroupsHandler,
EdgeJobsHandler: edgeJobsHandler,
EdgeStacksHandler: edgeStacksHandler,

View File

@ -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,

View File

@ -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

View File

@ -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 }

View File

@ -12,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) {
@ -80,28 +102,6 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sN
return false
}
// 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)

View File

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"strconv"
"time"
cmap "github.com/orcaman/concurrent-map"
@ -17,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
@ -31,8 +33,9 @@ type (
)
// 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,
@ -133,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 {

View 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)
}

View File

@ -0,0 +1,5 @@
package privateregistries
const (
RegistryIDLabel = "portainer.io/registry.id"
)

View File

@ -511,8 +511,8 @@ type (
Registry struct {
// Registry Identifier
ID RegistryID `json:"Id" example:"1"`
// Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab, 5 - ProGet)
Type RegistryType `json:"Type" enums:"1,2,3,4,5"`
// 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
@ -528,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
@ -1019,7 +1032,6 @@ type (
CheckCurrentEdition() error
BackupTo(w io.Writer) error
DockerHub() DockerHubService
CustomTemplate() CustomTemplateService
EdgeGroup() EdgeGroupService
EdgeJob() EdgeJobService
@ -1050,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)
@ -1169,6 +1175,9 @@ type (
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
@ -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
@ -1345,7 +1354,7 @@ const (
// APIVersion is the version number of the Portainer API
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
@ -1493,6 +1502,8 @@ const (
GitlabRegistry
// ProGetRegistry represents a proget registry
ProGetRegistry
// DockerHubRegistry represents a dockerhub registry
DockerHubRegistry
)
const (

View File

@ -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' } },
}
);
}

View File

@ -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')

View File

@ -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);
},
]);

View File

@ -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>

View File

@ -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.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);
}
}

View File

@ -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>

View File

@ -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: {

View File

@ -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();
});
}
}

View File

@ -6,10 +6,9 @@
</label>
<div ng-class="$ctrl.inputClass">
<select
ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Name"
ng-model="$ctrl.model.Registry"
id="image_registry"
selected-item-id="ctrl.selectedItemId"
class="form-control"
></select>
</div>
@ -86,12 +85,13 @@
<div ng-transclude></div>
<por-image-registry-rate-limits
ng-show="$ctrl.checkRateLimits"
is-docker-hub-registry="$ctrl.isDockerHubRegistry()"
ng-if="$ctrl.checkRateLimits && $ctrl.isDockerHubRegistry()"
endpoint="$ctrl.endpoint"
registry="$ctrl.model.Registry"
set-validity="$ctrl.setValidity"
is-authenticated="$ctrl.model.Registry.Authentication"
is-admin="$ctrl.isAdmin"
registry-id="$ctrl.model.Registry.Id"
>
</por-image-registry-rate-limits>
</div>

View File

@ -3,7 +3,6 @@ angular.module('portainer.docker').component('porImageRegistry', {
controller: 'porImageRegistryController',
bindings: {
model: '=', // must be of type PorImageRegistryModel
pullWarning: '<',
autoComplete: '<',
labelClass: '@',
inputClass: '@',
@ -12,6 +11,7 @@ angular.module('portainer.docker').component('porImageRegistry', {
checkRateLimits: '<',
onImageChange: '&',
setValidity: '<',
namespace: '<',
},
require: {
form: '^form',

View File

@ -1,77 +1,85 @@
import _ from 'lodash-es';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import { RegistryTypes } from 'Portainer/models/registryTypes';
angular.module('portainer.docker').factory('ImageHelper', [
function ImageHelperFactory() {
'use strict';
angular.module('portainer.docker').factory('ImageHelper', ImageHelperFactory);
function ImageHelperFactory() {
return {
isValidTag,
createImageConfigForContainer,
getImagesNamesForDownload,
removeDigestFromRepository,
imageContainsURL,
};
var helper = {};
function isValidTag(tag) {
return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
}
helper.isValidTag = isValidTag;
helper.createImageConfigForContainer = createImageConfigForContainer;
helper.getImagesNamesForDownload = getImagesNamesForDownload;
helper.removeDigestFromRepository = removeDigestFromRepository;
helper.imageContainsURL = imageContainsURL;
function getImagesNamesForDownload(images) {
var names = images.map(function (image) {
return image.RepoTags[0] !== '<none>:<none>' ? image.RepoTags[0] : image.Id;
});
return {
names: names,
};
}
function isValidTag(tag) {
return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
/**
*
* @param {PorImageRegistryModel} registry
*/
function createImageConfigForContainer(imageModel) {
return {
fromImage: buildImageFullURI(imageModel),
};
}
function imageContainsURL(image) {
const split = _.split(image, '/');
const url = split[0];
if (split.length > 1) {
return _.includes(url, '.') || _.includes(url, ':');
}
return false;
}
function getImagesNamesForDownload(images) {
var names = images.map(function (image) {
return image.RepoTags[0] !== '<none>:<none>' ? image.RepoTags[0] : image.Id;
});
return {
names: names,
};
}
function removeDigestFromRepository(repository) {
return repository.split('@sha')[0];
}
}
/**
* builds the complete uri for an image based on its registry
* @param {PorImageRegistryModel} imageModel
*/
export function buildImageFullURI(imageModel) {
if (!imageModel.UseRegistry) {
return imageModel.Image;
}
/**
*
* @param {PorImageRegistryModel} registry
*/
function createImageConfigForContainer(registry) {
const data = {
fromImage: '',
};
let fullImageName = '';
let fullImageName = '';
if (registry.UseRegistry) {
if (registry.Registry.Type === RegistryTypes.GITLAB) {
const slash = _.startsWith(registry.Image, ':') ? '' : '/';
fullImageName = registry.Registry.URL + '/' + registry.Registry.Gitlab.ProjectPath + slash + registry.Image;
} else if (registry.Registry.Type === RegistryTypes.QUAY) {
const name = registry.Registry.Quay.UseOrganisation ? registry.Registry.Quay.OrganisationName : registry.Registry.Username;
const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
fullImageName = url + name + '/' + registry.Image;
} else {
const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
fullImageName = url + registry.Image;
}
if (!_.includes(registry.Image, ':')) {
fullImageName += ':latest';
}
} else {
fullImageName = registry.Image;
}
switch (imageModel.Registry.Type) {
case RegistryTypes.GITLAB:
fullImageName = imageModel.Registry.URL + '/' + imageModel.Registry.Gitlab.ProjectPath + (imageModel.Image.startsWith(':') ? '' : '/') + imageModel.Image;
break;
case RegistryTypes.ANONYMOUS:
fullImageName = imageModel.Image;
break;
case RegistryTypes.QUAY:
fullImageName =
(imageModel.Registry.URL ? imageModel.Registry.URL + '/' : '') +
(imageModel.Registry.Quay.UseOrganisation ? imageModel.Registry.Quay.OrganisationName : imageModel.Registry.Username) +
'/' +
imageModel.Image;
break;
default:
fullImageName = imageModel.Registry.URL + '/' + imageModel.Image;
break;
}
data.fromImage = fullImageName;
return data;
}
if (!imageModel.Image.includes(':')) {
fullImageName += ':latest';
}
function imageContainsURL(image) {
const split = _.split(image, '/');
const url = split[0];
if (split.length > 1) {
return _.includes(url, '.') || _.includes(url, ':');
}
return false;
}
function removeDigestFromRepository(repository) {
return repository.split('@sha')[0];
}
return helper;
},
]);
return fullImageName;
}

View File

@ -575,7 +575,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
function loadFromContainerImageConfig() {
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image)
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image, endpoint.Id)
.then((model) => {
$scope.formValues.RegistryModel = model;
})

View File

@ -40,15 +40,14 @@
<!-- image-and-registry -->
<por-image-registry
model="formValues.RegistryModel"
pull-warning="formValues.alwaysPull"
ng-if="formValues.RegistryModel.Registry"
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
on-image-change="onImageNameChange()"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="formValues.alwaysPull"
on-image-change="onImageNameChange()"
set-validity="setPullImageValidity"
>
<!-- always-pull -->

View File

@ -190,7 +190,7 @@
</div>
<!-- !tag-description -->
<!-- image-and-registry -->
<por-image-registry model="config.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<por-image-registry model="config.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11" endpoint="endpoint"></por-image-registry>
<!-- !image-and-registry -->
<!-- tag-note -->
<div class="form-group">

View File

@ -21,7 +21,6 @@ angular.module('portainer.docker').controller('ContainerController', [
'ImageService',
'HttpRequestHelper',
'Authentication',
'StateManager',
'endpoint',
function (
$q,
@ -42,9 +41,9 @@ angular.module('portainer.docker').controller('ContainerController', [
ImageService,
HttpRequestHelper,
Authentication,
StateManager,
endpoint
) {
$scope.endpoint = endpoint;
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.displayRecreateButton = false;
@ -295,7 +294,7 @@ angular.module('portainer.docker').controller('ContainerController', [
if (!pullImage) {
return $q.when();
}
return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image).then(function pullImage(registryModel) {
return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image, endpoint.Id).then((registryModel) => {
return ImageService.pullImage(registryModel, true);
});
}

View File

@ -63,7 +63,7 @@
<rd-widget-body>
<form class="form-horizontal">
<!-- image-and-registry -->
<por-image-registry model="formValues.RegistryModel" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<por-image-registry model="formValues.RegistryModel" endpoint="endpoint" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<!-- !image-and-registry -->
<!-- tag-note -->
<div class="form-group">

View File

@ -2,11 +2,12 @@ import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
angular.module('portainer.docker').controller('ImageController', [
'$async',
'$q',
'$scope',
'$transition$',
'$state',
'$timeout',
'endpoint',
'ImageService',
'ImageHelper',
'RegistryService',
@ -15,7 +16,8 @@ angular.module('portainer.docker').controller('ImageController', [
'ModalService',
'FileSaver',
'Blob',
function ($q, $scope, $transition$, $state, $timeout, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) {
function ($async, $q, $scope, $transition$, $state, endpoint, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) {
$scope.endpoint = endpoint;
$scope.formValues = {
RegistryModel: new PorImageRegistryModel(),
};
@ -53,39 +55,38 @@ angular.module('portainer.docker').controller('ImageController', [
});
};
$scope.pushTag = function (repository) {
$('#uploadResourceHint').show();
RegistryService.retrievePorRegistryModelFromRepository(repository)
.then(function success(registryModel) {
return ImageService.pushImage(registryModel);
})
.then(function success() {
Notifications.success('Image successfully pushed', repository);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to push image to repository');
})
.finally(function final() {
$('#uploadResourceHint').hide();
});
};
$scope.pushTag = pushTag;
$scope.pullTag = function (repository) {
$('#downloadResourceHint').show();
RegistryService.retrievePorRegistryModelFromRepository(repository)
.then(function success(registryModel) {
return ImageService.pullImage(registryModel, false);
})
.then(function success() {
Notifications.success('Image successfully pulled', repository);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to pull image');
})
.finally(function final() {
async function pushTag(repository) {
return $async(async () => {
$('#uploadResourceHint').show();
try {
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
await ImageService.pushImage(registryModel);
Notifications.success('Image successfully pushed', repository);
} catch (err) {
Notifications.error('Failure', err, 'Unable to push image to repository');
} finally {
$('#uploadResourceHint').hide();
}
});
}
$scope.pullTag = pullTag;
async function pullTag(repository) {
return $async(async () => {
$('#downloadResourceHint').show();
try {
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
await ImageService.pullImage(registryModel);
Notifications.success('Image successfully pushed', repository);
} catch (err) {
Notifications.error('Failure', err, 'Unable to push image to repository');
} finally {
$('#downloadResourceHint').hide();
});
};
}
});
}
$scope.removeTag = function (repository) {
ImageService.deleteImage(repository, false)

View File

@ -17,7 +17,6 @@
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
pull-warning="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"

View File

@ -0,0 +1,16 @@
<rd-header>
<rd-header-title title-text="Registry access"></rd-header-title>
<rd-header-content> <a ui-sref="docker.registries">Registries</a> &gt; {{ $ctrl.registry.Name }} &gt; Access management </rd-header-content>
</rd-header>
<registry-details registry="$ctrl.registry" ng-if="$ctrl.registry"></registry-details>
<por-access-management
ng-if="$ctrl.registry && $ctrl.endpointGroup"
access-controlled-entity="$ctrl.registryEndpointAccesses"
entity-type="registry"
action-in-progress="$ctrl.state.actionInProgress"
update-access="$ctrl.updateAccess"
filter-users="$ctrl.filterUsers"
>
</por-access-management>

View File

@ -0,0 +1,7 @@
angular.module('portainer.docker').component('dockerRegistryAccessView', {
templateUrl: './registryAccess.html',
controller: 'DockerRegistryAccessController',
bindings: {
endpoint: '<',
},
});

View File

@ -0,0 +1,67 @@
import { TeamAccessViewModel, UserAccessViewModel } from 'Portainer/models/access';
class DockerRegistryAccessController {
/* @ngInject */
constructor($async, $state, Notifications, EndpointService, GroupService) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.updateAccess = this.updateAccess.bind(this);
this.filterUsers = this.filterUsers.bind(this);
}
updateAccess() {
return this.$async(async () => {
this.state.actionInProgress = true;
try {
await this.EndpointService.updateRegistryAccess(this.state.endpointId, this.state.registryId, this.registryEndpointAccesses);
this.Notifications.success('Access successfully updated');
this.$state.reload();
} catch (err) {
this.state.actionInProgress = false;
this.Notifications.error('Failure', err, 'Unable to update accesses');
}
});
}
filterUsers(users) {
const endpointUsers = this.endpoint.UserAccessPolicies;
const endpointTeams = this.endpoint.TeamAccessPolicies;
const endpointGroupUsers = this.endpointGroup.UserAccessPolicies;
const endpointGroupTeams = this.endpointGroup.TeamAccessPolicies;
return users.filter((userOrTeam) => {
const userRole = userOrTeam instanceof UserAccessViewModel && (endpointUsers[userOrTeam.Id] || endpointGroupUsers[userOrTeam.Id]);
const teamRole = userOrTeam instanceof TeamAccessViewModel && (endpointTeams[userOrTeam.Id] || endpointGroupTeams[userOrTeam.Id]);
return userRole || teamRole;
});
}
$onInit() {
return this.$async(async () => {
try {
this.state = {
viewReady: false,
actionInProgress: false,
endpointId: this.$state.params.endpointId,
registryId: this.$state.params.id,
};
this.registry = await this.EndpointService.registry(this.state.endpointId, this.state.registryId);
this.registryEndpointAccesses = this.registry.RegistryAccesses[this.state.endpointId] || {};
this.endpointGroup = await this.GroupService.group(this.endpoint.GroupId);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registry details');
} finally {
this.state.viewReady = true;
}
});
}
}
export default DockerRegistryAccessController;
angular.module('portainer.docker').controller('DockerRegistryAccessController', DockerRegistryAccessController);

View File

@ -50,7 +50,6 @@ angular.module('portainer.docker').controller('ServiceController', [
'VolumeService',
'ImageHelper',
'WebhookService',
'EndpointProvider',
'clipboard',
'WebhookHelper',
'NetworkService',
@ -82,7 +81,6 @@ angular.module('portainer.docker').controller('ServiceController', [
VolumeService,
ImageHelper,
WebhookService,
EndpointProvider,
clipboard,
WebhookHelper,
NetworkService,
@ -337,7 +335,7 @@ angular.module('portainer.docker').controller('ServiceController', [
Notifications.error('Failure', err, 'Unable to delete webhook');
});
} else {
WebhookService.createServiceWebhook(service.Id, EndpointProvider.endpointID())
WebhookService.createServiceWebhook(service.Id, endpoint.Id)
.then(function success(data) {
$scope.WebhookExists = true;
$scope.webhookID = data.Id;
@ -688,7 +686,7 @@ angular.module('portainer.docker').controller('ServiceController', [
availableImages: ImageService.images(),
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25),
webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
webhooks: WebhookService.webhooks(service.Id, endpoint.Id),
});
})
.then(async function success(data) {

View File

@ -1,6 +1,6 @@
export class EditEdgeGroupController {
/* @ngInject */
constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async, EndpointService, EndpointHelper) {
constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async, EndpointService) {
this.EdgeGroupService = EdgeGroupService;
this.GroupService = GroupService;
this.TagService = TagService;
@ -8,7 +8,6 @@ export class EditEdgeGroupController {
this.$state = $state;
this.$async = $async;
this.EndpointService = EndpointService;
this.EndpointHelper = EndpointHelper;
this.state = {
actionInProgress: false,

View File

@ -1,4 +1,6 @@
angular.module('portainer.kubernetes', ['portainer.app']).config([
import registriesModule from './registries';
angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
@ -272,6 +274,26 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
},
};
const registries = {
name: 'kubernetes.registries',
url: '/registries',
views: {
'content@': {
component: 'endpointRegistriesView',
},
},
};
const registriesAccess = {
name: 'kubernetes.registries.access',
url: '/:id/access',
views: {
'content@': {
component: 'kubernetesRegistryAccessView',
},
},
};
$stateRegistryProvider.register(kubernetes);
$stateRegistryProvider.register(applications);
$stateRegistryProvider.register(applicationCreation);
@ -297,5 +319,7 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
$stateRegistryProvider.register(resourcePoolAccess);
$stateRegistryProvider.register(volumes);
$stateRegistryProvider.register(volume);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registriesAccess);
},
]);

View File

@ -28,7 +28,7 @@ angular.module('portainer.docker').controller('KubernetesConfigurationsDatatable
};
this.isSystemConfig = function (item) {
return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item);
return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item) || item.IsRegistrySecret;
};
this.isExternalConfiguration = function (item) {

View File

@ -15,7 +15,17 @@
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.cluster({endpointId: $ctrl.endpointId})" ui-sref-active="active">Cluster <span class="menu-icon fa fa-server fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ($ctrl.currentState === 'kubernetes.cluster' || $ctrl.currentState === 'portainer.endpoints.endpoint.kubernetesConfig')">
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
<div
ng-if="
$ctrl.adminAccess &&
['kubernetes.cluster', 'portainer.endpoints.endpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access'].includes($ctrl.currentState)
"
>
<div class="sidebar-sublist">
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
<div class="sidebar-sublist">
<a ui-sref="kubernetes.registries({endpointId: $ctrl.endpointId})" ui-sref-active="active">Registries</a>
</div>
</div>
</li>

View File

@ -62,6 +62,9 @@ class KubernetesApplicationConverter {
if (containers.length) {
res.Image = containers[0].image;
}
if (data.spec.template && data.spec.template.spec && data.spec.template.spec.imagePullSecrets && data.spec.template.spec.imagePullSecrets.length) {
res.RegistryId = parseInt(data.spec.template.spec.imagePullSecrets[0].name.replace('registry-', ''), 10);
}
res.CreationDate = data.metadata.creationTimestamp;
res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data];
@ -268,7 +271,8 @@ class KubernetesApplicationConverter {
res.Name = app.Name;
res.StackName = app.StackName;
res.ApplicationOwner = app.ApplicationOwner;
res.Image = app.Image;
res.ImageModel.Image = app.Image;
res.ImageModel.Registry.Id = app.RegistryId;
res.ReplicaCount = app.TotalPodsCount;
res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(app.Limits.Memory);
res.CpuLimit = app.Limits.Cpu;
@ -292,7 +296,10 @@ class KubernetesApplicationConverter {
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
}
KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels);
if (app.Pods && app.Pods.length) {
KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels);
}
return res;
}

View File

@ -14,6 +14,7 @@ class KubernetesConfigurationConverter {
res.Data[entry.Key] = entry.Value;
});
res.ConfigurationOwner = secret.ConfigurationOwner;
res.IsRegistrySecret = secret.IsRegistrySecret;
return res;
}

View File

@ -10,10 +10,11 @@ import {
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
import { buildImageFullURI } from 'Docker/helpers/imageHelper';
class KubernetesDaemonSetConverter {
/**
* Generate KubernetesDaemonSet from KubenetesApplicationFormValues
* Generate KubernetesDaemonSet from KubernetesApplicationFormValues
* @param {KubernetesApplicationFormValues} formValues
*/
static applicationFormValuesToDaemonSet(formValues, volumeClaims) {
@ -23,7 +24,7 @@ class KubernetesDaemonSetConverter {
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
res.Image = formValues.Image;
res.ImageModel = formValues.ImageModel;
res.CpuLimit = formValues.CpuLimit;
res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
@ -35,7 +36,7 @@ class KubernetesDaemonSetConverter {
/**
* Generate CREATE payload from DaemonSet
* @param {KubernetesDaemonSetPayload} model DaemonSet to genereate payload from
* @param {KubernetesDaemonSetPayload} model DaemonSet to generate payload from
*/
static createPayload(daemonSet) {
const payload = new KubernetesDaemonSetCreatePayload();
@ -50,7 +51,10 @@ class KubernetesDaemonSetConverter {
payload.spec.template.metadata.labels.app = daemonSet.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
payload.spec.template.spec.containers[0].name = daemonSet.Name;
payload.spec.template.spec.containers[0].image = daemonSet.Image;
payload.spec.template.spec.containers[0].image = buildImageFullURI(daemonSet.ImageModel);
if (daemonSet.ImageModel.Registry && daemonSet.ImageModel.Registry.Authentication) {
payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${daemonSet.ImageModel.Registry.Id}` }];
}
payload.spec.template.spec.affinity = daemonSet.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', daemonSet.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts);

View File

@ -11,6 +11,7 @@ import {
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
import { buildImageFullURI } from 'Docker/helpers/imageHelper';
class KubernetesDeploymentConverter {
/**
@ -25,7 +26,7 @@ class KubernetesDeploymentConverter {
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
res.ReplicaCount = formValues.ReplicaCount;
res.Image = formValues.Image;
res.ImageModel = formValues.ImageModel;
res.CpuLimit = formValues.CpuLimit;
res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
@ -53,7 +54,10 @@ class KubernetesDeploymentConverter {
payload.spec.template.metadata.labels.app = deployment.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
payload.spec.template.spec.containers[0].name = deployment.Name;
payload.spec.template.spec.containers[0].image = deployment.Image;
payload.spec.template.spec.containers[0].image = buildImageFullURI(deployment.ImageModel);
if (deployment.ImageModel.Registry && deployment.ImageModel.Registry.Authentication) {
payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${deployment.ImageModel.Registry.Id}` }];
}
payload.spec.template.spec.affinity = deployment.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts);

View File

@ -28,7 +28,16 @@ class KubernetesResourcePoolConverter {
}
});
const ingresses = _.without(ingMap, undefined);
return [namespace, quota, ingresses];
const registries = _.map(formValues.Registries, (r) => {
if (!r.RegistryAccesses[formValues.EndpointId]) {
r.RegistryAccesses[formValues.EndpointId] = { Namespaces: [] };
}
if (!_.includes(r.RegistryAccesses[formValues.EndpointId].Namespaces, formValues.Name)) {
r.RegistryAccesses[formValues.EndpointId].Namespaces = [...r.RegistryAccesses[formValues.EndpointId].Namespaces, formValues.Name];
}
return r;
});
return [namespace, quota, ingresses, registries];
}
}

Some files were not shown because too many files have changed in this diff Show More