fix(api): manage registry authentication in the API (#1751)

pull/1697/head
Anthony Lapenna 2018-03-23 08:44:43 +10:00 committed by GitHub
parent c267f8bf57
commit 30dfd3d616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 183 additions and 42 deletions

View File

@ -52,6 +52,8 @@ func (handler *DockerHubHandler) handleGetDockerHub(w http.ResponseWriter, r *ht
return
}
dockerhub.Password = ""
encodeJSON(w, dockerhub, handler.Logger)
return
}

View File

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

View File

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

View File

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

View File

@ -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 = &registryAuthenticationHeader{
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(&registry, accessContext.userID, accessContext.teamMemberships))) {
matchingRegistry = &registry
break
}
}
if matchingRegistry != nil {
authenticationHeader = &registryAuthenticationHeader{
Username: matchingRegistry.Username,
Password: matchingRegistry.Password,
Serveraddress: matchingRegistry.URL,
}
}
}
return authenticationHeader
}

View File

@ -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 := &registryAccessContext{
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)

View File

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

View File

@ -69,7 +69,7 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques
filteredRegistries = make([]portainer.Registry, 0)
for _, registry := range registries {
if isRegistryAccessAuthorized(&registry, context.UserID, context.UserMemberships) {
if AuthorizedRegistryAccess(&registry, 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
}

View File

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

View File

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

View File

@ -37,8 +37,6 @@ angular.module('portainer.app')
service.encodedCredentials = function(registry) {
var credentials = {
username: registry.Username,
password: registry.Password,
serveraddress: registry.URL
};
return btoa(JSON.stringify(credentials));