diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go index 202a81fdc..3b2646dcb 100644 --- a/api/http/handler/registries/handler.go +++ b/api/http/handler/registries/handler.go @@ -46,6 +46,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete) h.PathPrefix("/registries/{id}/v2").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI))) - + h.PathPrefix("/registries/{id}/proxies/gitlab").Handler( + bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithRegistry))) + h.PathPrefix("/registries/proxies/gitlab").Handler( + bouncer.AdminAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithoutRegistry))) return h } diff --git a/api/http/handler/registries/proxy.go b/api/http/handler/registries/proxy.go index d54520508..3f94bed4a 100644 --- a/api/http/handler/registries/proxy.go +++ b/api/http/handler/registries/proxy.go @@ -41,7 +41,7 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt if proxy == nil { proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register registry proxy", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err} } } diff --git a/api/http/handler/registries/proxy_gitlab.go b/api/http/handler/registries/proxy_gitlab.go new file mode 100644 index 000000000..47b5f4169 --- /dev/null +++ b/api/http/handler/registries/proxy_gitlab.go @@ -0,0 +1,23 @@ +package registries + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" +) + +// request on /api/registries/proxies/gitlab +func (handler *Handler) proxyRequestsToGitlabAPIWithoutRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + domain := r.Header.Get("X-Gitlab-Domain") + if domain == "" { + return &httperror.HandlerError{http.StatusBadRequest, "No Gitlab domain provided", nil} + } + + proxy, err := handler.ProxyManager.CreateGitlabProxy(domain) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create gitlab proxy", err} + } + + http.StripPrefix("/registries/proxies/gitlab", proxy).ServeHTTP(w, r) + return nil +} diff --git a/api/http/handler/registries/proxy_management_gitlab.go b/api/http/handler/registries/proxy_management_gitlab.go new file mode 100644 index 000000000..28f1ead12 --- /dev/null +++ b/api/http/handler/registries/proxy_management_gitlab.go @@ -0,0 +1,66 @@ +package registries + +import ( + "encoding/json" + "net/http" + "strconv" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/portainer/api" +) + +// request on /api/registries/{id}/proxies/gitlab +func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(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} + } + + registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + if err == portainer.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} + } + + err = handler.requestBouncer.RegistryAccess(r, registry) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied} + } + + extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} + } + + var proxy http.Handler + proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension) + if proxy == nil { + proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err} + } + } + + config := &portainer.RegistryManagementConfiguration{ + Type: portainer.GitlabRegistry, + Password: registry.Password, + } + + encodedConfiguration, err := json.Marshal(config) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err} + } + + id := strconv.Itoa(int(registryID)) + r.Header.Set("X-RegistryManagement-Key", id+"-gitlab") + r.Header.Set("X-RegistryManagement-URI", registry.Gitlab.InstanceURL) + r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration)) + r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey) + + http.StripPrefix("/registries/"+id+"/proxies/gitlab", proxy).ServeHTTP(w, r) + return nil +} diff --git a/api/http/handler/registries/registry_create.go b/api/http/handler/registries/registry_create.go index 1b6dbf638..09f6d0a2e 100644 --- a/api/http/handler/registries/registry_create.go +++ b/api/http/handler/registries/registry_create.go @@ -12,11 +12,12 @@ import ( type registryCreatePayload struct { Name string - Type int + Type portainer.RegistryType URL string Authentication bool Username string Password string + Gitlab portainer.GitlabRegistryData } func (payload *registryCreatePayload) Validate(r *http.Request) error { @@ -29,8 +30,8 @@ func (payload *registryCreatePayload) Validate(r *http.Request) error { if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) { return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled") } - if payload.Type != 1 && payload.Type != 2 && payload.Type != 3 { - return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)") + if payload.Type != portainer.QuayRegistry && payload.Type != portainer.AzureRegistry && payload.Type != portainer.CustomRegistry && payload.Type != portainer.GitlabRegistry { + return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)") } return nil } @@ -42,16 +43,6 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - registries, err := handler.RegistryService.Registries() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err} - } - for _, r := range registries { - if r.URL == payload.URL { - return &httperror.HandlerError{http.StatusConflict, "A registry with the same URL already exists", portainer.ErrRegistryAlreadyExists} - } - } - registry := &portainer.Registry{ Type: portainer.RegistryType(payload.Type), Name: payload.Name, @@ -61,6 +52,7 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) * Password: payload.Password, UserAccessPolicies: portainer.UserAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Gitlab: payload.Gitlab, } err = handler.RegistryService.CreateRegistry(registry) diff --git a/api/http/proxy/gitlab.go b/api/http/proxy/gitlab.go new file mode 100644 index 000000000..b809a09a3 --- /dev/null +++ b/api/http/proxy/gitlab.go @@ -0,0 +1,38 @@ +package proxy + +import ( + "errors" + "net/http" + "net/url" +) + +type gitlabTransport struct { + httpTransport *http.Transport +} + +func newGitlabProxy(uri string) (http.Handler, error) { + url, err := url.Parse(uri) + if err != nil { + return nil, err + } + + proxy := newSingleHostReverseProxyWithHostHeader(url) + proxy.Transport = &gitlabTransport{ + httpTransport: &http.Transport{}, + } + + return proxy, nil +} + +func (transport *gitlabTransport) RoundTrip(request *http.Request) (*http.Response, error) { + token := request.Header.Get("Private-Token") + if token == "" { + return nil, errors.New("No gitlab token provided") + } + r, err := http.NewRequest(request.Method, request.URL.String(), nil) + if err != nil { + return nil, err + } + r.Header.Set("Private-Token", token) + return transport.httpTransport.RoundTrip(r) +} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 7a1f38580..aa4a68bef 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -182,3 +182,8 @@ func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, return manager.createDockerProxy(endpoint) } + +// CreateGitlabProxy creates a new HTTP reverse proxy that can be used to send requests to the Gitlab API.. +func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) { + return newGitlabProxy(url) +} diff --git a/api/portainer.go b/api/portainer.go index aac60aad4..078fde6ce 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -191,6 +191,12 @@ type ( // RegistryType represents a type of registry RegistryType int + // GitlabRegistryData represents data required for gitlab registry to work + GitlabRegistryData struct { + ProjectID int `json:"ProjectId"` + InstanceURL string `json:"InstanceURL"` + } + // Registry represents a Docker registry with all the info required // to connect to it Registry struct { @@ -202,6 +208,7 @@ type ( Username string `json:"Username"` Password string `json:"Password,omitempty"` ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"` + Gitlab GitlabRegistryData `json:"Gitlab"` UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` @@ -903,7 +910,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "1.22.1" + APIVersion = "1.23.0-dev" // DBVersion is the version number of the Portainer database DBVersion = 21 // AssetsServerURL represents the URL of the Portainer asset server @@ -913,7 +920,7 @@ const ( // VersionCheckURL represents the URL used to retrieve the latest version of Portainer VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest" // ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved - ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.1.json" + ExtensionDefinitionsURL = AssetsServerURL + "/extensions-" + APIVersion + ".json" // SupportProductsURL represents the URL where Portainer support products can be retrieved SupportProductsURL = AssetsServerURL + "/support.json" // PortainerAgentHeader represents the name of the header available in any agent response @@ -1076,6 +1083,8 @@ const ( AzureRegistry // CustomRegistry represents a custom registry CustomRegistry + // GitlabRegistry represents a gitlab registry + GitlabRegistry ) const ( diff --git a/app/app.js b/app/app.js index db35643a2..7b3419893 100644 --- a/app/app.js +++ b/app/app.js @@ -68,7 +68,7 @@ function initAuthentication(authManager, Authentication, $rootScope, $state) { // authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector // to have more controls on which URL should trigger the unauthenticated state. $rootScope.$on('unauthenticated', function (event, data) { - if (!_.includes(data.config.url, '/v2/')) { + if (!_.includes(data.config.url, '/v2/') && !_.includes(data.config.url, '/api/v4/')) { $state.go('portainer.auth', { error: 'Your session has expired' }); } }); diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index 107115f21..ab169a4c5 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -121,7 +121,7 @@ angular.module('portainer.docker') if (!portBinding.containerPort) { return; } - + let hostPort = portBinding.hostPort; const containerPortRange = parsePortRange(portBinding.containerPort); if (!isValidPortRange(containerPortRange)) { diff --git a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html index b076acffd..88c085a3d 100644 --- a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html +++ b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html @@ -30,7 +30,7 @@