feat(stacks): support compose v2.0 stack (#1963)
parent
ef15cd30eb
commit
e3d564325b
|
@ -38,6 +38,35 @@ func (service *StackService) Stack(ID portainer.StackID) (*portainer.Stack, erro
|
|||
return &stack, nil
|
||||
}
|
||||
|
||||
// StackByName returns a stack object by name.
|
||||
func (service *StackService) StackByName(name string) (*portainer.Stack, error) {
|
||||
var stack *portainer.Stack
|
||||
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(stackBucketName))
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var t portainer.Stack
|
||||
err := internal.UnmarshalStack(v, &t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Name == name {
|
||||
stack = &t
|
||||
}
|
||||
}
|
||||
|
||||
if stack == nil {
|
||||
return portainer.ErrStackNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
// Stacks returns an array containing all the stacks.
|
||||
func (service *StackService) Stacks() ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
|
@ -63,33 +92,6 @@ func (service *StackService) Stacks() ([]portainer.Stack, error) {
|
|||
return stacks, nil
|
||||
}
|
||||
|
||||
// StacksBySwarmID return an array containing all the stacks related to the specified Swarm ID.
|
||||
func (service *StackService) StacksBySwarmID(id string) ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(stackBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var stack portainer.Stack
|
||||
err := internal.UnmarshalStack(v, &stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stack.SwarmID == id {
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
// CreateStack creates a new stack.
|
||||
func (service *StackService) CreateStack(stack *portainer.Stack) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/portainer/portainer/http/client"
|
||||
"github.com/portainer/portainer/jwt"
|
||||
"github.com/portainer/portainer/ldap"
|
||||
"github.com/portainer/portainer/libcompose"
|
||||
|
||||
"log"
|
||||
)
|
||||
|
@ -64,8 +65,12 @@ func initStore(dataStorePath string) *bolt.Store {
|
|||
return store
|
||||
}
|
||||
|
||||
func initStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.StackManager, error) {
|
||||
return exec.NewStackManager(assetsPath, dataStorePath, signatureService, fileService)
|
||||
func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager {
|
||||
return libcompose.NewComposeStackManager(dataStorePath)
|
||||
}
|
||||
|
||||
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.SwarmStackManager, error) {
|
||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService)
|
||||
}
|
||||
|
||||
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||
|
@ -320,11 +325,13 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stackManager, err := initStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Data)
|
||||
|
||||
err = initSettings(store.SettingsService, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -394,7 +401,8 @@ func main() {
|
|||
RegistryService: store.RegistryService,
|
||||
DockerHubService: store.DockerHubService,
|
||||
StackService: store.StackService,
|
||||
StackManager: stackManager,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
|
|
|
@ -65,6 +65,7 @@ const (
|
|||
ErrStackNotFound = Error("Stack not found")
|
||||
ErrStackAlreadyExists = Error("A stack already exists with this name")
|
||||
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
|
||||
ErrStackNotExternal = Error("Not an external stack")
|
||||
)
|
||||
|
||||
// Endpoint extensions error
|
||||
|
|
|
@ -11,18 +11,18 @@ import (
|
|||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// StackManager represents a service for managing stacks.
|
||||
type StackManager struct {
|
||||
// SwarmStackManager represents a service for managing stacks.
|
||||
type SwarmStackManager struct {
|
||||
binaryPath string
|
||||
dataPath string
|
||||
signatureService portainer.DigitalSignatureService
|
||||
fileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewStackManager initializes a new StackManager service.
|
||||
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
||||
// It also updates the configuration of the Docker CLI binary.
|
||||
func NewStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*StackManager, error) {
|
||||
manager := &StackManager{
|
||||
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*SwarmStackManager, error) {
|
||||
manager := &SwarmStackManager{
|
||||
binaryPath: binaryPath,
|
||||
dataPath: dataPath,
|
||||
signatureService: signatureService,
|
||||
|
@ -38,7 +38,7 @@ func NewStackManager(binaryPath, dataPath string, signatureService portainer.Dig
|
|||
}
|
||||
|
||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||
func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
|
@ -54,14 +54,14 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []
|
|||
}
|
||||
|
||||
// Logout executes the docker logout command.
|
||||
func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
args = append(args, "logout")
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
|
||||
|
@ -81,7 +81,7 @@ func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint
|
|||
}
|
||||
|
||||
// Remove executes the docker stack rm command.
|
||||
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
|
@ -133,7 +133,7 @@ func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portaine
|
|||
return command, args
|
||||
}
|
||||
|
||||
func (manager *StackManager) updateDockerCLIConfiguration(dataPath string) error {
|
||||
func (manager *SwarmStackManager) updateDockerCLIConfiguration(dataPath string) error {
|
||||
configFilePath := path.Join(dataPath, "config.json")
|
||||
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
|
||||
if err != nil {
|
||||
|
@ -161,7 +161,7 @@ func (manager *StackManager) updateDockerCLIConfiguration(dataPath string) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (manager *StackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
|
||||
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
|
||||
var config map[string]interface{}
|
||||
|
||||
raw, err := manager.fileService.GetFileContent(path)
|
|
@ -77,9 +77,9 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string {
|
|||
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
|
||||
}
|
||||
|
||||
// StoreStackFileFromString creates a subfolder in the ComposeStorePath and stores a new file using the content from a string.
|
||||
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stackFileContent string) (string, error) {
|
||||
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
|
||||
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
|
||||
err := service.createDirectoryInStore(stackStorePath)
|
||||
if err != nil {
|
||||
|
@ -87,7 +87,6 @@ func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stac
|
|||
}
|
||||
|
||||
composeFilePath := path.Join(stackStorePath, fileName)
|
||||
data := []byte(stackFileContent)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
|
@ -98,31 +97,13 @@ func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stac
|
|||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
}
|
||||
|
||||
// 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, fileName string, r io.Reader) (string, error) {
|
||||
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
|
||||
err := service.createDirectoryInStore(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stackStorePath, fileName)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
|
||||
// It returns the path to the newly created file.
|
||||
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (string, error) {
|
||||
storePath := path.Join(TLSStorePath, folder)
|
||||
err := service.createDirectoryInStore(storePath)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
var fileName string
|
||||
|
@ -134,15 +115,16 @@ func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileTy
|
|||
case portainer.TLSFileKey:
|
||||
fileName = TLSKeyFile
|
||||
default:
|
||||
return portainer.ErrUndefinedTLSFileType
|
||||
return "", portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
tlsFilePath := path.Join(storePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(tlsFilePath, r)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return nil
|
||||
return path.Join(service.fileStorePath, tlsFilePath), nil
|
||||
}
|
||||
|
||||
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
|
||||
|
|
|
@ -6,18 +6,36 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
// errorResponse is a generic response for sending a error.
|
||||
type errorResponse struct {
|
||||
Err string `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
// WriteErrorResponse writes an error message to the response and logger.
|
||||
func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log.Logger) {
|
||||
if logger != nil {
|
||||
logger.Printf("http error: %s (code=%d)", err, code)
|
||||
type (
|
||||
// LoggerHandler defines a HTTP handler that includes a HandlerError return pointer
|
||||
LoggerHandler func(http.ResponseWriter, *http.Request) *HandlerError
|
||||
// HandlerError represents an error raised inside a HTTP handler
|
||||
HandlerError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
errorResponse struct {
|
||||
Err string `json:"err,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
|
||||
func (handler LoggerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
err := handler(rw, r)
|
||||
if err != nil {
|
||||
writeErrorResponse(rw, err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeErrorResponse(rw http.ResponseWriter, err *HandlerError) {
|
||||
log.Printf("http error: %s (err=%s) (code=%d)\n", err.Message, err.Err, err.StatusCode)
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(err.StatusCode)
|
||||
json.NewEncoder(rw).Encode(&errorResponse{Err: err.Message})
|
||||
}
|
||||
|
||||
// WriteError is a convenience function that creates a new HandlerError before calling writeErrorResponse.
|
||||
// For use outside of the standard http handlers.
|
||||
func WriteError(rw http.ResponseWriter, code int, message string, err error) {
|
||||
writeErrorResponse(rw, &HandlerError{code, message, err})
|
||||
}
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// AuthHandler represents an HTTP API handler for managing authentication.
|
||||
type AuthHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
authDisabled bool
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
|
||||
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
|
||||
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
|
||||
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
||||
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
|
||||
// when the server has been started with the --no-auth flag
|
||||
ErrAuthDisabled = portainer.Error("Authentication is disabled")
|
||||
)
|
||||
|
||||
// NewAuthHandler returns a new instance of AuthHandler.
|
||||
func NewAuthHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, authDisabled bool) *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
authDisabled: authDisabled,
|
||||
}
|
||||
h.Handle("/auth",
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth)))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postAuthRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
postAuthResponse struct {
|
||||
JWT string `json:"jwt"`
|
||||
}
|
||||
)
|
||||
|
||||
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if handler.authDisabled {
|
||||
httperror.WriteErrorResponse(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postAuthRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var username = req.Username
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.UserByUsername(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationLDAP && u.ID != 1 {
|
||||
err = handler.LDAPService.AuthenticateUser(username, password, &settings.LDAPSettings)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
}
|
||||
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type authenticatePayload struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type authenticateResponse struct {
|
||||
JWT string `json:"jwt"`
|
||||
}
|
||||
|
||||
func (payload *authenticatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Username) {
|
||||
return portainer.Error("Invalid username")
|
||||
}
|
||||
if govalidator.IsNull(payload.Password) {
|
||||
return portainer.Error("Invalid password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
if handler.authDisabled {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Cannot authenticate user. Portainer was started with the --no-auth flag", ErrAuthDisabled}
|
||||
}
|
||||
|
||||
var payload authenticatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
u, err := handler.UserService.UserByUsername(payload.Username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid credentials", ErrInvalidCredentials}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationLDAP && u.ID != 1 {
|
||||
err = handler.LDAPService.AuthenticateUser(payload.Username, payload.Password, &settings.LDAPSettings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate user via LDAP/AD", err}
|
||||
}
|
||||
} else {
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, payload.Password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", ErrInvalidCredentials}
|
||||
}
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
}
|
||||
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &authenticateResponse{JWT: token})
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
|
||||
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
||||
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
|
||||
// when the server has been started with the --no-auth flag
|
||||
ErrAuthDisabled = portainer.Error("Authentication is disabled")
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle authentication operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
authDisabled bool
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, authDisabled bool) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
authDisabled: authDisabled,
|
||||
}
|
||||
h.Handle("/auth",
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// AzureHandler represents an HTTP API handler for proxying requests to the Azure API.
|
||||
type AzureHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewAzureHandler returns a new instance of AzureHandler.
|
||||
func NewAzureHandler(bouncer *security.RequestBouncer) *AzureHandler {
|
||||
h := &AzureHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.PathPrefix("/{id}/azure").Handler(
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToAzureAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *AzureHandler) checkEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID) error {
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !security.AuthorizedEndpointAccess(endpoint, group, userID, memberships) {
|
||||
return portainer.ErrEndpointAccessDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *AzureHandler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
parsedID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.StripPrefix("/"+id+"/azure", proxy).ServeHTTP(w, r)
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||
type DockerHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewDockerHandler returns a new instance of DockerHandler.
|
||||
func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
|
||||
h := &DockerHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.PathPrefix("/{id}/docker").Handler(
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
parsedID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// DockerHubHandler represents an HTTP API handler for managing DockerHub.
|
||||
type DockerHubHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
DockerHubService portainer.DockerHubService
|
||||
}
|
||||
|
||||
// NewDockerHubHandler returns a new instance of DockerHubHandler.
|
||||
func NewDockerHubHandler(bouncer *security.RequestBouncer) *DockerHubHandler {
|
||||
h := &DockerHubHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetDockerHub))).Methods(http.MethodGet)
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutDockerHub))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
putDockerHubRequest struct {
|
||||
Authentication bool `valid:""`
|
||||
Username string `valid:""`
|
||||
Password string `valid:""`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetDockerHub handles GET requests on /dockerhub
|
||||
func (handler *DockerHubHandler) handleGetDockerHub(w http.ResponseWriter, r *http.Request) {
|
||||
dockerhub, err := handler.DockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dockerhub.Password = ""
|
||||
|
||||
encodeJSON(w, dockerhub, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// handlePutDockerHub handles PUT requests on /dockerhub
|
||||
func (handler *DockerHubHandler) handlePutDockerHub(w http.ResponseWriter, r *http.Request) {
|
||||
var req putDockerHubRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dockerhub := &portainer.DockerHub{
|
||||
Authentication: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
|
||||
if req.Authentication {
|
||||
dockerhub.Authentication = true
|
||||
dockerhub.Username = req.Username
|
||||
dockerhub.Password = req.Password
|
||||
}
|
||||
|
||||
err = handler.DockerHubService.StoreDockerHub(dockerhub)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package dockerhub
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/dockerhub
|
||||
func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
dockerhub, err := handler.DockerHubService.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)
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package dockerhub
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type dockerhubUpdatePayload struct {
|
||||
Authentication bool
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (payload *dockerhubUpdatePayload) 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")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/dockerhub
|
||||
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.DockerHubService.StoreDockerHub(dockerhub)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package dockerhub
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
func hideFields(dockerHub *portainer.DockerHub) {
|
||||
dockerHub.Password = ""
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle DockerHub operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DockerHubService portainer.DockerHubService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage Dockerhub operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
|
@ -1,625 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"github.com/portainer/portainer/http/client"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
|
||||
type EndpointHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
authorizeEndpointManagement bool
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
|
||||
// when the server has been started with the --external-endpoints flag
|
||||
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
|
||||
)
|
||||
|
||||
// NewEndpointHandler returns a new instance of EndpointHandler.
|
||||
func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *EndpointHandler {
|
||||
h := &EndpointHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
authorizeEndpointManagement: authorizeEndpointManagement,
|
||||
}
|
||||
h.Handle("/endpoints",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}/access",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
putEndpointAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"-"`
|
||||
AuthorizedTeams []int `valid:"-"`
|
||||
}
|
||||
|
||||
putEndpointsRequest struct {
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
PublicURL string `valid:"-"`
|
||||
GroupID int `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
TLSSkipVerify bool `valid:"-"`
|
||||
TLSSkipClientVerify bool `valid:"-"`
|
||||
AzureApplicationID string `valid:"-"`
|
||||
AzureTenantID string `valid:"-"`
|
||||
AzureAuthenticationKey string `valid:"-"`
|
||||
}
|
||||
|
||||
postEndpointPayload struct {
|
||||
name string
|
||||
url string
|
||||
endpointType int
|
||||
publicURL string
|
||||
groupID int
|
||||
useTLS bool
|
||||
skipTLSServerVerification bool
|
||||
skipTLSClientVerification bool
|
||||
caCert []byte
|
||||
cert []byte
|
||||
key []byte
|
||||
azureApplicationID string
|
||||
azureTenantID string
|
||||
azureAuthenticationKey string
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetEndpoints handles GET requests on /endpoints
|
||||
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
groups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredEndpoints, err := security.FilterEndpoints(endpoints, groups, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range filteredEndpoints {
|
||||
filteredEndpoints[i].AzureCredentials = portainer.AzureCredentials{}
|
||||
}
|
||||
|
||||
encodeJSON(w, filteredEndpoints, handler.Logger)
|
||||
}
|
||||
|
||||
func (handler *EndpointHandler) createAzureEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
||||
credentials := portainer.AzureCredentials{
|
||||
ApplicationID: payload.azureApplicationID,
|
||||
TenantID: payload.azureTenantID,
|
||||
AuthenticationKey: payload.azureAuthenticationKey,
|
||||
}
|
||||
|
||||
httpClient := client.NewHTTPClient()
|
||||
_, err := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.name,
|
||||
URL: proxy.AzureAPIBaseURL,
|
||||
Type: portainer.AzureEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||
PublicURL: payload.publicURL,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
AzureCredentials: credentials,
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.url, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointType := portainer.DockerEnvironment
|
||||
if agentOnDockerEnvironment {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.name,
|
||||
URL: payload.url,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||
PublicURL: payload.publicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: payload.useTLS,
|
||||
TLSSkipVerify: payload.skipTLSServerVerification,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
|
||||
if !payload.skipTLSServerVerification {
|
||||
r := bytes.NewReader(payload.caCert)
|
||||
// TODO: review the API exposed by the FileService to store
|
||||
// a file from a byte slice and return the path to the stored file instead
|
||||
// of using multiple legacy calls (StoreTLSFile, GetPathForTLSFile) here.
|
||||
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileCA, r)
|
||||
if err != nil {
|
||||
handler.EndpointService.DeleteEndpoint(endpoint.ID)
|
||||
return nil, err
|
||||
}
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
if !payload.skipTLSClientVerification {
|
||||
r := bytes.NewReader(payload.cert)
|
||||
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileCert, r)
|
||||
if err != nil {
|
||||
handler.EndpointService.DeleteEndpoint(endpoint.ID)
|
||||
return nil, err
|
||||
}
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSCertPath = certPath
|
||||
|
||||
r = bytes.NewReader(payload.key)
|
||||
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileKey, r)
|
||||
if err != nil {
|
||||
handler.EndpointService.DeleteEndpoint(endpoint.ID)
|
||||
return nil, err
|
||||
}
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
||||
endpointType := portainer.DockerEnvironment
|
||||
|
||||
if !strings.HasPrefix(payload.url, "unix://") {
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agentOnDockerEnvironment {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.name,
|
||||
URL: payload.url,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||
PublicURL: payload.publicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
}
|
||||
|
||||
err := handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
||||
if portainer.EndpointType(payload.endpointType) == portainer.AzureEnvironment {
|
||||
return handler.createAzureEndpoint(payload)
|
||||
}
|
||||
|
||||
if payload.useTLS {
|
||||
return handler.createTLSSecuredEndpoint(payload)
|
||||
}
|
||||
return handler.createUnsecuredEndpoint(payload)
|
||||
}
|
||||
|
||||
func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload, error) {
|
||||
payload := &postEndpointPayload{}
|
||||
payload.name = r.FormValue("Name")
|
||||
|
||||
endpointType := r.FormValue("EndpointType")
|
||||
|
||||
if payload.name == "" || endpointType == "" {
|
||||
return nil, ErrInvalidRequestFormat
|
||||
}
|
||||
|
||||
parsedType, err := strconv.Atoi(endpointType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload.url = r.FormValue("URL")
|
||||
payload.endpointType = parsedType
|
||||
|
||||
if portainer.EndpointType(payload.endpointType) != portainer.AzureEnvironment && payload.url == "" {
|
||||
return nil, ErrInvalidRequestFormat
|
||||
}
|
||||
|
||||
payload.publicURL = r.FormValue("PublicURL")
|
||||
|
||||
if portainer.EndpointType(payload.endpointType) == portainer.AzureEnvironment {
|
||||
payload.azureApplicationID = r.FormValue("AzureApplicationID")
|
||||
payload.azureTenantID = r.FormValue("AzureTenantID")
|
||||
payload.azureAuthenticationKey = r.FormValue("AzureAuthenticationKey")
|
||||
|
||||
if payload.azureApplicationID == "" || payload.azureTenantID == "" || payload.azureAuthenticationKey == "" {
|
||||
return nil, ErrInvalidRequestFormat
|
||||
}
|
||||
}
|
||||
|
||||
rawGroupID := r.FormValue("GroupID")
|
||||
if rawGroupID == "" {
|
||||
payload.groupID = 1
|
||||
} else {
|
||||
groupID, err := strconv.Atoi(rawGroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload.groupID = groupID
|
||||
}
|
||||
|
||||
payload.useTLS = r.FormValue("TLS") == "true"
|
||||
|
||||
if payload.useTLS {
|
||||
payload.skipTLSServerVerification = r.FormValue("TLSSkipVerify") == "true"
|
||||
payload.skipTLSClientVerification = r.FormValue("TLSSkipClientVerify") == "true"
|
||||
|
||||
if !payload.skipTLSServerVerification {
|
||||
caCert, err := getUploadedFileContent(r, "TLSCACertFile")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload.caCert = caCert
|
||||
}
|
||||
|
||||
if !payload.skipTLSClientVerification {
|
||||
cert, err := getUploadedFileContent(r, "TLSCertFile")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload.cert = cert
|
||||
key, err := getUploadedFileContent(r, "TLSKeyFile")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload.key = key
|
||||
}
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// handlePostEndpoints handles POST requests on /endpoints
|
||||
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := convertPostEndpointRequestToPayload(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.createEndpoint(payload)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
|
||||
func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointAccessRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AuthorizedUsers != nil {
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range req.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
endpoint.AuthorizedUsers = authorizedUserIDs
|
||||
}
|
||||
|
||||
if req.AuthorizedTeams != nil {
|
||||
authorizedTeamIDs := []portainer.TeamID{}
|
||||
for _, value := range req.AuthorizedTeams {
|
||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||
}
|
||||
endpoint.AuthorizedTeams = authorizedTeamIDs
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointsRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
endpoint.Name = req.Name
|
||||
}
|
||||
|
||||
if req.URL != "" {
|
||||
endpoint.URL = req.URL
|
||||
}
|
||||
|
||||
if req.PublicURL != "" {
|
||||
endpoint.PublicURL = req.PublicURL
|
||||
}
|
||||
|
||||
if req.GroupID != 0 {
|
||||
endpoint.GroupID = portainer.EndpointGroupID(req.GroupID)
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
if req.AzureApplicationID != "" {
|
||||
endpoint.AzureCredentials.ApplicationID = req.AzureApplicationID
|
||||
}
|
||||
if req.AzureTenantID != "" {
|
||||
endpoint.AzureCredentials.TenantID = req.AzureTenantID
|
||||
}
|
||||
if req.AzureAuthenticationKey != "" {
|
||||
endpoint.AzureCredentials.AuthenticationKey = req.AzureAuthenticationKey
|
||||
}
|
||||
}
|
||||
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
if req.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = req.TLSSkipVerify
|
||||
if !req.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
|
||||
}
|
||||
|
||||
if !req.TLSSkipClientVerify {
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
|
||||
}
|
||||
} else {
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(folder)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
||||
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
|
||||
|
||||
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,364 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// EndpointGroupHandler represents an HTTP API handler for managing endpoint groups.
|
||||
type EndpointGroupHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
}
|
||||
|
||||
// NewEndpointGroupHandler returns a new instance of EndpointGroupHandler.
|
||||
func NewEndpointGroupHandler(bouncer *security.RequestBouncer) *EndpointGroupHandler {
|
||||
h := &EndpointGroupHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/endpoint_groups",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpointGroups))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoint_groups",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpointGroups))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpointGroup))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointGroup))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}/access",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointGroupAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpointGroup))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postEndpointGroupsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
postEndpointGroupsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
Description string `valid:"-"`
|
||||
Labels []portainer.Pair `valid:""`
|
||||
AssociatedEndpoints []portainer.EndpointID `valid:""`
|
||||
}
|
||||
|
||||
putEndpointGroupAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"-"`
|
||||
AuthorizedTeams []int `valid:"-"`
|
||||
}
|
||||
|
||||
putEndpointGroupsRequest struct {
|
||||
Name string `valid:"-"`
|
||||
Description string `valid:"-"`
|
||||
Labels []portainer.Pair `valid:""`
|
||||
AssociatedEndpoints []portainer.EndpointID `valid:""`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetEndpointGroups handles GET requests on /endpoint_groups
|
||||
func (handler *EndpointGroupHandler) handleGetEndpointGroups(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredEndpointGroups, err := security.FilterEndpointGroups(endpointGroups, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, filteredEndpointGroups, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpointGroups handles POST requests on /endpoint_groups
|
||||
func (handler *EndpointGroupHandler) handlePostEndpointGroups(w http.ResponseWriter, r *http.Request) {
|
||||
var req postEndpointGroupsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointGroup := &portainer.EndpointGroup{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Labels: req.Labels,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
|
||||
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, req.AssociatedEndpoints)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, &postEndpointGroupsResponse{ID: int(endpointGroup.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetEndpointGroup handles GET requests on /endpoint_groups/:id
|
||||
func (handler *EndpointGroupHandler) handleGetEndpointGroup(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointGroupID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, endpointGroup, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutEndpointGroupAccess handles PUT requests on /endpoint_groups/:id/access
|
||||
func (handler *EndpointGroupHandler) handlePutEndpointGroupAccess(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointGroupID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointGroupAccessRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AuthorizedUsers != nil {
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range req.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
endpointGroup.AuthorizedUsers = authorizedUserIDs
|
||||
}
|
||||
|
||||
if req.AuthorizedTeams != nil {
|
||||
authorizedTeamIDs := []portainer.TeamID{}
|
||||
for _, value := range req.AuthorizedTeams {
|
||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||
}
|
||||
endpointGroup.AuthorizedTeams = authorizedTeamIDs
|
||||
}
|
||||
|
||||
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePutEndpointGroup handles PUT requests on /endpoint_groups/:id
|
||||
func (handler *EndpointGroupHandler) handlePutEndpointGroup(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointGroupID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointGroupsRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
groupID := portainer.EndpointGroupID(endpointGroupID)
|
||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(groupID)
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
endpointGroup.Name = req.Name
|
||||
}
|
||||
|
||||
if req.Description != "" {
|
||||
endpointGroup.Description = req.Description
|
||||
}
|
||||
|
||||
endpointGroup.Labels = req.Labels
|
||||
|
||||
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err = handler.updateEndpointGroup(endpoint, groupID, req.AssociatedEndpoints)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *EndpointGroupHandler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||
if endpoint.GroupID == groupID {
|
||||
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
|
||||
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *EndpointGroupHandler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
|
||||
for _, id := range associatedEndpoints {
|
||||
if id == endpoint.ID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
}
|
||||
|
||||
func (handler *EndpointGroupHandler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||
for _, id := range associatedEndpoints {
|
||||
|
||||
if id == endpoint.ID {
|
||||
endpoint.GroupID = groupID
|
||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDeleteEndpointGroup handles DELETE requests on /endpoint_groups/:id
|
||||
func (handler *EndpointGroupHandler) handleDeleteEndpointGroup(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointGroupID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if endpointGroupID == 1 {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveDefaultGroup, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
groupID := portainer.EndpointGroupID(endpointGroupID)
|
||||
_, err = handler.EndpointGroupService.EndpointGroup(groupID)
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.EndpointGroupService.DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == groupID {
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type endpointGroupCreatePayload struct {
|
||||
Name string
|
||||
Description string
|
||||
Labels []portainer.Pair
|
||||
AssociatedEndpoints []portainer.EndpointID
|
||||
}
|
||||
|
||||
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid endpoint group name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/endpoint_groups
|
||||
func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload endpointGroupCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpointGroup := &portainer.EndpointGroup{
|
||||
Name: payload.Name,
|
||||
Description: payload.Description,
|
||||
Labels: payload.Labels,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
|
||||
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the endpoint group inside the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, payload.AssociatedEndpoints)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpointGroup)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// DELETE request on /api/endpoint_groups/:id
|
||||
func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
}
|
||||
|
||||
if endpointGroupID == 1 {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", portainer.ErrCannotRemoveDefaultGroup}
|
||||
}
|
||||
|
||||
_, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.EndpointGroupService.DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the endpoint group from the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/endpoint_groups/:id
|
||||
func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpointGroup)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/endpoint_groups
|
||||
func (handler *Handler) endpointGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups 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}
|
||||
}
|
||||
|
||||
endpointGroups = security.FilterEndpointGroups(endpointGroups, securityContext)
|
||||
return response.JSON(w, endpointGroups)
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type endpointGroupUpdatePayload struct {
|
||||
Name string
|
||||
Description string
|
||||
Labels []portainer.Pair
|
||||
AssociatedEndpoints []portainer.EndpointID
|
||||
}
|
||||
|
||||
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/endpoint_groups/:id
|
||||
func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointGroupUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
endpointGroup.Name = payload.Name
|
||||
}
|
||||
|
||||
if payload.Description != "" {
|
||||
endpointGroup.Description = payload.Description
|
||||
}
|
||||
|
||||
endpointGroup.Labels = payload.Labels
|
||||
|
||||
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpointGroup)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type endpointGroupUpdateAccessPayload struct {
|
||||
AuthorizedUsers []int
|
||||
AuthorizedTeams []int
|
||||
}
|
||||
|
||||
func (payload *endpointGroupUpdateAccessPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/endpoint_groups/:id/access
|
||||
func (handler *Handler) endpointGroupUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointGroupUpdateAccessPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if payload.AuthorizedUsers != nil {
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range payload.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
endpointGroup.AuthorizedUsers = authorizedUserIDs
|
||||
}
|
||||
|
||||
if payload.AuthorizedTeams != nil {
|
||||
authorizedTeamIDs := []portainer.TeamID{}
|
||||
for _, value := range payload.AuthorizedTeams {
|
||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||
}
|
||||
endpointGroup.AuthorizedTeams = authorizedTeamIDs
|
||||
}
|
||||
|
||||
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpointGroup)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/endpoint_groups",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoint_groups",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}/access",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdateAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
|
||||
for _, id := range associatedEndpoints {
|
||||
if id == endpoint.ID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
}
|
||||
|
||||
func (handler *Handler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||
for _, id := range associatedEndpoints {
|
||||
|
||||
if id == endpoint.ID {
|
||||
endpoint.GroupID = groupID
|
||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||
if endpoint.GroupID == groupID {
|
||||
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
|
||||
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package endpointproxy
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to proxy requests to external APIs.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to proxy requests to external APIs.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.PathPrefix("/{id}/azure").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
||||
h.PathPrefix("/{id}/docker").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||
h.PathPrefix("/{id}/extensions/storidge").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) checkEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID) error {
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !security.AuthorizedEndpointAccess(endpoint, group, userID, memberships) {
|
||||
return portainer.ErrEndpointAccessDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package endpointproxy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
||||
}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||
}
|
||||
}
|
||||
|
||||
id := strconv.Itoa(endpointID)
|
||||
http.StripPrefix("/"+id+"/azure", proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package endpointproxy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
||||
}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||
}
|
||||
}
|
||||
|
||||
id := strconv.Itoa(endpointID)
|
||||
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package endpointproxy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
||||
}
|
||||
}
|
||||
|
||||
var storidgeExtension *portainer.EndpointExtension
|
||||
for _, extension := range endpoint.Extensions {
|
||||
if extension.Type == portainer.StoridgeEndpointExtension {
|
||||
storidgeExtension = &extension
|
||||
}
|
||||
}
|
||||
|
||||
if storidgeExtension == nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", portainer.ErrEndpointExtensionNotSupported}
|
||||
}
|
||||
|
||||
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy", err}
|
||||
}
|
||||
}
|
||||
|
||||
id := strconv.Itoa(endpointID)
|
||||
http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"github.com/portainer/portainer/http/client"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type endpointCreatePayload struct {
|
||||
Name string
|
||||
URL string
|
||||
EndpointType int
|
||||
PublicURL string
|
||||
GroupID int
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
TLSSkipClientVerify bool
|
||||
TLSCACertFile []byte
|
||||
TLSCertFile []byte
|
||||
TLSKeyFile []byte
|
||||
AzureApplicationID string
|
||||
AzureTenantID string
|
||||
AzureAuthenticationKey string
|
||||
}
|
||||
|
||||
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
|
||||
if err != nil || endpointType == 0 {
|
||||
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 3 (Azure environment)")
|
||||
}
|
||||
payload.EndpointType = endpointType
|
||||
|
||||
groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true)
|
||||
if groupID == 0 {
|
||||
groupID = 1
|
||||
}
|
||||
payload.GroupID = groupID
|
||||
|
||||
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
|
||||
payload.TLS = useTLS
|
||||
|
||||
if payload.TLS {
|
||||
skipTLSServerVerification, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLSSkipVerify", true)
|
||||
payload.TLSSkipVerify = skipTLSServerVerification
|
||||
skipTLSClientVerification, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLSSkipClientVerify", true)
|
||||
payload.TLSSkipClientVerify = skipTLSClientVerification
|
||||
|
||||
if !payload.TLSSkipVerify {
|
||||
caCert, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid CA certificate file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSCACertFile = caCert
|
||||
}
|
||||
|
||||
if !payload.TLSSkipClientVerify {
|
||||
cert, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid certificate file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSCertFile = cert
|
||||
|
||||
key, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid key file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSKeyFile = key
|
||||
}
|
||||
}
|
||||
|
||||
switch portainer.EndpointType(payload.EndpointType) {
|
||||
case portainer.AzureEnvironment:
|
||||
azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Azure application ID")
|
||||
}
|
||||
payload.AzureApplicationID = azureApplicationID
|
||||
|
||||
azureTenantID, err := request.RetrieveMultiPartFormValue(r, "AzureTenantID", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Azure tenant ID")
|
||||
}
|
||||
payload.AzureTenantID = azureTenantID
|
||||
|
||||
azureAuthenticationKey, err := request.RetrieveMultiPartFormValue(r, "AzureAuthenticationKey", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Azure authentication key")
|
||||
}
|
||||
payload.AzureAuthenticationKey = azureAuthenticationKey
|
||||
default:
|
||||
url, err := request.RetrieveMultiPartFormValue(r, "URL", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid endpoint URL")
|
||||
}
|
||||
payload.URL = url
|
||||
|
||||
publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true)
|
||||
payload.PublicURL = publicURL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/endpoints
|
||||
func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
|
||||
}
|
||||
|
||||
payload := &endpointCreatePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpoint, endpointCreationError := handler.createEndpoint(payload)
|
||||
if endpointCreationError != nil {
|
||||
return endpointCreationError
|
||||
}
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
||||
|
||||
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
|
||||
return handler.createAzureEndpoint(payload)
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
return handler.createTLSSecuredEndpoint(payload)
|
||||
}
|
||||
return handler.createUnsecuredEndpoint(payload)
|
||||
}
|
||||
|
||||
func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
credentials := portainer.AzureCredentials{
|
||||
ApplicationID: payload.AzureApplicationID,
|
||||
TenantID: payload.AzureTenantID,
|
||||
AuthenticationKey: payload.AzureAuthenticationKey,
|
||||
}
|
||||
|
||||
httpClient := client.NewHTTPClient()
|
||||
_, err := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", err}
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.Name,
|
||||
URL: payload.URL,
|
||||
Type: portainer.AzureEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
AzureCredentials: credentials,
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
endpointType := portainer.DockerEnvironment
|
||||
|
||||
if !strings.HasPrefix(payload.URL, "unix://") {
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, nil)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err}
|
||||
}
|
||||
if agentOnDockerEnvironment {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.Name,
|
||||
URL: payload.URL,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
}
|
||||
|
||||
err := handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipClientVerify, payload.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to create TLS configuration", err}
|
||||
}
|
||||
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err}
|
||||
}
|
||||
|
||||
endpointType := portainer.DockerEnvironment
|
||||
if agentOnDockerEnvironment {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.Name,
|
||||
URL: payload.URL,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: payload.TLS,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
|
||||
}
|
||||
|
||||
filesystemError := handler.storeTLSFiles(endpoint, payload)
|
||||
if err != nil {
|
||||
handler.EndpointService.DeleteEndpoint(endpoint.ID)
|
||||
return nil, filesystemError
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *endpointCreatePayload) *httperror.HandlerError {
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
|
||||
if !payload.TLSSkipVerify {
|
||||
caCertPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileCA, payload.TLSCACertFile)
|
||||
if err != nil {
|
||||
handler.EndpointService.DeleteEndpoint(endpoint.ID)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS CA certificate file on disk", err}
|
||||
}
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
if !payload.TLSSkipClientVerify {
|
||||
certPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileCert, payload.TLSCertFile)
|
||||
if err != nil {
|
||||
handler.EndpointService.DeleteEndpoint(endpoint.ID)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS certificate file on disk", err}
|
||||
}
|
||||
endpoint.TLSConfig.TLSCertPath = certPath
|
||||
|
||||
keyPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileKey, payload.TLSKeyFile)
|
||||
if err != nil {
|
||||
handler.EndpointService.DeleteEndpoint(endpoint.ID)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS key file on disk", err}
|
||||
}
|
||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// DELETE request on /api/endpoints/:id
|
||||
func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
folder := strconv.Itoa(endpointID)
|
||||
err = handler.FileService.DeleteTLSFiles(folder)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
||||
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type endpointExtensionAddPayload struct {
|
||||
Type int
|
||||
URL string
|
||||
}
|
||||
|
||||
func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error {
|
||||
if payload.Type != 1 {
|
||||
return portainer.Error("Invalid type value. Value must be one of: 1 (Storidge)")
|
||||
}
|
||||
if govalidator.IsNull(payload.URL) {
|
||||
return portainer.Error("Invalid URL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/endpoints/:id/extensions
|
||||
func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
var payload endpointExtensionAddPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
extensionType := portainer.EndpointExtensionType(payload.Type)
|
||||
|
||||
var extension *portainer.EndpointExtension
|
||||
for _, ext := range endpoint.Extensions {
|
||||
if ext.Type == extensionType {
|
||||
extension = &ext
|
||||
}
|
||||
}
|
||||
|
||||
if extension != nil {
|
||||
extension.URL = payload.URL
|
||||
} else {
|
||||
extension = &portainer.EndpointExtension{
|
||||
Type: extensionType,
|
||||
URL: payload.URL,
|
||||
}
|
||||
endpoint.Extensions = append(endpoint.Extensions, *extension)
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, extension)
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// DELETE request on /api/endpoints/:id/extensions/:extensionType
|
||||
func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
extensionType, err := request.RetrieveNumericRouteVariableValue(r, "extensionType")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension type route variable", err}
|
||||
}
|
||||
|
||||
for idx, ext := range endpoint.Extensions {
|
||||
if ext.Type == portainer.EndpointExtensionType(extensionType) {
|
||||
endpoint.Extensions = append(endpoint.Extensions[:idx], endpoint.Extensions[idx+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/endpoints/:id
|
||||
func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/endpoints
|
||||
func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups 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}
|
||||
}
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
|
||||
for _, endpoint := range filteredEndpoints {
|
||||
hideFields(&endpoint)
|
||||
}
|
||||
return response.JSON(w, filteredEndpoints)
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/client"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type endpointUpdatePayload struct {
|
||||
Name string
|
||||
URL string
|
||||
PublicURL string
|
||||
GroupID int
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
TLSSkipClientVerify bool
|
||||
AzureApplicationID string
|
||||
AzureTenantID string
|
||||
AzureAuthenticationKey string
|
||||
}
|
||||
|
||||
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/endpoints/:id
|
||||
func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
endpoint.Name = payload.Name
|
||||
}
|
||||
|
||||
if payload.URL != "" {
|
||||
endpoint.URL = payload.URL
|
||||
}
|
||||
|
||||
if payload.PublicURL != "" {
|
||||
endpoint.PublicURL = payload.PublicURL
|
||||
}
|
||||
|
||||
if payload.GroupID != 0 {
|
||||
endpoint.GroupID = portainer.EndpointGroupID(payload.GroupID)
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
credentials := endpoint.AzureCredentials
|
||||
if payload.AzureApplicationID != "" {
|
||||
credentials.ApplicationID = payload.AzureApplicationID
|
||||
}
|
||||
if payload.AzureTenantID != "" {
|
||||
credentials.TenantID = payload.AzureTenantID
|
||||
}
|
||||
if payload.AzureAuthenticationKey != "" {
|
||||
credentials.AuthenticationKey = payload.AzureAuthenticationKey
|
||||
}
|
||||
|
||||
httpClient := client.NewHTTPClient()
|
||||
_, authErr := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
|
||||
if authErr != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", authErr}
|
||||
}
|
||||
endpoint.AzureCredentials = credentials
|
||||
}
|
||||
|
||||
folder := strconv.Itoa(endpointID)
|
||||
if payload.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
if !payload.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
|
||||
}
|
||||
|
||||
if !payload.TLSSkipClientVerify {
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
|
||||
}
|
||||
} else {
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(folder)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type endpointUpdateAccessPayload struct {
|
||||
AuthorizedUsers []int
|
||||
AuthorizedTeams []int
|
||||
}
|
||||
|
||||
func (payload *endpointUpdateAccessPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/endpoints/:id/access
|
||||
func (handler *Handler) endpointUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointUpdateAccessPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
if payload.AuthorizedUsers != nil {
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range payload.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
endpoint.AuthorizedUsers = authorizedUserIDs
|
||||
}
|
||||
|
||||
if payload.AuthorizedTeams != nil {
|
||||
authorizedTeamIDs := []portainer.TeamID{}
|
||||
for _, value := range payload.AuthorizedTeams {
|
||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||
}
|
||||
endpoint.AuthorizedTeams = authorizedTeamIDs
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
|
||||
// when the server has been started with the --external-endpoints flag
|
||||
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
|
||||
)
|
||||
|
||||
func hideFields(endpoint *portainer.Endpoint) {
|
||||
endpoint.AzureCredentials = portainer.AzureCredentials{}
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
authorizeEndpointManagement bool
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
authorizeEndpointManagement: authorizeEndpointManagement,
|
||||
}
|
||||
|
||||
h.Handle("/endpoints",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}/access",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdateAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/extensions",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// ExtensionHandler represents an HTTP API handler for managing Settings.
|
||||
type ExtensionHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewExtensionHandler returns a new instance of ExtensionHandler.
|
||||
func NewExtensionHandler(bouncer *security.RequestBouncer) *ExtensionHandler {
|
||||
h := &ExtensionHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/{endpointId}/extensions",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostExtensions))).Methods(http.MethodPost)
|
||||
h.Handle("/{endpointId}/extensions/{extensionType}",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleDeleteExtensions))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postExtensionRequest struct {
|
||||
Type int `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (handler *ExtensionHandler) handlePostExtensions(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postExtensionRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
extensionType := portainer.EndpointExtensionType(req.Type)
|
||||
|
||||
var extension *portainer.EndpointExtension
|
||||
|
||||
for _, ext := range endpoint.Extensions {
|
||||
if ext.Type == extensionType {
|
||||
extension = &ext
|
||||
}
|
||||
}
|
||||
|
||||
if extension != nil {
|
||||
extension.URL = req.URL
|
||||
} else {
|
||||
extension = &portainer.EndpointExtension{
|
||||
Type: extensionType,
|
||||
URL: req.URL,
|
||||
}
|
||||
endpoint.Extensions = append(endpoint.Extensions, *extension)
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, extension, handler.Logger)
|
||||
}
|
||||
|
||||
func (handler *ExtensionHandler) handleDeleteExtensions(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
extType, err := strconv.Atoi(vars["extensionType"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
extensionType := portainer.EndpointExtensionType(extType)
|
||||
|
||||
for idx, ext := range endpoint.Extensions {
|
||||
if ext.Type == extensionType {
|
||||
endpoint.Extensions = append(endpoint.Extensions[:idx], endpoint.Extensions[idx+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// StoridgeHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||
type StoridgeHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewStoridgeHandler returns a new instance of StoridgeHandler.
|
||||
func NewStoridgeHandler(bouncer *security.RequestBouncer) *StoridgeHandler {
|
||||
h := &StoridgeHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.PathPrefix("/{id}/extensions/storidge").Handler(
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToStoridgeAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *StoridgeHandler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
parsedID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var storidgeExtension *portainer.EndpointExtension
|
||||
for _, extension := range endpoint.Extensions {
|
||||
if extension.Type == portainer.StoridgeEndpointExtension {
|
||||
storidgeExtension = &extension
|
||||
}
|
||||
}
|
||||
|
||||
if storidgeExtension == nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionNotSupported, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r)
|
||||
}
|
|
@ -1,24 +1,19 @@
|
|||
package handler
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileHandler represents an HTTP API handler for managing static files.
|
||||
type FileHandler struct {
|
||||
// Handler represents an HTTP API handler for managing static files.
|
||||
type Handler struct {
|
||||
http.Handler
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
// NewFileHandler returns a new instance of FileHandler.
|
||||
func NewFileHandler(assetPublicPath string) *FileHandler {
|
||||
h := &FileHandler{
|
||||
// NewHandler creates a handler to serve static files.
|
||||
func NewHandler(assetPublicPath string) *Handler {
|
||||
h := &Handler{
|
||||
Handler: http.FileServer(http.Dir(assetPublicPath)),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
@ -32,7 +27,7 @@ func isHTML(acceptContent []string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (handler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !isHTML(r.Header["Accept"]) {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else {
|
|
@ -1,53 +1,56 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/handler/extensions"
|
||||
"github.com/portainer/portainer/http/handler/auth"
|
||||
"github.com/portainer/portainer/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/http/handler/file"
|
||||
"github.com/portainer/portainer/http/handler/registries"
|
||||
"github.com/portainer/portainer/http/handler/resourcecontrols"
|
||||
"github.com/portainer/portainer/http/handler/settings"
|
||||
"github.com/portainer/portainer/http/handler/stacks"
|
||||
"github.com/portainer/portainer/http/handler/status"
|
||||
"github.com/portainer/portainer/http/handler/teammemberships"
|
||||
"github.com/portainer/portainer/http/handler/teams"
|
||||
"github.com/portainer/portainer/http/handler/templates"
|
||||
"github.com/portainer/portainer/http/handler/upload"
|
||||
"github.com/portainer/portainer/http/handler/users"
|
||||
"github.com/portainer/portainer/http/handler/websocket"
|
||||
)
|
||||
|
||||
// Handler is a collection of all the service handlers.
|
||||
type Handler struct {
|
||||
AuthHandler *AuthHandler
|
||||
UserHandler *UserHandler
|
||||
TeamHandler *TeamHandler
|
||||
TeamMembershipHandler *TeamMembershipHandler
|
||||
EndpointHandler *EndpointHandler
|
||||
EndpointGroupHandler *EndpointGroupHandler
|
||||
RegistryHandler *RegistryHandler
|
||||
DockerHubHandler *DockerHubHandler
|
||||
ExtensionHandler *ExtensionHandler
|
||||
StoridgeHandler *extensions.StoridgeHandler
|
||||
ResourceHandler *ResourceHandler
|
||||
StackHandler *StackHandler
|
||||
StatusHandler *StatusHandler
|
||||
SettingsHandler *SettingsHandler
|
||||
TemplatesHandler *TemplatesHandler
|
||||
DockerHandler *DockerHandler
|
||||
AzureHandler *AzureHandler
|
||||
WebSocketHandler *WebSocketHandler
|
||||
UploadHandler *UploadHandler
|
||||
FileHandler *FileHandler
|
||||
}
|
||||
AuthHandler *auth.Handler
|
||||
|
||||
const (
|
||||
// ErrInvalidJSON defines an error raised the app is unable to parse request data
|
||||
ErrInvalidJSON = portainer.Error("Invalid JSON")
|
||||
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
|
||||
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
||||
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
|
||||
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
|
||||
)
|
||||
DockerHubHandler *dockerhub.Handler
|
||||
EndpointGroupHandler *endpointgroups.Handler
|
||||
EndpointHandler *endpoints.Handler
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
FileHandler *file.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
SettingsHandler *settings.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
TeamMembershipHandler *teammemberships.Handler
|
||||
TeamHandler *teams.Handler
|
||||
TemplatesHandler *templates.Handler
|
||||
UploadHandler *upload.Handler
|
||||
UserHandler *users.Handler
|
||||
WebSocketHandler *websocket.Handler
|
||||
|
||||
// StoridgeHandler *extensions.StoridgeHandler
|
||||
// AzureHandler *azure.Handler
|
||||
// DockerHandler *docker.Handler
|
||||
}
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/auth"):
|
||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||
|
@ -58,24 +61,22 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
||||
switch {
|
||||
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)
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/extensions/storidge"):
|
||||
http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/extensions"):
|
||||
http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r)
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/azure/"):
|
||||
http.StripPrefix("/api/endpoints", h.AzureHandler).ServeHTTP(w, r)
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
default:
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/resource_controls"):
|
||||
http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r)
|
||||
http.StripPrefix("/api", h.ResourceControlHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/settings"):
|
||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/stacks"):
|
||||
http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/status"):
|
||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
|
@ -94,27 +95,3 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
h.FileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// encodeJSON encodes v to w in JSON format. WriteErrorResponse() is called if encoding fails.
|
||||
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, logger)
|
||||
}
|
||||
}
|
||||
|
||||
// getUploadedFileContent retrieve the content of a file uploaded in the request.
|
||||
// Uses requestParameter as the key to retrieve the file in the request payload.
|
||||
func getUploadedFileContent(request *http.Request, requestParameter string) ([]byte, error) {
|
||||
file, _, err := request.FormFile(requestParameter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileContent, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileContent, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func hideFields(registry *portainer.Registry) {
|
||||
registry.Password = ""
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle registry operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
RegistryService portainer.RegistryService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage registry operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
h.Handle("/registries",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/registries",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/registries/{id}/access",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdateAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type registryCreatePayload struct {
|
||||
Name string
|
||||
URL string
|
||||
Authentication bool
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid registry name")
|
||||
}
|
||||
if govalidator.IsNull(payload.URL) {
|
||||
return portainer.Error("Invalid registry URL")
|
||||
}
|
||||
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")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload registryCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
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{
|
||||
Name: payload.Name,
|
||||
URL: payload.URL,
|
||||
Authentication: payload.Authentication,
|
||||
Username: payload.Username,
|
||||
Password: payload.Password,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
|
||||
err = handler.RegistryService.CreateRegistry(registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the registry inside the database", err}
|
||||
}
|
||||
|
||||
hideFields(registry)
|
||||
return response.JSON(w, registry)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// DELETE request on /api/registries/:id
|
||||
func (handler *Handler) registryDelete(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}
|
||||
}
|
||||
|
||||
_, err = handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
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.RegistryService.DeleteRegistry(portainer.RegistryID(registryID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the registry from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/registries/:id
|
||||
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}
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
hideFields(registry)
|
||||
return response.JSON(w, registry)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/registries
|
||||
func (handler *Handler) registryList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
registries, err := handler.RegistryService.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 _, registry := range filteredRegistries {
|
||||
hideFields(®istry)
|
||||
}
|
||||
return response.JSON(w, registries)
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type registryUpdatePayload struct {
|
||||
Name string
|
||||
URL string
|
||||
Authentication bool
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (payload *registryUpdatePayload) 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")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/registries/:id
|
||||
func (handler *Handler) registryUpdate(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}
|
||||
}
|
||||
|
||||
var payload registryUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
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 && r.ID != registry.ID {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
registry.Name = payload.Name
|
||||
}
|
||||
|
||||
if payload.URL != "" {
|
||||
registry.URL = payload.URL
|
||||
}
|
||||
|
||||
if payload.Authentication {
|
||||
registry.Authentication = true
|
||||
registry.Username = payload.Username
|
||||
registry.Password = payload.Password
|
||||
} else {
|
||||
registry.Authentication = false
|
||||
registry.Username = ""
|
||||
registry.Password = ""
|
||||
}
|
||||
|
||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, registry)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type registryUpdateAccessPayload struct {
|
||||
AuthorizedUsers []int
|
||||
AuthorizedTeams []int
|
||||
}
|
||||
|
||||
func (payload *registryUpdateAccessPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/registries/:id/access
|
||||
func (handler *Handler) registryUpdateAccess(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}
|
||||
}
|
||||
|
||||
var payload registryUpdateAccessPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrEndpointGroupNotFound {
|
||||
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 payload.AuthorizedUsers != nil {
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range payload.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
registry.AuthorizedUsers = authorizedUserIDs
|
||||
}
|
||||
|
||||
if payload.AuthorizedTeams != nil {
|
||||
authorizedTeamIDs := []portainer.TeamID{}
|
||||
for _, value := range payload.AuthorizedTeams {
|
||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||
}
|
||||
registry.AuthorizedTeams = authorizedTeamIDs
|
||||
}
|
||||
|
||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, registry)
|
||||
}
|
|
@ -1,320 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// RegistryHandler represents an HTTP API handler for managing Docker registries.
|
||||
type RegistryHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
RegistryService portainer.RegistryService
|
||||
}
|
||||
|
||||
// NewRegistryHandler returns a new instance of RegistryHandler.
|
||||
func NewRegistryHandler(bouncer *security.RequestBouncer) *RegistryHandler {
|
||||
h := &RegistryHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/registries",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostRegistries))).Methods(http.MethodPost)
|
||||
h.Handle("/registries",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetRegistries))).Methods(http.MethodGet)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetRegistry))).Methods(http.MethodGet)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutRegistry))).Methods(http.MethodPut)
|
||||
h.Handle("/registries/{id}/access",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutRegistryAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteRegistry))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postRegistriesRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
Authentication bool `valid:""`
|
||||
Username string `valid:""`
|
||||
Password string `valid:""`
|
||||
}
|
||||
|
||||
postRegistriesResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putRegistryAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"-"`
|
||||
AuthorizedTeams []int `valid:"-"`
|
||||
}
|
||||
|
||||
putRegistriesRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
Authentication bool `valid:""`
|
||||
Username string `valid:""`
|
||||
Password string `valid:""`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetRegistries handles GET requests on /registries
|
||||
func (handler *RegistryHandler) handleGetRegistries(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredRegistries, err := security.FilterRegistries(registries, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range filteredRegistries {
|
||||
filteredRegistries[i].Password = ""
|
||||
}
|
||||
|
||||
encodeJSON(w, filteredRegistries, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostRegistries handles POST requests on /registries
|
||||
func (handler *RegistryHandler) handlePostRegistries(w http.ResponseWriter, r *http.Request) {
|
||||
var req postRegistriesRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
for _, r := range registries {
|
||||
if r.URL == req.URL {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrRegistryAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
Authentication: req.Authentication,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
|
||||
err = handler.RegistryService.CreateRegistry(registry)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postRegistriesResponse{ID: int(registry.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetRegistry handles GET requests on /registries/:id
|
||||
func (handler *RegistryHandler) handleGetRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
registryID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrRegistryNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registry.Password = ""
|
||||
|
||||
encodeJSON(w, registry, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutRegistryAccess handles PUT requests on /registries/:id/access
|
||||
func (handler *RegistryHandler) handlePutRegistryAccess(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
registryID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putRegistryAccessRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrRegistryNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AuthorizedUsers != nil {
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range req.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
registry.AuthorizedUsers = authorizedUserIDs
|
||||
}
|
||||
|
||||
if req.AuthorizedTeams != nil {
|
||||
authorizedTeamIDs := []portainer.TeamID{}
|
||||
for _, value := range req.AuthorizedTeams {
|
||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||
}
|
||||
registry.AuthorizedTeams = authorizedTeamIDs
|
||||
}
|
||||
|
||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePutRegistry handles PUT requests on /registries/:id
|
||||
func (handler *RegistryHandler) handlePutRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
registryID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putRegistriesRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrRegistryNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
for _, r := range registries {
|
||||
if r.URL == req.URL && r.ID != registry.ID {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrRegistryAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
registry.Name = req.Name
|
||||
}
|
||||
|
||||
if req.URL != "" {
|
||||
registry.URL = req.URL
|
||||
}
|
||||
|
||||
if req.Authentication {
|
||||
registry.Authentication = true
|
||||
registry.Username = req.Username
|
||||
registry.Password = req.Password
|
||||
} else {
|
||||
registry.Authentication = false
|
||||
registry.Username = ""
|
||||
registry.Password = ""
|
||||
}
|
||||
|
||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteRegistry handles DELETE requests on /registries/:id
|
||||
func (handler *RegistryHandler) handleDeleteRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
registryID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrRegistryNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.RegistryService.DeleteRegistry(portainer.RegistryID(registryID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,266 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// ResourceHandler represents an HTTP API handler for managing resource controls.
|
||||
type ResourceHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewResourceHandler returns a new instance of ResourceHandler.
|
||||
func NewResourceHandler(bouncer *security.RequestBouncer) *ResourceHandler {
|
||||
h := &ResourceHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/resource_controls",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostResources))).Methods(http.MethodPost)
|
||||
h.Handle("/resource_controls/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutResources))).Methods(http.MethodPut)
|
||||
h.Handle("/resource_controls/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteResources))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postResourcesRequest struct {
|
||||
ResourceID string `valid:"required"`
|
||||
Type string `valid:"required"`
|
||||
AdministratorsOnly bool `valid:"-"`
|
||||
Users []int `valid:"-"`
|
||||
Teams []int `valid:"-"`
|
||||
SubResourceIDs []string `valid:"-"`
|
||||
}
|
||||
|
||||
putResourcesRequest struct {
|
||||
AdministratorsOnly bool `valid:"-"`
|
||||
Users []int `valid:"-"`
|
||||
Teams []int `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostResources handles POST requests on /resources
|
||||
func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *http.Request) {
|
||||
var req postResourcesRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var resourceControlType portainer.ResourceControlType
|
||||
switch req.Type {
|
||||
case "container":
|
||||
resourceControlType = portainer.ContainerResourceControl
|
||||
case "service":
|
||||
resourceControlType = portainer.ServiceResourceControl
|
||||
case "volume":
|
||||
resourceControlType = portainer.VolumeResourceControl
|
||||
case "network":
|
||||
resourceControlType = portainer.NetworkResourceControl
|
||||
case "secret":
|
||||
resourceControlType = portainer.SecretResourceControl
|
||||
case "stack":
|
||||
resourceControlType = portainer.StackResourceControl
|
||||
case "config":
|
||||
resourceControlType = portainer.ConfigResourceControl
|
||||
default:
|
||||
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Users) == 0 && len(req.Teams) == 0 && !req.AdministratorsOnly {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := handler.ResourceControlService.ResourceControlByResourceID(req.ResourceID)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if rc != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceControlAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||
for _, v := range req.Users {
|
||||
userAccess := portainer.UserResourceAccess{
|
||||
UserID: portainer.UserID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
userAccesses = append(userAccesses, userAccess)
|
||||
}
|
||||
|
||||
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
|
||||
for _, v := range req.Teams {
|
||||
teamAccess := portainer.TeamResourceAccess{
|
||||
TeamID: portainer.TeamID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
teamAccesses = append(teamAccesses, teamAccess)
|
||||
}
|
||||
|
||||
resourceControl := portainer.ResourceControl{
|
||||
ResourceID: req.ResourceID,
|
||||
SubResourceIDs: req.SubResourceIDs,
|
||||
Type: resourceControlType,
|
||||
AdministratorsOnly: req.AdministratorsOnly,
|
||||
UserAccesses: userAccesses,
|
||||
TeamAccesses: teamAccesses,
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// handlePutResources handles PUT requests on /resources/:id
|
||||
func (handler *ResourceHandler) handlePutResources(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
resourceControlID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putResourcesRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
|
||||
if err == portainer.ErrResourceControlNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControl.AdministratorsOnly = req.AdministratorsOnly
|
||||
|
||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||
for _, v := range req.Users {
|
||||
userAccess := portainer.UserResourceAccess{
|
||||
UserID: portainer.UserID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
userAccesses = append(userAccesses, userAccess)
|
||||
}
|
||||
resourceControl.UserAccesses = userAccesses
|
||||
|
||||
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
|
||||
for _, v := range req.Teams {
|
||||
teamAccess := portainer.TeamResourceAccess{
|
||||
TeamID: portainer.TeamID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
teamAccesses = append(teamAccesses, teamAccess)
|
||||
}
|
||||
resourceControl.TeamAccesses = teamAccesses
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlUpdate(resourceControl, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteResources handles DELETE requests on /resources/:id
|
||||
func (handler *ResourceHandler) handleDeleteResources(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
resourceControlID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
|
||||
if err == portainer.ErrResourceControlNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package resourcecontrols
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle resource control operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage resource control operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/resource_controls",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/resource_controls/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/resource_controls/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package resourcecontrols
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type resourceControlCreatePayload struct {
|
||||
ResourceID string
|
||||
Type string
|
||||
AdministratorsOnly bool
|
||||
Users []int
|
||||
Teams []int
|
||||
SubResourceIDs []string
|
||||
}
|
||||
|
||||
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.ResourceID) {
|
||||
return portainer.Error("Invalid resource identifier")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.Type) {
|
||||
return portainer.Error("Invalid type")
|
||||
}
|
||||
|
||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.AdministratorsOnly {
|
||||
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or AdministratorOnly")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/resource_controls
|
||||
func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload resourceControlCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
var resourceControlType portainer.ResourceControlType
|
||||
switch payload.Type {
|
||||
case "container":
|
||||
resourceControlType = portainer.ContainerResourceControl
|
||||
case "service":
|
||||
resourceControlType = portainer.ServiceResourceControl
|
||||
case "volume":
|
||||
resourceControlType = portainer.VolumeResourceControl
|
||||
case "network":
|
||||
resourceControlType = portainer.NetworkResourceControl
|
||||
case "secret":
|
||||
resourceControlType = portainer.SecretResourceControl
|
||||
case "stack":
|
||||
resourceControlType = portainer.StackResourceControl
|
||||
case "config":
|
||||
resourceControlType = portainer.ConfigResourceControl
|
||||
default:
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", portainer.ErrInvalidResourceControlType}
|
||||
}
|
||||
|
||||
rc, err := handler.ResourceControlService.ResourceControlByResourceID(payload.ResourceID)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
|
||||
}
|
||||
if rc != nil {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A resource control is already associated to this resource", portainer.ErrResourceControlAlreadyExists}
|
||||
}
|
||||
|
||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||
for _, v := range payload.Users {
|
||||
userAccess := portainer.UserResourceAccess{
|
||||
UserID: portainer.UserID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
userAccesses = append(userAccesses, userAccess)
|
||||
}
|
||||
|
||||
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
|
||||
for _, v := range payload.Teams {
|
||||
teamAccess := portainer.TeamResourceAccess{
|
||||
TeamID: portainer.TeamID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
teamAccesses = append(teamAccesses, teamAccess)
|
||||
}
|
||||
|
||||
resourceControl := portainer.ResourceControl{
|
||||
ResourceID: payload.ResourceID,
|
||||
SubResourceIDs: payload.SubResourceIDs,
|
||||
Type: resourceControlType,
|
||||
AdministratorsOnly: payload.AdministratorsOnly,
|
||||
UserAccesses: userAccesses,
|
||||
TeamAccesses: teamAccesses,
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create a resource control for the specified resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the resource control inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, resourceControl)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package resourcecontrols
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// DELETE request on /api/resource_controls/:id
|
||||
func (handler *Handler) resourceControlDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
resourceControlID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
if err == portainer.ErrResourceControlNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the resource control", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package resourcecontrols
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type resourceControlUpdatePayload struct {
|
||||
AdministratorsOnly bool
|
||||
Users []int
|
||||
Teams []int
|
||||
}
|
||||
|
||||
func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
|
||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.AdministratorsOnly {
|
||||
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or AdministratorOnly")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/resource_controls/:id
|
||||
func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
resourceControlID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload resourceControlUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
if err == portainer.ErrResourceControlNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
resourceControl.AdministratorsOnly = payload.AdministratorsOnly
|
||||
|
||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||
for _, v := range payload.Users {
|
||||
userAccess := portainer.UserResourceAccess{
|
||||
UserID: portainer.UserID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
userAccesses = append(userAccesses, userAccess)
|
||||
}
|
||||
resourceControl.UserAccesses = userAccesses
|
||||
|
||||
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
|
||||
for _, v := range payload.Teams {
|
||||
teamAccess := portainer.TeamResourceAccess{
|
||||
TeamID: portainer.TeamID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
teamAccesses = append(teamAccesses, teamAccess)
|
||||
}
|
||||
resourceControl.TeamAccesses = teamAccesses
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlUpdate(resourceControl, securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, resourceControl)
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// SettingsHandler represents an HTTP API handler for managing Settings.
|
||||
type SettingsHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
SettingsService portainer.SettingsService
|
||||
LDAPService portainer.LDAPService
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewSettingsHandler returns a new instance of OldSettingsHandler.
|
||||
func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
|
||||
h := &SettingsHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet)
|
||||
h.Handle("/settings",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettings))).Methods(http.MethodPut)
|
||||
h.Handle("/settings/public",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetPublicSettings))).Methods(http.MethodGet)
|
||||
h.Handle("/settings/authentication/checkLDAP",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettingsLDAPCheck))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
publicSettingsResponse struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
}
|
||||
|
||||
putSettingsRequest struct {
|
||||
TemplatesURL string `valid:"required"`
|
||||
LogoURL string `valid:""`
|
||||
BlackListedLabels []portainer.Pair `valid:""`
|
||||
DisplayExternalContributors bool `valid:""`
|
||||
AuthenticationMethod int `valid:"required"`
|
||||
LDAPSettings portainer.LDAPSettings `valid:""`
|
||||
AllowBindMountsForRegularUsers bool `valid:""`
|
||||
AllowPrivilegedModeForRegularUsers bool `valid:""`
|
||||
}
|
||||
|
||||
putSettingsLDAPCheckRequest struct {
|
||||
LDAPSettings portainer.LDAPSettings `valid:""`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetSettings handles GET requests on /settings
|
||||
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, settings, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// handleGetPublicSettings handles GET requests on /settings/public
|
||||
func (handler *SettingsHandler) handleGetPublicSettings(w http.ResponseWriter, r *http.Request) {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
DisplayExternalContributors: settings.DisplayExternalContributors,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
}
|
||||
|
||||
encodeJSON(w, publicSettings, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// handlePutSettings handles PUT requests on /settings
|
||||
func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var req putSettingsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
settings := &portainer.Settings{
|
||||
TemplatesURL: req.TemplatesURL,
|
||||
LogoURL: req.LogoURL,
|
||||
BlackListedLabels: req.BlackListedLabels,
|
||||
DisplayExternalContributors: req.DisplayExternalContributors,
|
||||
LDAPSettings: req.LDAPSettings,
|
||||
AllowBindMountsForRegularUsers: req.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: req.AllowPrivilegedModeForRegularUsers,
|
||||
}
|
||||
|
||||
if req.AuthenticationMethod == 1 {
|
||||
settings.AuthenticationMethod = portainer.AuthenticationInternal
|
||||
} else if req.AuthenticationMethod == 2 {
|
||||
settings.AuthenticationMethod = portainer.AuthenticationLDAP
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
|
||||
err := handler.FileService.DeleteTLSFiles(filesystem.LDAPStorePath)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.SettingsService.StoreSettings(settings)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePutSettingsLDAPCheck handles PUT requests on /settings/ldap/check
|
||||
func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter, r *http.Request) {
|
||||
var req putSettingsLDAPCheckRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if (req.LDAPSettings.TLSConfig.TLS || req.LDAPSettings.StartTLS) && !req.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
req.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
err = handler.LDAPService.TestConnectivity(&req.LDAPSettings)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle settings operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
SettingsService portainer.SettingsService
|
||||
LDAPService portainer.LDAPService
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/settings",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/settings/public",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
|
||||
h.Handle("/settings/authentication/checkLDAP",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/settings
|
||||
func (handler *Handler) settingsInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, settings)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type settingsLDAPCheckPayload struct {
|
||||
LDAPSettings portainer.LDAPSettings
|
||||
}
|
||||
|
||||
func (payload *settingsLDAPCheckPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /settings/ldap/check
|
||||
func (handler *Handler) settingsLDAPCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload settingsLDAPCheckPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
if (payload.LDAPSettings.TLSConfig.TLS || payload.LDAPSettings.StartTLS) && !payload.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
payload.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
err = handler.LDAPService.TestConnectivity(&payload.LDAPSettings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to connect to LDAP server", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type publicSettingsResponse struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
}
|
||||
|
||||
// GET request on /api/settings/public
|
||||
func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
DisplayExternalContributors: settings.DisplayExternalContributors,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
}
|
||||
|
||||
return response.JSON(w, publicSettings)
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type settingsUpdatePayload struct {
|
||||
TemplatesURL string
|
||||
LogoURL string
|
||||
BlackListedLabels []portainer.Pair
|
||||
DisplayExternalContributors bool
|
||||
AuthenticationMethod int
|
||||
LDAPSettings portainer.LDAPSettings
|
||||
AllowBindMountsForRegularUsers bool
|
||||
AllowPrivilegedModeForRegularUsers bool
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.TemplatesURL) || !govalidator.IsURL(payload.TemplatesURL) {
|
||||
return portainer.Error("Invalid templates URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.AuthenticationMethod == 0 {
|
||||
return portainer.Error("Invalid authentication method")
|
||||
}
|
||||
if payload.AuthenticationMethod != 1 && payload.AuthenticationMethod != 2 {
|
||||
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal) or 2 (LDAP/AD)")
|
||||
}
|
||||
if !govalidator.IsNull(payload.LogoURL) && !govalidator.IsURL(payload.LogoURL) {
|
||||
return portainer.Error("Invalid logo URL. Must correspond to a valid URL format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/settings
|
||||
func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload settingsUpdatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
settings := &portainer.Settings{
|
||||
TemplatesURL: payload.TemplatesURL,
|
||||
LogoURL: payload.LogoURL,
|
||||
BlackListedLabels: payload.BlackListedLabels,
|
||||
DisplayExternalContributors: payload.DisplayExternalContributors,
|
||||
LDAPSettings: payload.LDAPSettings,
|
||||
AllowBindMountsForRegularUsers: payload.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: payload.AllowPrivilegedModeForRegularUsers,
|
||||
}
|
||||
|
||||
settings.AuthenticationMethod = portainer.AuthenticationMethod(payload.AuthenticationMethod)
|
||||
tlsError := handler.updateTLS(settings)
|
||||
if tlsError != nil {
|
||||
return tlsError
|
||||
}
|
||||
|
||||
err = handler.SettingsService.StoreSettings(settings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, settings)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateTLS(settings *portainer.Settings) *httperror.HandlerError {
|
||||
if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
|
||||
err := handler.FileService.DeleteTLSFiles(filesystem.LDAPStorePath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,794 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// StackHandler represents an HTTP API handler for managing Stack.
|
||||
type StackHandler struct {
|
||||
stackCreationMutex *sync.Mutex
|
||||
stackDeletionMutex *sync.Mutex
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
StackService portainer.StackService
|
||||
EndpointService portainer.EndpointService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
StackManager portainer.StackManager
|
||||
}
|
||||
|
||||
type stackDeploymentConfig struct {
|
||||
endpoint *portainer.Endpoint
|
||||
stack *portainer.Stack
|
||||
prune bool
|
||||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
}
|
||||
|
||||
// NewStackHandler returns a new instance of StackHandler.
|
||||
func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler {
|
||||
h := &StackHandler{
|
||||
Router: mux.NewRouter(),
|
||||
stackCreationMutex: &sync.Mutex{},
|
||||
stackDeletionMutex: &sync.Mutex{},
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/{endpointId}/stacks",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostStacks))).Methods(http.MethodPost)
|
||||
h.Handle("/{endpointId}/stacks",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStacks))).Methods(http.MethodGet)
|
||||
h.Handle("/{endpointId}/stacks/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStack))).Methods(http.MethodGet)
|
||||
h.Handle("/{endpointId}/stacks/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteStack))).Methods(http.MethodDelete)
|
||||
h.Handle("/{endpointId}/stacks/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutStack))).Methods(http.MethodPut)
|
||||
h.Handle("/{endpointId}/stacks/{id}/stackfile",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStackFile))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postStacksRequest struct {
|
||||
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"`
|
||||
}
|
||||
getStackFileResponse struct {
|
||||
StackFileContent string `json:"StackFileContent"`
|
||||
}
|
||||
putStackRequest struct {
|
||||
StackFileContent string `valid:"required"`
|
||||
Env []portainer.Pair `valid:""`
|
||||
Prune bool `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostStacks handles POST requests on /:endpointId/stacks?method=<method>
|
||||
func (handler *StackHandler) handlePostStacks(w http.ResponseWriter, r *http.Request) {
|
||||
method := r.FormValue("method")
|
||||
if method == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if method == "string" {
|
||||
handler.handlePostStacksStringMethod(w, r)
|
||||
} else if method == "repository" {
|
||||
handler.handlePostStacksRepositoryMethod(w, r)
|
||||
} else if method == "file" {
|
||||
handler.handlePostStacksFileMethod(w, r)
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postStacksRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackName := req.Name
|
||||
if stackName == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackFileContent := req.StackFileContent
|
||||
if stackFileContent == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
swarmID := req.SwarmID
|
||||
if swarmID == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil && err != portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, stackName) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrStackAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackName + "_" + swarmID),
|
||||
Name: stackName,
|
||||
SwarmID: swarmID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: req.Env,
|
||||
}
|
||||
|
||||
projectPath, err := handler.FileService.StoreStackFileFromString(string(stack.ID), stack.EntryPoint, stackFileContent)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredRegistries, err := security.FilterRegistries(registries, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
config := stackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: false,
|
||||
}
|
||||
err = handler.deployStack(&config)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postStacksResponse{ID: string(stack.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postStacksRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackName := req.Name
|
||||
swarmID := req.SwarmID
|
||||
|
||||
if stackName == "" || swarmID == "" || req.RepositoryURL == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.RepositoryAuthentication && (req.RepositoryUsername == "" || req.RepositoryPassword == "") {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.ComposeFilePathInRepository == "" {
|
||||
req.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil && err != portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, stackName) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrStackAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackName + "_" + swarmID),
|
||||
Name: stackName,
|
||||
SwarmID: swarmID,
|
||||
EntryPoint: req.ComposeFilePathInRepository,
|
||||
Env: req.Env,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(string(stack.ID))
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
// Ensure projectPath is empty
|
||||
err = handler.FileService.RemoveDirectory(projectPath)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredRegistries, err := security.FilterRegistries(registries, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
config := stackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: false,
|
||||
}
|
||||
err = handler.deployStack(&config)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postStacksResponse{ID: string(stack.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackName := r.FormValue("Name")
|
||||
if stackName == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
swarmID := r.FormValue("SwarmID")
|
||||
if swarmID == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
envParam := r.FormValue("Env")
|
||||
var env []portainer.Pair
|
||||
if err = json.Unmarshal([]byte(envParam), &env); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackFile, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
defer stackFile.Close()
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil && err != portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, stackName) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrStackAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackName + "_" + swarmID),
|
||||
Name: stackName,
|
||||
SwarmID: swarmID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: env,
|
||||
}
|
||||
|
||||
projectPath, err := handler.FileService.StoreStackFileFromReader(string(stack.ID), stack.EntryPoint, stackFile)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredRegistries, err := security.FilterRegistries(registries, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
config := stackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: false,
|
||||
}
|
||||
err = handler.deployStack(&config)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postStacksResponse{ID: string(stack.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetStacks handles GET requests on /:endpointId/stacks?swarmId=<swarmId>
|
||||
func (handler *StackHandler) handleGetStacks(w http.ResponseWriter, r *http.Request) {
|
||||
swarmID := r.FormValue("swarmId")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
_, err = handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var stacks []portainer.Stack
|
||||
if swarmID == "" {
|
||||
stacks, err = handler.StackService.Stacks()
|
||||
} else {
|
||||
stacks, err = handler.StackService.StacksBySwarmID(swarmID)
|
||||
}
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControls, err := handler.ResourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredStacks := proxy.FilterStacks(stacks, resourceControls, securityContext.IsAdmin,
|
||||
securityContext.UserID, securityContext.UserMemberships)
|
||||
|
||||
encodeJSON(w, filteredStacks, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetStack handles GET requests on /:endpointId/stacks/:id
|
||||
func (handler *StackHandler) handleGetStack(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
stackID := vars["id"]
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
||||
if resourceControl != nil {
|
||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
extendedStack.ResourceControl = *resourceControl
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, extendedStack, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutStack handles PUT requests on /:endpointId/stacks/:id
|
||||
func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
stackID := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putStackRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
stack.Env = req.Env
|
||||
|
||||
_, err = handler.FileService.StoreStackFileFromString(string(stack.ID), stack.EntryPoint, req.StackFileContent)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.StackService.UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredRegistries, err := security.FilterRegistries(registries, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
config := stackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: req.Prune,
|
||||
}
|
||||
err = handler.deployStack(&config)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetStackFile handles GET requests on /:endpointId/stacks/:id/stackfile
|
||||
func (handler *StackHandler) handleGetStackFile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
stackID := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &getStackFileResponse{StackFileContent: stackFileContent}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleDeleteStack handles DELETE requests on /:endpointId/stacks/:id
|
||||
func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
stackID := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
handler.stackDeletionMutex.Lock()
|
||||
err = handler.StackManager.Remove(stack, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
handler.stackDeletionMutex.Unlock()
|
||||
|
||||
err = handler.StackService.DeleteStack(portainer.StackID(stackID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *StackHandler) deployStack(config *stackDeploymentConfig) error {
|
||||
handler.stackCreationMutex.Lock()
|
||||
|
||||
handler.StackManager.Login(config.dockerhub, config.registries, config.endpoint)
|
||||
|
||||
err := handler.StackManager.Deploy(config.stack, config.prune, config.endpoint)
|
||||
if err != nil {
|
||||
handler.stackCreationMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.StackManager.Logout(config.endpoint)
|
||||
if err != nil {
|
||||
handler.stackCreationMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
handler.stackCreationMutex.Unlock()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type composeStackFromFileContentPayload struct {
|
||||
Name string
|
||||
StackFileContent string
|
||||
}
|
||||
|
||||
func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return portainer.Error("Invalid stack file content")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
var payload composeStackFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
stackIdentifier := buildStackIdentifier(payload.Name, endpoint.ID)
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackIdentifier),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
}
|
||||
|
||||
projectPath, err := handler.FileService.StoreStackFileFromBytes(string(stack.ID), stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
err = handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
type composeStackFromGitRepositoryPayload struct {
|
||||
Name string
|
||||
RepositoryURL string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
ComposeFilePathInRepository string
|
||||
}
|
||||
|
||||
func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
|
||||
return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
var payload composeStackFromGitRepositoryPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
stackIdentifier := buildStackIdentifier(payload.Name, endpoint.ID)
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackIdentifier),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(string(stack.ID))
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
gitCloneParams := &cloneRepositoryParameters{
|
||||
url: payload.RepositoryURL,
|
||||
path: projectPath,
|
||||
authentication: payload.RepositoryAuthentication,
|
||||
username: payload.RepositoryUsername,
|
||||
password: payload.RepositoryPassword,
|
||||
}
|
||||
err = handler.cloneGitRepository(gitCloneParams)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
|
||||
}
|
||||
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
err = handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
type composeStackFromFileUploadPayload struct {
|
||||
Name string
|
||||
StackFileContent []byte
|
||||
}
|
||||
|
||||
func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
composeFileContent, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
|
||||
}
|
||||
payload.StackFileContent = composeFileContent
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
payload := &composeStackFromFileUploadPayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
stackIdentifier := buildStackIdentifier(payload.Name, endpoint.ID)
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackIdentifier),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
}
|
||||
|
||||
projectPath, err := handler.FileService.StoreStackFileFromBytes(string(stack.ID), stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
err = handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
type composeStackDeploymentConfig struct {
|
||||
stack *portainer.Stack
|
||||
endpoint *portainer.Endpoint
|
||||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||
|
||||
config := &composeStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// TODO: libcompose uses credentials store into a config.json file to pull images from
|
||||
// private registries. Right now the only solution is to re-use the embedded Docker binary
|
||||
// to login/logout, which will generate the required data in the config.json file and then
|
||||
// clean it. Hence the use of the mutex.
|
||||
// We should contribute to libcompose to support authentication without using the config.json file.
|
||||
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
|
||||
|
||||
err := handler.ComposeStackManager.Up(config.stack, config.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handler.SwarmStackManager.Logout(config.endpoint)
|
||||
}
|
|
@ -0,0 +1,334 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type swarmStackFromFileContentPayload struct {
|
||||
Name string
|
||||
SwarmID string
|
||||
StackFileContent string
|
||||
Env []portainer.Pair
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
if govalidator.IsNull(payload.SwarmID) {
|
||||
return portainer.Error("Invalid Swarm ID")
|
||||
}
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return portainer.Error("Invalid stack file content")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
var payload swarmStackFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
stackIdentifier := buildStackIdentifier(payload.Name, endpoint.ID)
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackIdentifier),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
projectPath, err := handler.FileService.StoreStackFileFromBytes(string(stack.ID), stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
err = handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
type swarmStackFromGitRepositoryPayload struct {
|
||||
Name string
|
||||
SwarmID string
|
||||
Env []portainer.Pair
|
||||
RepositoryURL string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
ComposeFilePathInRepository string
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
if govalidator.IsNull(payload.SwarmID) {
|
||||
return portainer.Error("Invalid Swarm ID")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
|
||||
return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
var payload swarmStackFromGitRepositoryPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
stackIdentifier := buildStackIdentifier(payload.Name, endpoint.ID)
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackIdentifier),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(string(stack.ID))
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
gitCloneParams := &cloneRepositoryParameters{
|
||||
url: payload.RepositoryURL,
|
||||
path: projectPath,
|
||||
authentication: payload.RepositoryAuthentication,
|
||||
username: payload.RepositoryUsername,
|
||||
password: payload.RepositoryPassword,
|
||||
}
|
||||
err = handler.cloneGitRepository(gitCloneParams)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
|
||||
}
|
||||
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
err = handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
type swarmStackFromFileUploadPayload struct {
|
||||
Name string
|
||||
SwarmID string
|
||||
StackFileContent []byte
|
||||
Env []portainer.Pair
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
swarmID, err := request.RetrieveMultiPartFormValue(r, "SwarmID", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Swarm ID")
|
||||
}
|
||||
payload.SwarmID = swarmID
|
||||
|
||||
composeFileContent, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
|
||||
}
|
||||
payload.StackFileContent = composeFileContent
|
||||
|
||||
var env []portainer.Pair
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Env", &env, true)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Env parameter")
|
||||
}
|
||||
payload.Env = env
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
payload := &swarmStackFromFileUploadPayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
stackIdentifier := buildStackIdentifier(payload.Name, endpoint.ID)
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackIdentifier),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
projectPath, err := handler.FileService.StoreStackFileFromBytes(string(stack.ID), stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
err = handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
type swarmStackDeploymentConfig struct {
|
||||
stack *portainer.Stack
|
||||
endpoint *portainer.Endpoint
|
||||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
prune bool
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
dockerhub, err := handler.DockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||
|
||||
config := &swarmStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: prune,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
|
||||
|
||||
err := handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.SwarmStackManager.Logout(config.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package stacks
|
||||
|
||||
type cloneRepositoryParameters struct {
|
||||
url string
|
||||
path string
|
||||
authentication bool
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
|
||||
if parameters.authentication {
|
||||
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.path, parameters.username, parameters.password)
|
||||
}
|
||||
return handler.GitService.ClonePublicRepository(parameters.url, parameters.path)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle stack operations.
|
||||
type Handler struct {
|
||||
stackCreationMutex *sync.Mutex
|
||||
stackDeletionMutex *sync.Mutex
|
||||
*mux.Router
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
StackService portainer.StackService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SwarmStackManager portainer.SwarmStackManager
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage stack operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
stackCreationMutex: &sync.Mutex{},
|
||||
stackDeletionMutex: &sync.Mutex{},
|
||||
}
|
||||
h.Handle("/stacks",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/stacks",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackList))).Methods(http.MethodGet)
|
||||
h.Handle("/stacks/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/stacks/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/stacks/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/stacks/{id}/file",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) checkEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID) error {
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !security.AuthorizedEndpointAccess(endpoint, group, userID, memberships) {
|
||||
return portainer.ErrEndpointAccessDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
|
||||
if !*doCleanUp {
|
||||
return nil
|
||||
}
|
||||
|
||||
handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildStackIdentifier(stackName string, endpointID portainer.EndpointID) string {
|
||||
return stackName + "_" + strconv.Itoa(int(endpointID))
|
||||
}
|
||||
|
||||
// POST request on /api/stacks?type=<type>&method=<method>&endpointId=<endpointId>
|
||||
func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackType, err := request.RetrieveNumericQueryParameter(r, "type", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: type", err}
|
||||
}
|
||||
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
||||
}
|
||||
}
|
||||
|
||||
switch portainer.StackType(stackType) {
|
||||
case portainer.DockerSwarmStack:
|
||||
return handler.createSwarmStack(w, r, method, endpoint)
|
||||
case portainer.DockerComposeStack:
|
||||
return handler.createComposeStack(w, r, method, endpoint)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", request.ErrInvalidQueryParameter}
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
|
||||
switch method {
|
||||
case "string":
|
||||
return handler.createComposeStackFromFileContent(w, r, endpoint)
|
||||
case "repository":
|
||||
return handler.createComposeStackFromGitRepository(w, r, endpoint)
|
||||
case "file":
|
||||
return handler.createComposeStackFromFileUpload(w, r, endpoint)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", request.ErrInvalidQueryParameter}
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
switch method {
|
||||
case "string":
|
||||
return handler.createSwarmStackFromFileContent(w, r, endpoint)
|
||||
case "repository":
|
||||
return handler.createSwarmStackFromGitRepository(w, r, endpoint)
|
||||
case "file":
|
||||
return handler.createSwarmStackFromFileUpload(w, r, endpoint)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", request.ErrInvalidQueryParameter}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// DELETE request on /api/stacks/:id?external=<external>&endpointId=<endpointId>
|
||||
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
|
||||
if externalStack {
|
||||
return handler.deleteExternalStack(r, w, stackID)
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
if !securityContext.IsAdmin && !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.deleteStack(stack, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
err = handler.StackService.DeleteStack(portainer.StackID(stackID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err}
|
||||
}
|
||||
|
||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string) *httperror.HandlerError {
|
||||
stack, err := handler.StackService.StackByName(stackName)
|
||||
if err != nil && err != portainer.ErrStackNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err}
|
||||
}
|
||||
if stack != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", portainer.ErrStackNotExternal}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
|
||||
if err != nil && err == portainer.ErrEndpointAccessDenied {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify permission to access endpoint", err}
|
||||
}
|
||||
}
|
||||
|
||||
stack = &portainer.Stack{
|
||||
Name: stackName,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
}
|
||||
|
||||
err = handler.deleteStack(stack, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete stack", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
if stack.Type == portainer.DockerSwarmStack {
|
||||
return handler.SwarmStackManager.Remove(stack, endpoint)
|
||||
}
|
||||
return handler.ComposeStackManager.Down(stack, endpoint)
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type stackFileResponse struct {
|
||||
StackFileContent string `json:"StackFileContent"`
|
||||
}
|
||||
|
||||
// GET request on /api/stacks/:id/file
|
||||
func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
||||
if resourceControl != nil {
|
||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
extendedStack.ResourceControl = *resourceControl
|
||||
} else {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &stackFileResponse{StackFileContent: stackFileContent})
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/stacks/:id
|
||||
func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
||||
if resourceControl != nil {
|
||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
extendedStack.ResourceControl = *resourceControl
|
||||
} else {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, extendedStack)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type stackListOperationFilters struct {
|
||||
SwarmID string `json:"SwarmID"`
|
||||
EndpointID int `json:"EndpointID"`
|
||||
}
|
||||
|
||||
// GET request on /api/stacks?(filters=<filters>)
|
||||
func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var filters stackListOperationFilters
|
||||
err := request.RetrieveJSONQueryParameter(r, "filters", &filters, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
}
|
||||
stacks = filterStacks(stacks, &filters)
|
||||
|
||||
resourceControls, err := handler.ResourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls 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}
|
||||
}
|
||||
|
||||
filteredStacks := proxy.FilterStacks(stacks, resourceControls, securityContext.IsAdmin,
|
||||
securityContext.UserID, securityContext.UserMemberships)
|
||||
|
||||
return response.JSON(w, filteredStacks)
|
||||
}
|
||||
|
||||
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters) []portainer.Stack {
|
||||
if filters.EndpointID == 0 && filters.SwarmID == "" {
|
||||
return stacks
|
||||
}
|
||||
|
||||
filteredStacks := make([]portainer.Stack, 0, len(stacks))
|
||||
for _, stack := range stacks {
|
||||
if stack.Type == portainer.DockerComposeStack && stack.EndpointID == portainer.EndpointID(filters.EndpointID) {
|
||||
filteredStacks = append(filteredStacks, stack)
|
||||
}
|
||||
if stack.Type == portainer.DockerSwarmStack && stack.SwarmID == filters.SwarmID {
|
||||
filteredStacks = append(filteredStacks, stack)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredStacks
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type updateComposeStackPayload struct {
|
||||
StackFileContent string
|
||||
}
|
||||
|
||||
func (payload *updateComposeStackPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return portainer.Error("Invalid stack file content")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateSwarmStackPayload struct {
|
||||
StackFileContent string
|
||||
Env []portainer.Pair
|
||||
Prune bool
|
||||
}
|
||||
|
||||
func (payload *updateSwarmStackPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return portainer.Error("Invalid stack file content")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/stacks/:id
|
||||
func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
if !securityContext.IsAdmin && !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
||||
if updateError != nil {
|
||||
return updateError
|
||||
}
|
||||
|
||||
err = handler.StackService.UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateAndDeployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
if stack.Type == portainer.DockerSwarmStack {
|
||||
return handler.updateSwarmStack(r, stack, endpoint)
|
||||
}
|
||||
return handler.updateComposeStack(r, stack, endpoint)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
var payload updateComposeStackPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
_, err = handler.FileService.StoreStackFileFromBytes(string(stack.ID), stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
|
||||
}
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
err = handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
var payload updateSwarmStackPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stack.Env = payload.Env
|
||||
|
||||
_, err = handler.FileService.StoreStackFileFromBytes(string(stack.ID), stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
|
||||
}
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
err = handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// StatusHandler represents an HTTP API handler for managing Status.
|
||||
type StatusHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
Status *portainer.Status
|
||||
}
|
||||
|
||||
// NewStatusHandler returns a new instance of StatusHandler.
|
||||
func NewStatusHandler(bouncer *security.RequestBouncer, status *portainer.Status) *StatusHandler {
|
||||
h := &StatusHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
Status: status,
|
||||
}
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetStatus))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetStatus handles GET requests on /status
|
||||
func (handler *StatusHandler) handleGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
encodeJSON(w, handler.Status, handler.Logger)
|
||||
return
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle status operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
Status *portainer.Status
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage status operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Status: status,
|
||||
}
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/status
|
||||
func (handler *Handler) statusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
return response.JSON(w, handler.Status)
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// TeamHandler represents an HTTP API handler for managing teams.
|
||||
type TeamHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewTeamHandler returns a new instance of TeamHandler.
|
||||
func NewTeamHandler(bouncer *security.RequestBouncer) *TeamHandler {
|
||||
h := &TeamHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/teams",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostTeams))).Methods(http.MethodPost)
|
||||
h.Handle("/teams",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeam))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutTeam))).Methods(http.MethodPut)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteTeam))).Methods(http.MethodDelete)
|
||||
h.Handle("/teams/{id}/memberships",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetMemberships))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postTeamsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
}
|
||||
|
||||
postTeamsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putTeamRequest struct {
|
||||
Name string `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostTeams handles POST requests on /teams
|
||||
func (handler *TeamHandler) handlePostTeams(w http.ResponseWriter, r *http.Request) {
|
||||
var req postTeamsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.TeamByName(req.Name)
|
||||
if err != nil && err != portainer.ErrTeamNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if team != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrTeamAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
team = &portainer.Team{
|
||||
Name: req.Name,
|
||||
}
|
||||
|
||||
err = handler.TeamService.CreateTeam(team)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postTeamsResponse{ID: int(team.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetTeams handles GET requests on /teams
|
||||
func (handler *TeamHandler) handleGetTeams(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := handler.TeamService.Teams()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredTeams := security.FilterUserTeams(teams, securityContext)
|
||||
|
||||
encodeJSON(w, filteredTeams, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetTeam handles GET requests on /teams/:id
|
||||
func (handler *TeamHandler) handleGetTeam(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
tid, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
teamID := portainer.TeamID(tid)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(teamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.Team(teamID)
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &team, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutTeam handles PUT requests on /teams/:id
|
||||
func (handler *TeamHandler) handlePutTeam(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
teamID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putTeamRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.Team(portainer.TeamID(teamID))
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
team.Name = req.Name
|
||||
}
|
||||
|
||||
err = handler.TeamService.UpdateTeam(team.ID, team)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteTeam handles DELETE requests on /teams/:id
|
||||
func (handler *TeamHandler) handleDeleteTeam(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
teamID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.TeamService.Team(portainer.TeamID(teamID))
|
||||
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.TeamService.DeleteTeam(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembershipByTeamID(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetMemberships handles GET requests on /teams/:id/memberships
|
||||
func (handler *TeamHandler) handleGetMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
tid, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
teamID := portainer.TeamID(tid)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(teamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByTeamID(teamID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, memberships, handler.Logger)
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// TeamMembershipHandler represents an HTTP API handler for managing teams.
|
||||
type TeamMembershipHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewTeamMembershipHandler returns a new instance of TeamMembershipHandler.
|
||||
func NewTeamMembershipHandler(bouncer *security.RequestBouncer) *TeamMembershipHandler {
|
||||
h := &TeamMembershipHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostTeamMemberships))).Methods(http.MethodPost)
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeamsMemberships))).Methods(http.MethodGet)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutTeamMembership))).Methods(http.MethodPut)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteTeamMembership))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postTeamMembershipsRequest struct {
|
||||
UserID int `valid:"required"`
|
||||
TeamID int `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
postTeamMembershipsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putTeamMembershipRequest struct {
|
||||
UserID int `valid:"required"`
|
||||
TeamID int `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostTeamMemberships handles POST requests on /team_memberships
|
||||
func (handler *TeamMembershipHandler) handlePostTeamMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postTeamMembershipsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
userID := portainer.UserID(req.UserID)
|
||||
teamID := portainer.TeamID(req.TeamID)
|
||||
role := portainer.MembershipRole(req.Role)
|
||||
|
||||
if !security.AuthorizedTeamManagement(teamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(memberships) > 0 {
|
||||
for _, membership := range memberships {
|
||||
if membership.UserID == userID && membership.TeamID == teamID {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrTeamMembershipAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
membership := &portainer.TeamMembership{
|
||||
UserID: userID,
|
||||
TeamID: teamID,
|
||||
Role: role,
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.CreateTeamMembership(membership)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postTeamMembershipsResponse{ID: int(membership.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetTeamsMemberships handles GET requests on /team_memberships
|
||||
func (handler *TeamMembershipHandler) handleGetTeamsMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMemberships()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, memberships, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutTeamMembership handles PUT requests on /team_memberships/:id
|
||||
func (handler *TeamMembershipHandler) handlePutTeamMembership(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
membershipID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putTeamMembershipRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
userID := portainer.UserID(req.UserID)
|
||||
teamID := portainer.TeamID(req.TeamID)
|
||||
role := portainer.MembershipRole(req.Role)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(teamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err == portainer.ErrTeamMembershipNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && membership.Role != role {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
membership.UserID = userID
|
||||
membership.TeamID = teamID
|
||||
membership.Role = role
|
||||
|
||||
err = handler.TeamMembershipService.UpdateTeamMembership(membership.ID, membership)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteTeamMembership handles DELETE requests on /team_memberships/:id
|
||||
func (handler *TeamMembershipHandler) handleDeleteTeamMembership(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
membershipID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err == portainer.ErrTeamMembershipNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(membership.TeamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package teammemberships
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle team membership operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team membership operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package teammemberships
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type teamMembershipCreatePayload struct {
|
||||
UserID int
|
||||
TeamID int
|
||||
Role int
|
||||
}
|
||||
|
||||
func (payload *teamMembershipCreatePayload) Validate(r *http.Request) error {
|
||||
if payload.UserID == 0 {
|
||||
return portainer.Error("Invalid UserID")
|
||||
}
|
||||
if payload.TeamID == 0 {
|
||||
return portainer.Error("Invalid TeamID")
|
||||
}
|
||||
if payload.Role != 1 && payload.Role != 2 {
|
||||
return portainer.Error("Invalid role value. Value must be one of: 1 (leader) or 2 (member)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/team_memberships
|
||||
func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload teamMembershipCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(portainer.TeamID(payload.TeamID), securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to manage team memberships", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(portainer.UserID(payload.UserID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve team memberships from the database", err}
|
||||
}
|
||||
|
||||
if len(memberships) > 0 {
|
||||
for _, membership := range memberships {
|
||||
if membership.UserID == portainer.UserID(payload.UserID) && membership.TeamID == portainer.TeamID(payload.TeamID) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Team membership already registered", portainer.ErrTeamMembershipAlreadyExists}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
membership := &portainer.TeamMembership{
|
||||
UserID: portainer.UserID(payload.UserID),
|
||||
TeamID: portainer.TeamID(payload.TeamID),
|
||||
Role: portainer.MembershipRole(payload.Role),
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.CreateTeamMembership(membership)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team memberships inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, membership)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package teammemberships
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// DELETE request on /api/team_memberships/:id
|
||||
func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
membershipID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid membership identifier route variable", err}
|
||||
}
|
||||
|
||||
membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err == portainer.ErrTeamMembershipNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team membership with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(membership.TeamID, securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the membership", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the team membership from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package teammemberships
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/team_memberships
|
||||
func (handler *Handler) teamMembershipList(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 && !securityContext.IsTeamLeader {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to list team memberships", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMemberships()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve team memberships from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, memberships)
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package teammemberships
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type teamMembershipUpdatePayload struct {
|
||||
UserID int
|
||||
TeamID int
|
||||
Role int
|
||||
}
|
||||
|
||||
func (payload *teamMembershipUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.UserID == 0 {
|
||||
return portainer.Error("Invalid UserID")
|
||||
}
|
||||
if payload.TeamID == 0 {
|
||||
return portainer.Error("Invalid TeamID")
|
||||
}
|
||||
if payload.Role != 1 && payload.Role != 2 {
|
||||
return portainer.Error("Invalid role value. Value must be one of: 1 (leader) or 2 (member)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/team_memberships/:id
|
||||
func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
membershipID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid membership identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload teamMembershipUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(portainer.TeamID(payload.TeamID), securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the membership", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err == portainer.ErrTeamMembershipNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team membership with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && membership.Role != portainer.MembershipRole(payload.Role) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the role of membership", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
membership.UserID = portainer.UserID(payload.UserID)
|
||||
membership.TeamID = portainer.TeamID(payload.TeamID)
|
||||
membership.Role = portainer.MembershipRole(payload.Role)
|
||||
|
||||
err = handler.TeamMembershipService.UpdateTeamMembership(membership.ID, membership)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist membership changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, membership)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle team operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/teams",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/teams",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/teams/{id}/memberships",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type teamCreatePayload struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (payload *teamCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid team name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload teamCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.TeamByName(payload.Name)
|
||||
if err != nil && err != portainer.ErrTeamNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve teams from the database", err}
|
||||
}
|
||||
if team != nil {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A team with the same name already exists", portainer.ErrTeamAlreadyExists}
|
||||
}
|
||||
|
||||
team = &portainer.Team{
|
||||
Name: payload.Name,
|
||||
}
|
||||
|
||||
err = handler.TeamService.CreateTeam(team)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the team inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, team)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// DELETE request on /api/teams/:id
|
||||
func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
teamID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid team identifier route variable", err}
|
||||
}
|
||||
|
||||
_, err = handler.TeamService.Team(portainer.TeamID(teamID))
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.TeamService.DeleteTeam(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the team from the database", err}
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembershipByTeamID(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete associated team memberships from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/teams/:id
|
||||
func (handler *Handler) teamInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
teamID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid team identifier route variable", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(portainer.TeamID(teamID), securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to team", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.Team(portainer.TeamID(teamID))
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, team)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/teams
|
||||
func (handler *Handler) teamList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
teams, err := handler.TeamService.Teams()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve teams 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}
|
||||
}
|
||||
|
||||
filteredTeams := security.FilterUserTeams(teams, securityContext)
|
||||
|
||||
return response.JSON(w, filteredTeams)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/teams/:id/memberships
|
||||
func (handler *Handler) teamMemberships(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
teamID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid team identifier route variable", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(portainer.TeamID(teamID), securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to team", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByTeamID(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve associated team memberships from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, memberships)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type teamUpdatePayload struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (payload *teamUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/teams/:id
|
||||
func (handler *Handler) teamUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
teamID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid team identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload teamUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.Team(portainer.TeamID(teamID))
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
team.Name = payload.Name
|
||||
}
|
||||
|
||||
err = handler.TeamService.UpdateTeam(team.ID, team)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to persist team changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, team)
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||
type TemplatesHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
const (
|
||||
containerTemplatesURLLinuxServerIo = "https://tools.linuxserver.io/portainer.json"
|
||||
)
|
||||
|
||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||
func NewTemplatesHandler(bouncer *security.RequestBouncer) *TemplatesHandler {
|
||||
h := &TemplatesHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/templates",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetTemplates handles GET requests on /templates?key=<key>
|
||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.FormValue("key")
|
||||
if key == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var templatesURL string
|
||||
switch key {
|
||||
case "containers":
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
templatesURL = settings.TemplatesURL
|
||||
case "linuxserver.io":
|
||||
templatesURL = containerTemplatesURLLinuxServerIo
|
||||
default:
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get(templatesURL)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(body)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
const (
|
||||
containerTemplatesURLLinuxServerIo = "https://tools.linuxserver.io/portainer.json"
|
||||
)
|
||||
|
||||
// Handler represents an HTTP API handler for managing templates.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewHandler returns a new instance of Handler.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/templates",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/templates?key=<key>
|
||||
func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
key, err := request.RetrieveQueryParameter(r, "key", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: key", err}
|
||||
}
|
||||
|
||||
templatesURL, templateErr := handler.retrieveTemplateURLFromKey(key)
|
||||
if templateErr != nil {
|
||||
return templateErr
|
||||
}
|
||||
|
||||
resp, err := http.Get(templatesURL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates via the network", err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to read template response", err}
|
||||
}
|
||||
|
||||
return response.Bytes(w, body, "application/json")
|
||||
}
|
||||
|
||||
func (handler *Handler) retrieveTemplateURLFromKey(key string) (string, *httperror.HandlerError) {
|
||||
switch key {
|
||||
case "containers":
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return "", &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
return settings.TemplatesURL, nil
|
||||
case "linuxserver.io":
|
||||
return containerTemplatesURLLinuxServerIo, nil
|
||||
}
|
||||
return "", &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: key. Value must be one of: containers or linuxserver.io", request.ErrInvalidQueryParameter}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// UploadHandler represents an HTTP API handler for managing file uploads.
|
||||
type UploadHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewUploadHandler returns a new instance of UploadHandler.
|
||||
func NewUploadHandler(bouncer *security.RequestBouncer) *UploadHandler {
|
||||
h := &UploadHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostUploadTLS))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
// handlePostUploadTLS handles POST requests on /upload/tls/{certificate:(?:ca|cert|key)}?folder=<folder>
|
||||
func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
certificate := vars["certificate"]
|
||||
|
||||
folder := r.FormValue("folder")
|
||||
if folder == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
file, _, err := r.FormFile("file")
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var fileType portainer.TLSFileType
|
||||
switch certificate {
|
||||
case "ca":
|
||||
fileType = portainer.TLSFileCA
|
||||
case "cert":
|
||||
fileType = portainer.TLSFileCert
|
||||
case "key":
|
||||
fileType = portainer.TLSFileKey
|
||||
default:
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.FileService.StoreTLSFile(folder, fileType, file)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package upload
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle upload operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage upload operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.uploadTLS))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package upload
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// POST request on /api/upload/tls/{certificate:(?:ca|cert|key)}?folder=<folder>
|
||||
func (handler *Handler) uploadTLS(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
certificate, err := request.RetrieveRouteVariableValue(r, "certificate")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid certificate route variable", err}
|
||||
}
|
||||
|
||||
folder, err := request.RetrieveMultiPartFormValue(r, "folder", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: folder", err}
|
||||
}
|
||||
|
||||
file, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid certificate file. Ensure that the certificate file is uploaded correctly", err}
|
||||
}
|
||||
|
||||
var fileType portainer.TLSFileType
|
||||
switch certificate {
|
||||
case "ca":
|
||||
fileType = portainer.TLSFileCA
|
||||
case "cert":
|
||||
fileType = portainer.TLSFileCert
|
||||
case "key":
|
||||
fileType = portainer.TLSFileKey
|
||||
default:
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid certificate route value. Value must be one of: ca, cert or key", portainer.ErrUndefinedTLSFileType}
|
||||
}
|
||||
|
||||
_, err = handler.FileService.StoreTLSFileFromBytes(folder, fileType, file)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist certificate file on disk", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -1,463 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// UserHandler represents an HTTP API handler for managing users.
|
||||
type UserHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewUserHandler returns a new instance of UserHandler.
|
||||
func NewUserHandler(bouncer *security.RequestBouncer) *UserHandler {
|
||||
h := &UserHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/users",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
|
||||
h.Handle("/users",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/memberships",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetMemberships))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUserPasswd))).Methods(http.MethodPost)
|
||||
h.Handle("/users/admin/check",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetAdminCheck))).Methods(http.MethodGet)
|
||||
h.Handle("/users/admin/init",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAdminInit))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postUsersRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:""`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
postUsersResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
postUserPasswdRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
postUserPasswdResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
putUserRequest struct {
|
||||
Password string `valid:"-"`
|
||||
Role int `valid:"-"`
|
||||
}
|
||||
|
||||
postAdminInitRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostUsers handles POST requests on /users
|
||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var req postUsersRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && req.Role == 1 {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.ContainsAny(req.Username, " ") {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrInvalidUsername, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername(req.Username)
|
||||
if err != nil && err != portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUserAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var role portainer.UserRole
|
||||
if req.Role == 1 {
|
||||
role = portainer.AdministratorRole
|
||||
} else {
|
||||
role = portainer.StandardUserRole
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: req.Username,
|
||||
Role: role,
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postUsersResponse{ID: int(user.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetUsers handles GET requests on /users
|
||||
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := handler.UserService.Users()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredUsers := security.FilterUsers(users, securityContext)
|
||||
|
||||
for i := range filteredUsers {
|
||||
filteredUsers[i].Password = ""
|
||||
}
|
||||
|
||||
encodeJSON(w, filteredUsers, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:id/passwd
|
||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postUserPasswdRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
valid := true
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
if err != nil {
|
||||
valid = false
|
||||
}
|
||||
|
||||
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetUser handles GET requests on /users/:id
|
||||
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
encodeJSON(w, &user, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutUser handles PUT requests on /users/:id
|
||||
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putUserRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password == "" && req.Role == 0 {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != "" {
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Role != 0 {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
if req.Role == 1 {
|
||||
user.Role = portainer.AdministratorRole
|
||||
} else {
|
||||
user.Role = portainer.StandardUserRole
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user.ID, user)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetAdminCheck handles GET requests on /users/admin/check
|
||||
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(users) == 0 {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
||||
var req postAdminInitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(users) == 0 {
|
||||
user := &portainer.User{
|
||||
Username: req.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteUser handles DELETE requests on /users/:id
|
||||
func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.ID == portainer.UserID(userID) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrAdminCannotRemoveSelf, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.DeleteUser(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetMemberships handles GET requests on /users/:id/memberships
|
||||
func (handler *UserHandler) handleGetMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, memberships, handler.Logger)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
// GET request on /api/users/admin/check
|
||||
func (handler *Handler) adminCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "No administrator account found inside the database", portainer.ErrUserNotFound}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
)
|
||||
|
||||
type adminInitPayload struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (payload *adminInitPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Username) || govalidator.Contains(payload.Username, " ") {
|
||||
return portainer.Error("Invalid username. Must not contain any whitespace")
|
||||
}
|
||||
if govalidator.IsNull(payload.Password) {
|
||||
return portainer.Error("Invalid password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/users/admin/init
|
||||
func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload adminInitPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||
}
|
||||
|
||||
if len(users) != 0 {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Unable to retrieve users from the database", portainer.ErrAdminAlreadyInitialized}
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure}
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, user)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func hideFields(user *portainer.User) {
|
||||
user.Password = ""
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle user operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage user operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/users",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/users",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/memberships",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userPassword))).Methods(http.MethodPost)
|
||||
h.Handle("/users/admin/check",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet)
|
||||
h.Handle("/users/admin/init",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminInit))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type userCreatePayload struct {
|
||||
Username string
|
||||
Password string
|
||||
Role int
|
||||
}
|
||||
|
||||
func (payload *userCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Username) || govalidator.Contains(payload.Username, " ") {
|
||||
return portainer.Error("Invalid username. Must not contain any whitespace")
|
||||
}
|
||||
|
||||
if payload.Role != 1 && payload.Role != 2 {
|
||||
return portainer.Error("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/users
|
||||
func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload userCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create user", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && payload.Role == 1 {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create administrator user", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername(payload.Username)
|
||||
if err != nil && err != portainer.ErrUserNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||
}
|
||||
if user != nil {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Another user with the same username already exists", portainer.ErrUserAlreadyExists}
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.UserRole(payload.Role),
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||
}
|
||||
|
||||
hideFields(user)
|
||||
return response.JSON(w, user)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/request"
|
||||
"github.com/portainer/portainer/http/response"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// DELETE request on /api/users/:id
|
||||
func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
userID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
|
||||
if tokenData.ID == portainer.UserID(userID) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Cannot remove your own user account. Contact another administrator", portainer.ErrAdminCannotRemoveSelf}
|
||||
}
|
||||
|
||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.UserService.DeleteUser(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user from the database", err}
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user memberships from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue