diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3aef1e1f..1af8bc0be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,9 +30,6 @@ You can have a use Github filters to list these issues: * intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate * advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced -### Linting - -Please check your code using `grunt lint` before submitting your pull requests. ### Commit Message Format diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index 236845410..1f59fd0c8 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -42,20 +42,17 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) { fileStorePath: path.Join(dataStorePath, fileStorePath), } - // Checking if a mount directory exists is broken with Go on Windows. - // This will need to be reviewed after the issue has been fixed in Go. - // See: https://github.com/portainer/portainer/issues/474 - // err := createDirectoryIfNotExist(dataStorePath, 0755) - // if err != nil { - // return nil, err - // } - - err := service.createDirectoryInStoreIfNotExist(TLSStorePath) + err := os.MkdirAll(dataStorePath, 0755) if err != nil { return nil, err } - err = service.createDirectoryInStoreIfNotExist(ComposeStorePath) + err = service.createDirectoryInStore(TLSStorePath) + if err != nil { + return nil, err + } + + err = service.createDirectoryInStore(ComposeStorePath) if err != nil { return nil, err } @@ -76,14 +73,14 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string { // StoreStackFileFromString creates a subfolder in the ComposeStorePath and stores a new file using the content from a string. // It returns the path to the folder where the file is stored. -func (service *Service) StoreStackFileFromString(stackIdentifier, stackFileContent string) (string, error) { +func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stackFileContent string) (string, error) { stackStorePath := path.Join(ComposeStorePath, stackIdentifier) - err := service.createDirectoryInStoreIfNotExist(stackStorePath) + err := service.createDirectoryInStore(stackStorePath) if err != nil { return "", err } - composeFilePath := path.Join(stackStorePath, ComposeFileDefaultName) + composeFilePath := path.Join(stackStorePath, fileName) data := []byte(stackFileContent) r := bytes.NewReader(data) @@ -97,14 +94,14 @@ func (service *Service) StoreStackFileFromString(stackIdentifier, stackFileConte // StoreStackFileFromReader creates a subfolder in the ComposeStorePath and stores a new file using the content from an io.Reader. // It returns the path to the folder where the file is stored. -func (service *Service) StoreStackFileFromReader(stackIdentifier string, r io.Reader) (string, error) { +func (service *Service) StoreStackFileFromReader(stackIdentifier, fileName string, r io.Reader) (string, error) { stackStorePath := path.Join(ComposeStorePath, stackIdentifier) - err := service.createDirectoryInStoreIfNotExist(stackStorePath) + err := service.createDirectoryInStore(stackStorePath) if err != nil { return "", err } - composeFilePath := path.Join(stackStorePath, ComposeFileDefaultName) + composeFilePath := path.Join(stackStorePath, fileName) err = service.createFileInStore(composeFilePath, r) if err != nil { @@ -117,7 +114,7 @@ func (service *Service) StoreStackFileFromReader(stackIdentifier string, r io.Re // StoreTLSFile creates a folder in the TLSStorePath and stores a new file with the content from r. func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileType, r io.Reader) error { storePath := path.Join(TLSStorePath, folder) - err := service.createDirectoryInStoreIfNotExist(storePath) + err := service.createDirectoryInStore(storePath) if err != nil { return err } @@ -201,24 +198,10 @@ func (service *Service) GetFileContent(filePath string) (string, error) { return string(content), nil } -// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system. -func (service *Service) createDirectoryInStoreIfNotExist(name string) error { +// createDirectoryInStore creates a new directory in the file store +func (service *Service) createDirectoryInStore(name string) error { path := path.Join(service.fileStorePath, name) - return createDirectoryIfNotExist(path, 0700) -} - -// createDirectoryIfNotExist creates a directory if it doesn't exists on the file system. -func createDirectoryIfNotExist(path string, mode uint32) error { - _, err := os.Stat(path) - if os.IsNotExist(err) { - err = os.Mkdir(path, os.FileMode(mode)) - if err != nil { - return err - } - } else if err != nil { - return err - } - return nil + return os.MkdirAll(path, 0700) } // createFile creates a new file in the file store with the content from r. diff --git a/api/git/git.go b/api/git/git.go index 8758363b9..5e58363c7 100644 --- a/api/git/git.go +++ b/api/git/git.go @@ -1,6 +1,9 @@ package git import ( + "net/url" + "strings" + "gopkg.in/src-d/go-git.v4" ) @@ -14,12 +17,23 @@ func NewService(dataStorePath string) (*Service, error) { return service, nil } -// CloneRepository clones a git repository using the specified URL in the specified +// ClonePublicRepository clones a public git repository using the specified URL in the specified // destination folder. -func (service *Service) CloneRepository(url, destination string) error { - _, err := git.PlainClone(destination, false, &git.CloneOptions{ - URL: url, - }) +func (service *Service) ClonePublicRepository(repositoryURL, destination string) error { + return cloneRepository(repositoryURL, destination) +} +// ClonePrivateRepositoryWithBasicAuth clones a private git repository using the specified URL in the specified +// destination folder. It will use the specified username and password for basic HTTP authentication. +func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, destination, username, password string) error { + credentials := username + ":" + url.PathEscape(password) + repositoryURL = strings.Replace(repositoryURL, "://", "://"+credentials+"@", 1) + return cloneRepository(repositoryURL, destination) +} + +func cloneRepository(repositoryURL, destination string) error { + _, err := git.PlainClone(destination, false, &git.CloneOptions{ + URL: repositoryURL, + }) return err } diff --git a/api/http/handler/dockerhub.go b/api/http/handler/dockerhub.go index a5ff36b57..75acc0517 100644 --- a/api/http/handler/dockerhub.go +++ b/api/http/handler/dockerhub.go @@ -52,6 +52,8 @@ func (handler *DockerHubHandler) handleGetDockerHub(w http.ResponseWriter, r *ht return } + dockerhub.Password = "" + encodeJSON(w, dockerhub, handler.Logger) return } diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index ac0cc7b9f..05eea9ef5 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -52,7 +52,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/endpoints"): switch { - case strings.Contains(r.URL.Path, "/docker"): + case strings.Contains(r.URL.Path, "/docker/"): http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/stacks"): http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r) diff --git a/api/http/handler/registry.go b/api/http/handler/registry.go index 9afeb1178..37cb2c971 100644 --- a/api/http/handler/registry.go +++ b/api/http/handler/registry.go @@ -91,6 +91,10 @@ func (handler *RegistryHandler) handleGetRegistries(w http.ResponseWriter, r *ht return } + for i := range filteredRegistries { + filteredRegistries[i].Password = "" + } + encodeJSON(w, filteredRegistries, handler.Logger) } @@ -159,6 +163,8 @@ func (handler *RegistryHandler) handleGetRegistry(w http.ResponseWriter, r *http return } + registry.Password = "" + encodeJSON(w, registry, handler.Logger) } diff --git a/api/http/handler/stack.go b/api/http/handler/stack.go index 86b597e40..5b9e53187 100644 --- a/api/http/handler/stack.go +++ b/api/http/handler/stack.go @@ -70,12 +70,15 @@ func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler { type ( postStacksRequest struct { - Name string `valid:"required"` - SwarmID string `valid:"required"` - StackFileContent string `valid:""` - GitRepository string `valid:""` - PathInRepository string `valid:""` - Env []portainer.Pair `valid:""` + Name string `valid:"required"` + SwarmID string `valid:"required"` + StackFileContent string `valid:""` + RepositoryURL string `valid:""` + RepositoryAuthentication bool `valid:""` + RepositoryUsername string `valid:""` + RepositoryPassword string `valid:""` + ComposeFilePathInRepository string `valid:""` + Env []portainer.Pair `valid:""` } postStacksResponse struct { ID string `json:"Id"` @@ -179,7 +182,7 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter, Env: req.Env, } - projectPath, err := handler.FileService.StoreStackFileFromString(string(stack.ID), stackFileContent) + projectPath, err := handler.FileService.StoreStackFileFromString(string(stack.ID), stack.EntryPoint, stackFileContent) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return @@ -263,24 +266,20 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri } stackName := req.Name - if stackName == "" { - httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) - return - } - swarmID := req.SwarmID - if swarmID == "" { + + if stackName == "" || swarmID == "" || req.RepositoryURL == "" { httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) return } - if req.GitRepository == "" { + if req.RepositoryAuthentication && (req.RepositoryUsername == "" || req.RepositoryPassword == "") { httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) return } - if req.PathInRepository == "" { - req.PathInRepository = filesystem.ComposeFileDefaultName + if req.ComposeFilePathInRepository == "" { + req.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName } stacks, err := handler.StackService.Stacks() @@ -300,7 +299,7 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri ID: portainer.StackID(stackName + "_" + swarmID), Name: stackName, SwarmID: swarmID, - EntryPoint: req.PathInRepository, + EntryPoint: req.ComposeFilePathInRepository, Env: req.Env, } @@ -314,7 +313,11 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri return } - err = handler.GitService.CloneRepository(req.GitRepository, projectPath) + if req.RepositoryAuthentication { + err = handler.GitService.ClonePrivateRepositoryWithBasicAuth(req.RepositoryURL, projectPath, req.RepositoryUsername, req.RepositoryPassword) + } else { + err = handler.GitService.ClonePublicRepository(req.RepositoryURL, projectPath) + } if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return @@ -431,7 +434,7 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r Env: env, } - projectPath, err := handler.FileService.StoreStackFileFromReader(string(stack.ID), stackFile) + projectPath, err := handler.FileService.StoreStackFileFromReader(string(stack.ID), stack.EntryPoint, stackFile) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return @@ -631,7 +634,7 @@ func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Reque } stack.Env = req.Env - _, err = handler.FileService.StoreStackFileFromString(string(stack.ID), req.StackFileContent) + _, err = handler.FileService.StoreStackFileFromString(string(stack.ID), stack.EntryPoint, req.StackFileContent) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index 1602f9d8d..91015367c 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -15,6 +15,8 @@ type proxyFactory struct { ResourceControlService portainer.ResourceControlService TeamMembershipService portainer.TeamMembershipService SettingsService portainer.SettingsService + RegistryService portainer.RegistryService + DockerHubService portainer.DockerHubService } func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler { @@ -45,6 +47,8 @@ func (factory *proxyFactory) newDockerSocketProxy(path string) http.Handler { ResourceControlService: factory.ResourceControlService, TeamMembershipService: factory.TeamMembershipService, SettingsService: factory.SettingsService, + RegistryService: factory.RegistryService, + DockerHubService: factory.DockerHubService, dockerTransport: newSocketTransport(path), } proxy.Transport = transport @@ -57,6 +61,8 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL) *httputil.Reve ResourceControlService: factory.ResourceControlService, TeamMembershipService: factory.TeamMembershipService, SettingsService: factory.SettingsService, + RegistryService: factory.RegistryService, + DockerHubService: factory.DockerHubService, dockerTransport: &http.Transport{}, } proxy.Transport = transport diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index a5b57a535..747c7870a 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -17,7 +17,7 @@ type Manager struct { } // NewManager initializes a new proxy Service -func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager { +func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService, registryService portainer.RegistryService, dockerHubService portainer.DockerHubService) *Manager { return &Manager{ proxies: cmap.New(), extensionProxies: cmap.New(), @@ -25,6 +25,8 @@ func NewManager(resourceControlService portainer.ResourceControlService, teamMem ResourceControlService: resourceControlService, TeamMembershipService: teamMembershipService, SettingsService: settingsService, + RegistryService: registryService, + DockerHubService: dockerHubService, }, } } diff --git a/api/http/proxy/registry.go b/api/http/proxy/registry.go new file mode 100644 index 000000000..5edeb73b7 --- /dev/null +++ b/api/http/proxy/registry.go @@ -0,0 +1,37 @@ +package proxy + +import ( + "github.com/portainer/portainer" + "github.com/portainer/portainer/http/security" +) + +func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader { + var authenticationHeader *registryAuthenticationHeader + + if serverAddress == "" { + authenticationHeader = ®istryAuthenticationHeader{ + Username: accessContext.dockerHub.Username, + Password: accessContext.dockerHub.Password, + Serveraddress: "docker.io", + } + } else { + var matchingRegistry *portainer.Registry + for _, registry := range accessContext.registries { + if registry.URL == serverAddress && + (accessContext.isAdmin || (!accessContext.isAdmin && security.AuthorizedRegistryAccess(®istry, accessContext.userID, accessContext.teamMemberships))) { + matchingRegistry = ®istry + break + } + } + + if matchingRegistry != nil { + authenticationHeader = ®istryAuthenticationHeader{ + Username: matchingRegistry.Username, + Password: matchingRegistry.Password, + Serveraddress: matchingRegistry.URL, + } + } + } + + return authenticationHeader +} diff --git a/api/http/proxy/transport.go b/api/http/proxy/transport.go index 3b0b1fa3c..0e61dc1a5 100644 --- a/api/http/proxy/transport.go +++ b/api/http/proxy/transport.go @@ -1,6 +1,8 @@ package proxy import ( + "encoding/base64" + "encoding/json" "net/http" "path" "strings" @@ -14,6 +16,8 @@ type ( dockerTransport *http.Transport ResourceControlService portainer.ResourceControlService TeamMembershipService portainer.TeamMembershipService + RegistryService portainer.RegistryService + DockerHubService portainer.DockerHubService SettingsService portainer.SettingsService } restrictedOperationContext struct { @@ -22,6 +26,18 @@ type ( userTeamIDs []portainer.TeamID resourceControls []portainer.ResourceControl } + registryAccessContext struct { + isAdmin bool + userID portainer.UserID + teamMemberships []portainer.TeamMembership + registries []portainer.Registry + dockerHub *portainer.DockerHub + } + registryAuthenticationHeader struct { + Username string `json:"username"` + Password string `json:"password"` + Serveraddress string `json:"serveraddress"` + } operationExecutor struct { operationContext *restrictedOperationContext labelBlackList []portainer.Pair @@ -62,6 +78,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon return p.proxyTaskRequest(request) case strings.HasPrefix(path, "/build"): return p.proxyBuildRequest(request) + case strings.HasPrefix(path, "/images"): + return p.proxyImageRequest(request) default: return p.executeDockerRequest(request) } @@ -119,7 +137,7 @@ func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Res func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Response, error) { switch requestPath := request.URL.Path; requestPath { case "/services/create": - return p.executeDockerRequest(request) + return p.replaceRegistryAuthenticationHeader(request) case "/services": return p.rewriteOperation(request, serviceListOperation) @@ -235,6 +253,54 @@ func (p *proxyTransport) proxyBuildRequest(request *http.Request) (*http.Respons return p.interceptAndRewriteRequest(request, buildOperation) } +func (p *proxyTransport) proxyImageRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/images/create": + return p.replaceRegistryAuthenticationHeader(request) + default: + if match, _ := path.Match("/images/*/push", requestPath); match { + return p.replaceRegistryAuthenticationHeader(request) + } + return p.executeDockerRequest(request) + } +} + +func (p *proxyTransport) replaceRegistryAuthenticationHeader(request *http.Request) (*http.Response, error) { + accessContext, err := p.createRegistryAccessContext(request) + if err != nil { + return nil, err + } + + originalHeader := request.Header.Get("X-Registry-Auth") + + if originalHeader != "" { + + decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader) + if err != nil { + return nil, err + } + + var originalHeaderData registryAuthenticationHeader + err = json.Unmarshal(decodedHeaderData, &originalHeaderData) + if err != nil { + return nil, err + } + + authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.Serveraddress, accessContext) + + headerData, err := json.Marshal(authenticationHeader) + if err != nil { + return nil, err + } + + header := base64.StdEncoding.EncodeToString(headerData) + + request.Header.Set("X-Registry-Auth", header) + } + + return p.executeDockerRequest(request) +} + // restrictedOperation ensures that the current user has the required authorizations // before executing the original request. func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) { @@ -270,7 +336,7 @@ func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID s return p.executeDockerRequest(request) } -// rewriteOperation will create a new operation context with data that will be used +// rewriteOperationWithLabelFiltering will create a new operation context with data that will be used // to decorate the original request's response as well as retrieve all the black listed labels // to filter the resources. func (p *proxyTransport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) { @@ -341,6 +407,43 @@ func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Re return p.executeDockerRequest(request) } +func (p *proxyTransport) createRegistryAccessContext(request *http.Request) (*registryAccessContext, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + accessContext := ®istryAccessContext{ + isAdmin: true, + userID: tokenData.ID, + } + + hub, err := p.DockerHubService.DockerHub() + if err != nil { + return nil, err + } + accessContext.dockerHub = hub + + registries, err := p.RegistryService.Registries() + if err != nil { + return nil, err + } + accessContext.registries = registries + + if tokenData.Role != portainer.AdministratorRole { + accessContext.isAdmin = false + + teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) + if err != nil { + return nil, err + } + + accessContext.teamMemberships = teamMemberships + } + + return accessContext, nil +} + func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) { var err error tokenData, err := security.RetrieveTokenData(request) diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go index 976c2947f..54932f673 100644 --- a/api/http/security/authorization.go +++ b/api/http/security/authorization.go @@ -140,3 +140,22 @@ func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, userID portainer.Use } return false } + +// 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 { + for _, authorizedUserID := range registry.AuthorizedUsers { + if authorizedUserID == userID { + return true + } + } + for _, membership := range memberships { + for _, authorizedTeamID := range registry.AuthorizedTeams { + if membership.TeamID == authorizedTeamID { + return true + } + } + } + return false +} diff --git a/api/http/security/filter.go b/api/http/security/filter.go index 9f28f19c0..ffe5e1c49 100644 --- a/api/http/security/filter.go +++ b/api/http/security/filter.go @@ -69,7 +69,7 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques filteredRegistries = make([]portainer.Registry, 0) for _, registry := range registries { - if isRegistryAccessAuthorized(®istry, context.UserID, context.UserMemberships) { + if AuthorizedRegistryAccess(®istry, context.UserID, context.UserMemberships) { filteredRegistries = append(filteredRegistries, registry) } } @@ -87,7 +87,7 @@ func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestC filteredEndpoints = make([]portainer.Endpoint, 0) for _, endpoint := range endpoints { - if isEndpointAccessAuthorized(&endpoint, context.UserID, context.UserMemberships) { + if AuthorizedEndpointAccess(&endpoint, context.UserID, context.UserMemberships) { filteredEndpoints = append(filteredEndpoints, endpoint) } } @@ -95,35 +95,3 @@ func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestC return filteredEndpoints, nil } - -func isRegistryAccessAuthorized(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool { - for _, authorizedUserID := range registry.AuthorizedUsers { - if authorizedUserID == userID { - return true - } - } - for _, membership := range memberships { - for _, authorizedTeamID := range registry.AuthorizedTeams { - if membership.TeamID == authorizedTeamID { - return true - } - } - } - return false -} - -func isEndpointAccessAuthorized(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool { - for _, authorizedUserID := range endpoint.AuthorizedUsers { - if authorizedUserID == userID { - return true - } - } - for _, membership := range memberships { - for _, authorizedTeamID := range endpoint.AuthorizedTeams { - if membership.TeamID == authorizedTeamID { - return true - } - } - } - return false -} diff --git a/api/http/server.go b/api/http/server.go index fc5f08972..536344f68 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -42,7 +42,7 @@ type Server struct { // Start starts the HTTP server func (server *Server) Start() error { requestBouncer := security.NewRequestBouncer(server.JWTService, server.UserService, server.TeamMembershipService, server.AuthDisabled) - proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService) + proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService, server.RegistryService, server.DockerHubService) var fileHandler = handler.NewFileHandler(filepath.Join(server.AssetsPath, "public")) var authHandler = handler.NewAuthHandler(requestBouncer, server.AuthDisabled) diff --git a/api/portainer.go b/api/portainer.go index f001f83df..b3a5bc63d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -152,7 +152,7 @@ type ( URL string `json:"URL"` Authentication bool `json:"Authentication"` Username string `json:"Username"` - Password string `json:"Password"` + Password string `json:"Password,omitempty"` AuthorizedUsers []UserID `json:"AuthorizedUsers"` AuthorizedTeams []TeamID `json:"AuthorizedTeams"` } @@ -162,7 +162,7 @@ type ( DockerHub struct { Authentication bool `json:"Authentication"` Username string `json:"Username"` - Password string `json:"Password"` + Password string `json:"Password,omitempty"` } // EndpointID represents an endpoint identifier. @@ -369,13 +369,14 @@ type ( DeleteTLSFile(folder string, fileType TLSFileType) error DeleteTLSFiles(folder string) error GetStackProjectPath(stackIdentifier string) string - StoreStackFileFromString(stackIdentifier string, stackFileContent string) (string, error) - StoreStackFileFromReader(stackIdentifier string, r io.Reader) (string, error) + StoreStackFileFromString(stackIdentifier, fileName, stackFileContent string) (string, error) + StoreStackFileFromReader(stackIdentifier, fileName string, r io.Reader) (string, error) } // GitService represents a service for managing Git. GitService interface { - CloneRepository(url, destination string) error + ClonePublicRepository(repositoryURL, destination string) error + ClonePrivateRepositoryWithBasicAuth(repositoryURL, destination, username, password string) error } // EndpointWatcher represents a service to synchronize the endpoints via an external source. @@ -400,7 +401,7 @@ type ( const ( // APIVersion is the version number of the Portainer API. - APIVersion = "1.16.4" + APIVersion = "1.16.5" // DBVersion is the version number of the Portainer database. DBVersion = 8 // DefaultTemplatesURL represents the default URL for the templates definitions. diff --git a/api/swagger.yaml b/api/swagger.yaml index 4d2546919..1d36febde 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -56,7 +56,7 @@ info: **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). - version: "1.16.4" + version: "1.16.5" title: "Portainer API" contact: email: "info@portainer.io" @@ -2143,7 +2143,7 @@ definitions: description: "Is analytics enabled" Version: type: "string" - example: "1.16.4" + example: "1.16.5" description: "Portainer API version" PublicSettingsInspectResponse: type: "object" @@ -2904,14 +2904,26 @@ definitions: type: "string" example: "version: 3\n services:\n web:\n image:nginx" description: "Content of the Stack file. Required when using the 'string' deployment method." - GitRepository: + RepositoryURL: type: "string" example: "https://github.com/openfaas/faas" - description: "URL of a public Git repository hosting the Stack file. Required when using the 'repository' deployment method." - PathInRepository: + description: "URL of a Git repository hosting the Stack file. Required when using the 'repository' deployment method." + ComposeFilePathInRepository: type: "string" example: "docker-compose.yml" description: "Path to the Stack file inside the Git repository. Required when using the 'repository' deployment method." + RepositoryAuthentication: + type: "boolean" + example: true + description: "Use basic authentication to clone the Git repository." + RepositoryUsername: + type: "string" + example: "myGitUsername" + description: "Username used in basic authentication. Required when RepositoryAuthentication is true." + RepositoryPassword: + type: "string" + example: "myGitPassword" + description: "Password used in basic authentication. Required when RepositoryAuthentication is true." Env: type: "array" description: "A list of environment variables used during stack deployment" diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.html b/app/docker/components/datatables/configs-datatable/configsDatatable.html index 96a2dcc76..f41830f44 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.html +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.html @@ -14,7 +14,7 @@
{{ item.Running }}
/ {{ item.Replicas }}
- Scale
+ Scale
-
+