mirror of https://github.com/portainer/portainer
feat(extensions): remove rbac extension (#4157)
* feat(extensions): remove rbac extension client code * feat(extensions): remove server rbac code * remove extensions code * fix(notifications): remove error * feat(extensions): remove authorizations service * feat(rbac): deprecate fields * fix(portainer): revert change * fix(bouncer): remove rbac authorization check * feat(sidebar): remove roles link * fix(portainer): remove portainer modulepull/4196/head
parent
8629738e34
commit
9d18d47194
|
@ -340,11 +340,6 @@ func (store *Store) EndpointRelation() portainer.EndpointRelationService {
|
||||||
return store.EndpointRelationService
|
return store.EndpointRelationService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension gives access to the Extension data management layer
|
|
||||||
func (store *Store) Extension() portainer.ExtensionService {
|
|
||||||
return store.ExtensionService
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registry gives access to the Registry data management layer
|
// Registry gives access to the Registry data management layer
|
||||||
func (store *Store) Registry() portainer.RegistryService {
|
func (store *Store) Registry() portainer.RegistryService {
|
||||||
return store.RegistryService
|
return store.RegistryService
|
||||||
|
|
|
@ -3,7 +3,6 @@ package bolt
|
||||||
import (
|
import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt/errors"
|
"github.com/portainer/portainer/api/bolt/errors"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Init creates the default data set.
|
// Init creates the default data set.
|
||||||
|
@ -83,60 +82,5 @@ func (store *Store) Init() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
roles, err := store.RoleService.Roles()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(roles) == 0 {
|
|
||||||
environmentAdministratorRole := &portainer.Role{
|
|
||||||
Name: "Endpoint administrator",
|
|
||||||
Description: "Full control of all resources in an endpoint",
|
|
||||||
Priority: 1,
|
|
||||||
Authorizations: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
environmentReadOnlyUserRole := &portainer.Role{
|
|
||||||
Name: "Helpdesk",
|
|
||||||
Description: "Read-only access of all resources in an endpoint",
|
|
||||||
Priority: 2,
|
|
||||||
Authorizations: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(false),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
standardUserRole := &portainer.Role{
|
|
||||||
Name: "Standard user",
|
|
||||||
Description: "Full control of assigned resources in an endpoint",
|
|
||||||
Priority: 3,
|
|
||||||
Authorizations: authorization.DefaultEndpointAuthorizationsForStandardUserRole(false),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(standardUserRole)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
readOnlyUserRole := &portainer.Role{
|
|
||||||
Name: "Read-only user",
|
|
||||||
Description: "Read-only access of assigned resources in an endpoint",
|
|
||||||
Priority: 4,
|
|
||||||
Authorizations: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(false),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(readOnlyUserRole)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/git"
|
"github.com/portainer/portainer/api/git"
|
||||||
"github.com/portainer/portainer/api/http"
|
"github.com/portainer/portainer/api/http"
|
||||||
"github.com/portainer/portainer/api/http/client"
|
"github.com/portainer/portainer/api/http/client"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
"github.com/portainer/portainer/api/internal/snapshot"
|
"github.com/portainer/portainer/api/internal/snapshot"
|
||||||
"github.com/portainer/portainer/api/jwt"
|
"github.com/portainer/portainer/api/jwt"
|
||||||
"github.com/portainer/portainer/api/kubernetes"
|
"github.com/portainer/portainer/api/kubernetes"
|
||||||
|
@ -318,17 +317,6 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap
|
||||||
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
|
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initExtensionManager(fileService portainer.FileService, dataStore portainer.DataStore) (portainer.ExtensionManager, error) {
|
|
||||||
extensionManager := exec.NewExtensionManager(fileService, dataStore)
|
|
||||||
|
|
||||||
err := extensionManager.StartExtensions()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensionManager, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
|
func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
|
||||||
timer1 := time.NewTimer(5 * time.Minute)
|
timer1 := time.NewTimer(5 * time.Minute)
|
||||||
<-timer1.C
|
<-timer1.C
|
||||||
|
@ -372,11 +360,6 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionManager, err := initExtensionManager(fileService, dataStore)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reverseTunnelService := chisel.NewService(dataStore)
|
reverseTunnelService := chisel.NewService(dataStore)
|
||||||
|
|
||||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||||
|
@ -439,10 +422,9 @@ func main() {
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
log.Println("Created admin user with the given password.")
|
log.Println("Created admin user with the given password.")
|
||||||
user := &portainer.User{
|
user := &portainer.User{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Role: portainer.AdministratorRole,
|
Role: portainer.AdministratorRole,
|
||||||
Password: adminPasswordHash,
|
Password: adminPasswordHash,
|
||||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
|
||||||
}
|
}
|
||||||
err := dataStore.User().CreateUser(user)
|
err := dataStore.User().CreateUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -469,7 +451,6 @@ func main() {
|
||||||
SwarmStackManager: swarmStackManager,
|
SwarmStackManager: swarmStackManager,
|
||||||
ComposeStackManager: composeStackManager,
|
ComposeStackManager: composeStackManager,
|
||||||
KubernetesDeployer: kubernetesDeployer,
|
KubernetesDeployer: kubernetesDeployer,
|
||||||
ExtensionManager: extensionManager,
|
|
||||||
CryptoService: cryptoService,
|
CryptoService: cryptoService,
|
||||||
JWTService: jwtService,
|
JWTService: jwtService,
|
||||||
FileService: fileService,
|
FileService: fileService,
|
||||||
|
|
|
@ -1,307 +0,0 @@
|
||||||
package exec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-semver/semver"
|
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/http/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/"
|
|
||||||
var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`)
|
|
||||||
|
|
||||||
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
|
||||||
portainer.RBACExtension: "extension-rbac",
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtensionManager represents a service used to
|
|
||||||
// manage extension processes.
|
|
||||||
type ExtensionManager struct {
|
|
||||||
processes cmap.ConcurrentMap
|
|
||||||
fileService portainer.FileService
|
|
||||||
dataStore portainer.DataStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExtensionManager returns a pointer to an ExtensionManager
|
|
||||||
func NewExtensionManager(fileService portainer.FileService, dataStore portainer.DataStore) *ExtensionManager {
|
|
||||||
return &ExtensionManager{
|
|
||||||
processes: cmap.New(),
|
|
||||||
fileService: fileService,
|
|
||||||
dataStore: dataStore,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func processKey(ID portainer.ExtensionID) string {
|
|
||||||
return strconv.Itoa(int(ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildExtensionURL(extension *portainer.Extension) string {
|
|
||||||
return fmt.Sprintf("%s%s-%s-%s-%s.zip", extensionDownloadBaseURL, extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
|
|
||||||
extensionFilename := fmt.Sprintf("%s-%s-%s-%s", extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
extensionFilename += ".exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionPath := path.Join(
|
|
||||||
binaryPath,
|
|
||||||
extensionFilename)
|
|
||||||
|
|
||||||
return extensionPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchExtensionDefinitions will fetch the list of available
|
|
||||||
// extension definitions from the official Portainer assets server.
|
|
||||||
// If it cannot retrieve the data from the Internet it will fallback to the locally cached
|
|
||||||
// manifest file.
|
|
||||||
func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
|
|
||||||
var extensionData []byte
|
|
||||||
|
|
||||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 5)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extensions manifest via Internet. Extensions will be retrieved from local cache and might not be up to date] [err: %s]", err)
|
|
||||||
|
|
||||||
extensionData, err = manager.fileService.GetFileContent(portainer.LocalExtensionManifestFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var extensions []portainer.Extension
|
|
||||||
err = json.Unmarshal(extensionData, &extensions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallExtension will install the extension from an archive. It will extract the extension version number from
|
|
||||||
// the archive file name first and return an error if the file name is not valid (cannot find extension version).
|
|
||||||
// It will then extract the archive and execute the EnableExtension function to enable the extension.
|
|
||||||
// Since we're missing information about this extension (stored on Portainer.io server) we need to assume
|
|
||||||
// default information based on the extension ID.
|
|
||||||
func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error {
|
|
||||||
extensionVersion := extensionVersionRegexp.FindString(archiveFileName)
|
|
||||||
if extensionVersion == "" {
|
|
||||||
return errors.New("invalid extension archive filename: unable to retrieve extension version")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := manager.fileService.ExtractExtensionArchive(extensionArchive)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch extension.ID {
|
|
||||||
case portainer.RBACExtension:
|
|
||||||
extension.Name = "Role-Based Access Control"
|
|
||||||
}
|
|
||||||
extension.ShortDescription = "Extension enabled offline"
|
|
||||||
extension.Version = extensionVersion
|
|
||||||
extension.Available = true
|
|
||||||
|
|
||||||
return manager.EnableExtension(extension, licenseKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnableExtension will check for the existence of the extension binary on the filesystem
|
|
||||||
// first. If it does not exist, it will download it from the official Portainer assets server.
|
|
||||||
// After installing the binary on the filesystem, it will execute the binary in license check
|
|
||||||
// mode to validate the extension license. If the license is valid, it will then start
|
|
||||||
// the extension process and register it in the processes map.
|
|
||||||
func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension, licenseKey string) error {
|
|
||||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
|
||||||
extensionBinaryExists, err := manager.fileService.FileExists(extensionBinaryPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !extensionBinaryExists {
|
|
||||||
err := manager.downloadExtension(extension)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
licenseDetails, err := validateLicense(extensionBinaryPath, licenseKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
extension.License = portainer.LicenseInformation{
|
|
||||||
LicenseKey: licenseKey,
|
|
||||||
Company: licenseDetails[0],
|
|
||||||
Expiration: licenseDetails[1],
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
extension.Version = licenseDetails[2]
|
|
||||||
|
|
||||||
return manager.startExtensionProcess(extension, extensionBinaryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisableExtension will retrieve the process associated to the extension
|
|
||||||
// from the processes map and kill the process. It will then remove the process
|
|
||||||
// from the processes map and remove the binary associated to the extension
|
|
||||||
// from the filesystem
|
|
||||||
func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension) error {
|
|
||||||
process, ok := manager.processes.Get(processKey(extension.ID))
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := process.(*exec.Cmd).Process.Kill()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.processes.Remove(processKey(extension.ID))
|
|
||||||
|
|
||||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
|
||||||
return manager.fileService.RemoveDirectory(extensionBinaryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartExtensions will retrieve the extensions definitions from the Internet and check if a new version of each
|
|
||||||
// extension is available. If so, it will automatically install the new version of the extension. If no update is
|
|
||||||
// available it will simply start the extension.
|
|
||||||
// The purpose of this function is to be ran at startup, as such most of the error handling won't block the program execution
|
|
||||||
// and will log warning messages instead.
|
|
||||||
func (manager *ExtensionManager) StartExtensions() error {
|
|
||||||
extensions, err := manager.dataStore.Extension().Extensions()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
definitions, err := manager.FetchExtensionDefinitions()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extension information from Internet. Skipping extensions update check.] [err: %s]", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager.updateAndStartExtensions(extensions, definitions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *ExtensionManager) updateAndStartExtensions(extensions []portainer.Extension, definitions []portainer.Extension) error {
|
|
||||||
for _, definition := range definitions {
|
|
||||||
for _, extension := range extensions {
|
|
||||||
if extension.ID == definition.ID {
|
|
||||||
definitionVersion := semver.New(definition.Version)
|
|
||||||
extensionVersion := semver.New(extension.Version)
|
|
||||||
|
|
||||||
if extensionVersion.LessThan(*definitionVersion) {
|
|
||||||
log.Printf("[INFO] [exec,extensions] [message: new version detected, updating extension] [extension: %s] [current_version: %s] [available_version: %s]", extension.Name, extension.Version, definition.Version)
|
|
||||||
err := manager.UpdateExtension(&extension, definition.Version)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[WARN] [exec,extensions] [message: unable to update extension automatically] [extension: %s] [current_version: %s] [available_version: %s] [err: %s]", extension.Name, extension.Version, definition.Version, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := manager.EnableExtension(&extension, extension.License.LicenseKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[WARN] [exec,extensions] [message: unable to start extension] [extension: %s] [err: %s]", extension.Name, err)
|
|
||||||
extension.Enabled = false
|
|
||||||
extension.License.Valid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := manager.dataStore.Extension().Persist(&extension)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExtension will download the new extension binary from the official Portainer assets
|
|
||||||
// server, disable the previous extension via DisableExtension, trigger a license check
|
|
||||||
// and then start the extension process and add it to the processes map
|
|
||||||
func (manager *ExtensionManager) UpdateExtension(extension *portainer.Extension, version string) error {
|
|
||||||
oldVersion := extension.Version
|
|
||||||
|
|
||||||
extension.Version = version
|
|
||||||
err := manager.downloadExtension(extension)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
extension.Version = oldVersion
|
|
||||||
err = manager.DisableExtension(extension)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
extension.Version = version
|
|
||||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
|
||||||
|
|
||||||
licenseDetails, err := validateLicense(extensionBinaryPath, extension.License.LicenseKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
extension.Version = licenseDetails[2]
|
|
||||||
|
|
||||||
return manager.startExtensionProcess(extension, extensionBinaryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *ExtensionManager) downloadExtension(extension *portainer.Extension) error {
|
|
||||||
extensionURL := buildExtensionURL(extension)
|
|
||||||
|
|
||||||
data, err := client.Get(extensionURL, 30)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager.fileService.ExtractExtensionArchive(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
|
||||||
licenseCheckProcess := exec.Command(binaryPath, "-license", licenseKey, "-check")
|
|
||||||
cmdOutput := &bytes.Buffer{}
|
|
||||||
licenseCheckProcess.Stdout = cmdOutput
|
|
||||||
|
|
||||||
err := licenseCheckProcess.Run()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
|
|
||||||
return nil, errors.New("invalid extension license key")
|
|
||||||
}
|
|
||||||
|
|
||||||
output := string(cmdOutput.Bytes())
|
|
||||||
|
|
||||||
return strings.Split(output, "|"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
|
|
||||||
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
|
|
||||||
extensionProcess.Stdout = os.Stdout
|
|
||||||
extensionProcess.Stderr = os.Stderr
|
|
||||||
|
|
||||||
err := extensionProcess.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[DEBUG] [exec,extension] [message: unable to start extension process] [err: %s]", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
|
|
||||||
manager.processes.Set(processKey(extension.ID), extensionProcess)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/archive"
|
|
||||||
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -96,12 +95,6 @@ func (service *Service) GetBinaryFolder() string {
|
||||||
return path.Join(service.fileStorePath, BinaryStorePath)
|
return path.Join(service.fileStorePath, BinaryStorePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractExtensionArchive extracts the content of an extension archive
|
|
||||||
// specified as raw data into the binary store on the filesystem
|
|
||||||
func (service *Service) ExtractExtensionArchive(data []byte) error {
|
|
||||||
return archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveDirectory removes a directory on the filesystem.
|
// RemoveDirectory removes a directory on the filesystem.
|
||||||
func (service *Service) RemoveDirectory(directoryPath string) error {
|
func (service *Service) RemoveDirectory(directoryPath string) error {
|
||||||
return os.RemoveAll(directoryPath)
|
return os.RemoveAll(directoryPath)
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type authenticatePayload struct {
|
type authenticatePayload struct {
|
||||||
|
@ -79,11 +78,6 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler.writeToken(w, user)
|
return handler.writeToken(w, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,9 +97,8 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &portainer.User{
|
user := &portainer.User{
|
||||||
Username: username,
|
Username: username,
|
||||||
Role: portainer.StandardUserRole,
|
Role: portainer.StandardUserRole,
|
||||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.DataStore.User().CreateUser(user)
|
err = handler.DataStore.User().CreateUser(user)
|
||||||
|
@ -118,11 +111,6 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler.writeToken(w, user)
|
return handler.writeToken(w, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type oauthPayload struct {
|
type oauthPayload struct {
|
||||||
|
@ -76,9 +75,8 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
user = &portainer.User{
|
user = &portainer.User{
|
||||||
Username: username,
|
Username: username,
|
||||||
Role: portainer.StandardUserRole,
|
Role: portainer.StandardUserRole,
|
||||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.DataStore.User().CreateUser(user)
|
err = handler.DataStore.User().CreateUser(user)
|
||||||
|
@ -99,10 +97,6 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.writeToken(w, user)
|
return handler.writeToken(w, user)
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler is the HTTP handler used to handle authentication operations.
|
// Handler is the HTTP handler used to handle authentication operations.
|
||||||
|
@ -21,7 +20,6 @@ type Handler struct {
|
||||||
LDAPService portainer.LDAPService
|
LDAPService portainer.LDAPService
|
||||||
OAuthService portainer.OAuthService
|
OAuthService portainer.OAuthService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
AuthorizationService *authorization.Service
|
|
||||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,10 +39,8 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAuthorizations := false
|
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
|
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
|
||||||
updateAuthorizations = true
|
|
||||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -56,13 +54,6 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateAuthorizations {
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tagID := range endpointGroup.TagIDs {
|
for _, tagID := range endpointGroup.TagIDs {
|
||||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -92,15 +92,12 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAuthorizations := false
|
|
||||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) {
|
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) {
|
||||||
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
|
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
|
||||||
updateAuthorizations = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
|
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
|
||||||
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
|
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||||
updateAuthorizations = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||||
|
@ -108,13 +105,6 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateAuthorizations {
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tagsChanged {
|
if tagsChanged {
|
||||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -7,14 +7,12 @@ import (
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
DataStore portainer.DataStore
|
DataStore portainer.DataStore
|
||||||
AuthorizationService *authorization.Service
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage endpoint group operations.
|
// NewHandler creates a handler to manage endpoint group operations.
|
||||||
|
|
|
@ -24,7 +24,7 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -445,15 +445,6 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
group, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(group.UserAccessPolicies) > 0 || len(group.TeamAccessPolicies) > 0 {
|
|
||||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tagID := range endpoint.TagIDs {
|
for _, tagID := range endpoint.TagIDs {
|
||||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -40,13 +40,6 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||||
|
|
||||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||||
|
|
||||||
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
|
err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint relation from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint relation from the database", err}
|
||||||
|
|
|
@ -24,7 +24,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,15 +126,12 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
endpoint.Kubernetes = *payload.Kubernetes
|
endpoint.Kubernetes = *payload.Kubernetes
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAuthorizations := false
|
|
||||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
|
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
|
||||||
endpoint.UserAccessPolicies = payload.UserAccessPolicies
|
endpoint.UserAccessPolicies = payload.UserAccessPolicies
|
||||||
updateAuthorizations = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) {
|
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) {
|
||||||
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
|
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||||
updateAuthorizations = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Status != nil {
|
if payload.Status != nil {
|
||||||
|
@ -226,13 +223,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateAuthorizations {
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && (groupIDChanged || tagsChanged) {
|
if (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && (groupIDChanged || tagsChanged) {
|
||||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -24,7 +23,6 @@ type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
requestBouncer *security.RequestBouncer
|
requestBouncer *security.RequestBouncer
|
||||||
DataStore portainer.DataStore
|
DataStore portainer.DataStore
|
||||||
AuthorizationService *authorization.Service
|
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
package extensions
|
|
||||||
|
|
||||||
import (
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
|
||||||
tmp := policies[key]
|
|
||||||
tmp.RoleID = 4
|
|
||||||
policies[key] = tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateTeamAccessPolicyToReadOnlyRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
|
||||||
tmp := policies[key]
|
|
||||||
tmp.RoleID = 4
|
|
||||||
policies[key] = tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) upgradeRBACData() error {
|
|
||||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpointGroup := range endpointGroups {
|
|
||||||
for key := range endpointGroup.UserAccessPolicies {
|
|
||||||
updateUserAccessPolicyToReadOnlyRole(endpointGroup.UserAccessPolicies, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key := range endpointGroup.TeamAccessPolicies {
|
|
||||||
updateTeamAccessPolicyToReadOnlyRole(endpointGroup.TeamAccessPolicies, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
for key := range endpoint.UserAccessPolicies {
|
|
||||||
updateUserAccessPolicyToReadOnlyRole(endpoint.UserAccessPolicies, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key := range endpoint.TeamAccessPolicies {
|
|
||||||
updateTeamAccessPolicyToReadOnlyRole(endpoint.TeamAccessPolicies, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUserAccessPolicyToNoRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
|
||||||
tmp := policies[key]
|
|
||||||
tmp.RoleID = 0
|
|
||||||
policies[key] = tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateTeamAccessPolicyToNoRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
|
||||||
tmp := policies[key]
|
|
||||||
tmp.RoleID = 0
|
|
||||||
policies[key] = tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) downgradeRBACData() error {
|
|
||||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpointGroup := range endpointGroups {
|
|
||||||
for key := range endpointGroup.UserAccessPolicies {
|
|
||||||
updateUserAccessPolicyToNoRole(endpointGroup.UserAccessPolicies, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key := range endpointGroup.TeamAccessPolicies {
|
|
||||||
updateTeamAccessPolicyToNoRole(endpointGroup.TeamAccessPolicies, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
for key := range endpoint.UserAccessPolicies {
|
|
||||||
updateUserAccessPolicyToNoRole(endpoint.UserAccessPolicies, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key := range endpoint.TeamAccessPolicies {
|
|
||||||
updateTeamAccessPolicyToNoRole(endpoint.TeamAccessPolicies, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
package extensions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type extensionCreatePayload struct {
|
|
||||||
License string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (payload *extensionCreatePayload) Validate(r *http.Request) error {
|
|
||||||
if govalidator.IsNull(payload.License) {
|
|
||||||
return errors.New("Invalid license")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
var payload extensionCreatePayload
|
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionIdentifier, err := strconv.Atoi(string(payload.License[0]))
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid license format", err}
|
|
||||||
}
|
|
||||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
|
||||||
|
|
||||||
extensions, err := handler.DataStore.Extension().Extensions()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, existingExtension := range extensions {
|
|
||||||
if existingExtension.ID == extensionID && existingExtension.Enabled {
|
|
||||||
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", errors.New("This extension is already enabled")}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension := &portainer.Extension{
|
|
||||||
ID: extensionID,
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionDefinitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, def := range extensionDefinitions {
|
|
||||||
if def.ID == extension.ID {
|
|
||||||
extension.Version = def.Version
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.ExtensionManager.EnableExtension(extension, payload.License)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to enable extension", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension.Enabled = true
|
|
||||||
|
|
||||||
if extension.ID == portainer.RBACExtension {
|
|
||||||
err = handler.upgradeRBACData()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.DataStore.Extension().Persist(extension)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Empty(w)
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package extensions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/bolt/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DELETE request on /api/extensions/:id
|
|
||||||
func (handler *Handler) extensionDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
|
||||||
}
|
|
||||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
|
||||||
|
|
||||||
extension, err := handler.DataStore.Extension().Extension(extensionID)
|
|
||||||
if err == errors.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.ExtensionManager.DisableExtension(extension)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete extension", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if extensionID == portainer.RBACExtension {
|
|
||||||
err = handler.downgradeRBACData()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.DataStore.Extension().DeleteExtension(extensionID)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the extension from the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Empty(w)
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package extensions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/bolt/errors"
|
|
||||||
"github.com/portainer/portainer/api/http/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GET request on /api/extensions/:id
|
|
||||||
func (handler *Handler) extensionInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
|
||||||
|
|
||||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
localExtension, err := handler.DataStore.Extension().Extension(extensionID)
|
|
||||||
if err != nil && err != errors.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension information from the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension portainer.Extension
|
|
||||||
var extensionDefinition portainer.Extension
|
|
||||||
|
|
||||||
for _, definition := range definitions {
|
|
||||||
if definition.ID == extensionID {
|
|
||||||
extensionDefinition = definition
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if localExtension == nil {
|
|
||||||
extension = extensionDefinition
|
|
||||||
} else {
|
|
||||||
extension = *localExtension
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeExtensionAndDefinition(&extension, &extensionDefinition)
|
|
||||||
|
|
||||||
description, _ := client.Get(extension.DescriptionURL, 5)
|
|
||||||
extension.Description = string(description)
|
|
||||||
|
|
||||||
return response.JSON(w, extension)
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package extensions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GET request on /api/extensions?store=<store>
|
|
||||||
func (handler *Handler) extensionList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
fetchManifestInformation, _ := request.RetrieveBooleanQueryParameter(r, "store", true)
|
|
||||||
|
|
||||||
extensions, err := handler.DataStore.Extension().Extensions()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions from the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fetchManifestInformation {
|
|
||||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
extensions = mergeExtensionsAndDefinitions(extensions, definitions)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, extensions)
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
package extensions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type extensionUpdatePayload struct {
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (payload *extensionUpdatePayload) Validate(r *http.Request) error {
|
|
||||||
if govalidator.IsNull(payload.Version) {
|
|
||||||
return errors.New("Invalid extension version")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) extensionUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
|
||||||
}
|
|
||||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
|
||||||
|
|
||||||
var payload extensionUpdatePayload
|
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension, err := handler.DataStore.Extension().Extension(extensionID)
|
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.ExtensionManager.UpdateExtension(extension, payload.Version)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update extension", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.DataStore.Extension().Persist(extension)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Empty(w)
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
package extensions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type extensionUploadPayload struct {
|
|
||||||
License string
|
|
||||||
ExtensionArchive []byte
|
|
||||||
ArchiveFileName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (payload *extensionUploadPayload) Validate(r *http.Request) error {
|
|
||||||
license, err := request.RetrieveMultiPartFormValue(r, "License", false)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Invalid license")
|
|
||||||
}
|
|
||||||
payload.License = license
|
|
||||||
|
|
||||||
fileData, fileName, err := request.RetrieveMultiPartFormFile(r, "file")
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Invalid extension archive file. Ensure that the file is uploaded correctly")
|
|
||||||
}
|
|
||||||
payload.ExtensionArchive = fileData
|
|
||||||
payload.ArchiveFileName = fileName
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
payload := &extensionUploadPayload{}
|
|
||||||
err := payload.Validate(r)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionIdentifier, err := strconv.Atoi(string(payload.License[0]))
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid license format", err}
|
|
||||||
}
|
|
||||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
|
||||||
|
|
||||||
extension := &portainer.Extension{
|
|
||||||
ID: extensionID,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = handler.ExtensionManager.DisableExtension(extension)
|
|
||||||
|
|
||||||
err = handler.ExtensionManager.InstallExtension(extension, payload.License, payload.ArchiveFileName, payload.ExtensionArchive)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to install extension", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension.Enabled = true
|
|
||||||
|
|
||||||
if extension.ID == portainer.RBACExtension {
|
|
||||||
err = handler.upgradeRBACData()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.DataStore.Extension().Persist(extension)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Empty(w)
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
package extensions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/coreos/go-semver/semver"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler is the HTTP handler used to handle extension operations.
|
|
||||||
type Handler struct {
|
|
||||||
*mux.Router
|
|
||||||
DataStore portainer.DataStore
|
|
||||||
ExtensionManager portainer.ExtensionManager
|
|
||||||
AuthorizationService *authorization.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHandler creates a handler to manage extension operations.
|
|
||||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|
||||||
h := &Handler{
|
|
||||||
Router: mux.NewRouter(),
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Handle("/extensions",
|
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
|
||||||
h.Handle("/extensions",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
|
||||||
h.Handle("/extensions/upload",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionUpload))).Methods(http.MethodPost)
|
|
||||||
h.Handle("/extensions/{id}",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
|
||||||
h.Handle("/extensions/{id}",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete)
|
|
||||||
h.Handle("/extensions/{id}/update",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeExtensionsAndDefinitions(extensions, definitions []portainer.Extension) []portainer.Extension {
|
|
||||||
for _, definition := range definitions {
|
|
||||||
foundInDB := false
|
|
||||||
|
|
||||||
for idx, extension := range extensions {
|
|
||||||
if extension.ID == definition.ID {
|
|
||||||
foundInDB = true
|
|
||||||
mergeExtensionAndDefinition(&extensions[idx], &definition)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundInDB {
|
|
||||||
extensions = append(extensions, definition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeExtensionAndDefinition(extension, definition *portainer.Extension) {
|
|
||||||
extension.Name = definition.Name
|
|
||||||
extension.ShortDescription = definition.ShortDescription
|
|
||||||
extension.Deal = definition.Deal
|
|
||||||
extension.Available = definition.Available
|
|
||||||
extension.DescriptionURL = definition.DescriptionURL
|
|
||||||
extension.Images = definition.Images
|
|
||||||
extension.Logo = definition.Logo
|
|
||||||
extension.Price = definition.Price
|
|
||||||
extension.PriceDescription = definition.PriceDescription
|
|
||||||
extension.ShopURL = definition.ShopURL
|
|
||||||
|
|
||||||
definitionVersion := semver.New(definition.Version)
|
|
||||||
extensionVersion := semver.New(extension.Version)
|
|
||||||
if extensionVersion.LessThan(*definitionVersion) {
|
|
||||||
extension.UpdateAvailable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
extension.Version = definition.Version
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
||||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||||
"github.com/portainer/portainer/api/http/handler/extensions"
|
|
||||||
"github.com/portainer/portainer/api/http/handler/file"
|
"github.com/portainer/portainer/api/http/handler/file"
|
||||||
"github.com/portainer/portainer/api/http/handler/motd"
|
"github.com/portainer/portainer/api/http/handler/motd"
|
||||||
"github.com/portainer/portainer/api/http/handler/registries"
|
"github.com/portainer/portainer/api/http/handler/registries"
|
||||||
|
@ -50,7 +49,6 @@ type Handler struct {
|
||||||
EndpointProxyHandler *endpointproxy.Handler
|
EndpointProxyHandler *endpointproxy.Handler
|
||||||
FileHandler *file.Handler
|
FileHandler *file.Handler
|
||||||
MOTDHandler *motd.Handler
|
MOTDHandler *motd.Handler
|
||||||
ExtensionHandler *extensions.Handler
|
|
||||||
RegistryHandler *registries.Handler
|
RegistryHandler *registries.Handler
|
||||||
ResourceControlHandler *resourcecontrols.Handler
|
ResourceControlHandler *resourcecontrols.Handler
|
||||||
RoleHandler *roles.Handler
|
RoleHandler *roles.Handler
|
||||||
|
@ -104,8 +102,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
default:
|
default:
|
||||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/extensions"):
|
|
||||||
http.StripPrefix("/api", h.ExtensionHandler).ServeHTTP(w, r)
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
||||||
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||||
|
|
|
@ -3,8 +3,6 @@ package settings
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
|
@ -19,12 +17,11 @@ func hideFields(settings *portainer.Settings) {
|
||||||
// Handler is the HTTP handler used to handle settings operations.
|
// Handler is the HTTP handler used to handle settings operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
AuthorizationService *authorization.Service
|
DataStore portainer.DataStore
|
||||||
DataStore portainer.DataStore
|
FileService portainer.FileService
|
||||||
FileService portainer.FileService
|
JWTService portainer.JWTService
|
||||||
JWTService portainer.JWTService
|
LDAPService portainer.LDAPService
|
||||||
LDAPService portainer.LDAPService
|
SnapshotService portainer.SnapshotService
|
||||||
SnapshotService portainer.SnapshotService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage settings operations.
|
// NewHandler creates a handler to manage settings operations.
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,10 +115,8 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAuthorizations := false
|
|
||||||
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
||||||
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
||||||
updateAuthorizations = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.EnableHostManagementFeatures != nil {
|
if payload.EnableHostManagementFeatures != nil {
|
||||||
|
@ -179,37 +176,9 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateAuthorizations {
|
|
||||||
err := handler.updateVolumeBrowserSetting(settings)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update RBAC authorizations", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, settings)
|
return response.JSON(w, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateVolumeBrowserSetting(settings *portainer.Settings) error {
|
|
||||||
err := handler.AuthorizationService.UpdateVolumeBrowsingAuthorizations(settings.AllowVolumeBrowserForRegularUsers)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
extension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
|
||||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if extension != nil {
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error {
|
func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error {
|
||||||
settings.SnapshotInterval = snapshotInterval
|
settings.SnapshotInterval = snapshotInterval
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
)
|
)
|
||||||
|
@ -81,22 +80,8 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
|
||||||
|
|
||||||
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
|
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
|
||||||
isAdmin := user.Role == portainer.AdministratorRole
|
isAdmin := user.Role == portainer.AdministratorRole
|
||||||
if isAdmin {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rbacExtension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
return isAdmin, nil
|
||||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
|
||||||
return false, errors.New("Unable to verify if RBAC extension is loaded")
|
|
||||||
}
|
|
||||||
|
|
||||||
if rbacExtension == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, endpointResourceAccess := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
|
|
||||||
|
|
||||||
return endpointResourceAccess, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) {
|
func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) {
|
||||||
|
|
|
@ -76,7 +76,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
@ -114,30 +114,8 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
if !securityContext.IsAdmin {
|
||||||
if err != nil {
|
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
rbacExtension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
|
||||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify if RBAC extension is loaded", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointResourceAccess := false
|
|
||||||
_, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
|
|
||||||
if ok {
|
|
||||||
endpointResourceAccess = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if rbacExtension != nil {
|
|
||||||
if !securityContext.IsAdmin && !endpointResourceAccess {
|
|
||||||
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !securityContext.IsAdmin {
|
|
||||||
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stack, err := handler.DataStore.Stack().StackByName(stackName)
|
stack, err := handler.DataStore.Stack().StackByName(stackName)
|
||||||
|
@ -155,7 +133,7 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt/errors"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
)
|
)
|
||||||
|
@ -44,14 +43,6 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
stacks = authorization.DecorateStacks(stacks, resourceControls)
|
stacks = authorization.DecorateStacks(stacks, resourceControls)
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
if !securityContext.IsAdmin {
|
||||||
rbacExtensionEnabled := true
|
|
||||||
_, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
|
||||||
if err == errors.ErrObjectNotFound {
|
|
||||||
rbacExtensionEnabled = false
|
|
||||||
} else if err != nil && err != errors.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check if RBAC extension is enabled", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
|
||||||
|
@ -62,7 +53,7 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||||
}
|
}
|
||||||
|
|
||||||
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled)
|
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, stacks)
|
return response.JSON(w, stacks)
|
||||||
|
|
|
@ -53,7 +53,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -14,8 +13,7 @@ import (
|
||||||
// Handler is the HTTP handler used to handle team membership operations.
|
// Handler is the HTTP handler used to handle team membership operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
DataStore portainer.DataStore
|
DataStore portainer.DataStore
|
||||||
AuthorizationService *authorization.Service
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage team membership operations.
|
// NewHandler creates a handler to manage team membership operations.
|
||||||
|
|
|
@ -72,10 +72,5 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team memberships inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team memberships inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, membership)
|
return response.JSON(w, membership)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,10 +40,5 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the team membership from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the team membership from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package teams
|
package teams
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
@ -13,8 +12,7 @@ import (
|
||||||
// Handler is the HTTP handler used to handle team operations.
|
// Handler is the HTTP handler used to handle team operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
DataStore portainer.DataStore
|
DataStore portainer.DataStore
|
||||||
AuthorizationService *authorization.Service
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage team operations.
|
// NewHandler creates a handler to manage team operations.
|
||||||
|
|
|
@ -34,10 +34,5 @@ func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *http
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete associated team memberships from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete associated team memberships from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.AuthorizationService.RemoveTeamAccessPolicies(portainer.TeamID(teamID))
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clean-up team access policies", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type adminInitPayload struct {
|
type adminInitPayload struct {
|
||||||
|
@ -45,9 +44,8 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &portainer.User{
|
user := &portainer.User{
|
||||||
Username: payload.Username,
|
Username: payload.Username,
|
||||||
Role: portainer.AdministratorRole,
|
Role: portainer.AdministratorRole,
|
||||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -28,9 +27,8 @@ func hideFields(user *portainer.User) {
|
||||||
// Handler is the HTTP handler used to handle user operations.
|
// Handler is the HTTP handler used to handle user operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
DataStore portainer.DataStore
|
DataStore portainer.DataStore
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
AuthorizationService *authorization.Service
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage user operations.
|
// NewHandler creates a handler to manage user operations.
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type userCreatePayload struct {
|
type userCreatePayload struct {
|
||||||
|
@ -62,9 +61,8 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
||||||
}
|
}
|
||||||
|
|
||||||
user = &portainer.User{
|
user = &portainer.User{
|
||||||
Username: payload.Username,
|
Username: payload.Username,
|
||||||
Role: portainer.UserRole(payload.Role),
|
Role: portainer.UserRole(payload.Role),
|
||||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settings, err := handler.DataStore.Settings().Settings()
|
settings, err := handler.DataStore.Settings().Settings()
|
||||||
|
|
|
@ -81,10 +81,5 @@ func (handler *Handler) deleteUser(w http.ResponseWriter, user *portainer.User)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user memberships from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user memberships from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.AuthorizationService.RemoveUserAccessPolicies(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clean-up user access policies", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/docker"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
@ -402,16 +401,6 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
if tokenData.Role != portainer.AdministratorRole {
|
||||||
rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension)
|
|
||||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := transport.dataStore.User().User(tokenData.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if volumeBrowseRestrictionCheck {
|
if volumeBrowseRestrictionCheck {
|
||||||
settings, err := transport.dataStore.Settings().Settings()
|
settings, err := transport.dataStore.Settings().Settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -419,28 +408,10 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
||||||
}
|
}
|
||||||
|
|
||||||
if !settings.AllowVolumeBrowserForRegularUsers {
|
if !settings.AllowVolumeBrowserForRegularUsers {
|
||||||
if rbacExtension == nil {
|
return responseutils.WriteAccessDeniedResponse()
|
||||||
return responseutils.WriteAccessDeniedResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return access denied for all roles except endpoint-administrator
|
|
||||||
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList]
|
|
||||||
if !userCanBrowse {
|
|
||||||
return responseutils.WriteAccessDeniedResponse()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointResourceAccess := false
|
|
||||||
_, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess]
|
|
||||||
if ok {
|
|
||||||
endpointResourceAccess = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if rbacExtension != nil && endpointResourceAccess {
|
|
||||||
return transport.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
|
teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -713,25 +684,5 @@ func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool,
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenData.Role == portainer.AdministratorRole {
|
return tokenData.Role == portainer.AdministratorRole, nil
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := transport.dataStore.User().User(tokenData.ID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension)
|
|
||||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if rbacExtension == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, endpointResourceAccess := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess]
|
|
||||||
|
|
||||||
return endpointResourceAccess, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package factory
|
package factory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -16,12 +15,8 @@ import (
|
||||||
|
|
||||||
const azureAPIBaseURL = "https://management.azure.com"
|
const azureAPIBaseURL = "https://management.azure.com"
|
||||||
|
|
||||||
var extensionPorts = map[portainer.ExtensionID]string{
|
|
||||||
portainer.RBACExtension: "7003",
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// ProxyFactory is a factory to create reverse proxies to Docker endpoints and extensions
|
// ProxyFactory is a factory to create reverse proxies
|
||||||
ProxyFactory struct {
|
ProxyFactory struct {
|
||||||
dataStore portainer.DataStore
|
dataStore portainer.DataStore
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
|
@ -44,25 +39,6 @@ func NewProxyFactory(dataStore portainer.DataStore, signatureService portainer.D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildExtensionURL returns the URL to an extension server
|
|
||||||
func BuildExtensionURL(extensionID portainer.ExtensionID) string {
|
|
||||||
return fmt.Sprintf("http://%s:%s", portainer.ExtensionServer, extensionPorts[extensionID])
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExtensionProxy returns a new HTTP proxy to an extension server
|
|
||||||
func (factory *ProxyFactory) NewExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
|
||||||
address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID]
|
|
||||||
|
|
||||||
extensionURL, err := url.Parse(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionURL.Scheme = "http"
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(extensionURL)
|
|
||||||
return proxy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLegacyExtensionProxy returns a new HTTP proxy to a legacy extension server (Storidge)
|
// NewLegacyExtensionProxy returns a new HTTP proxy to a legacy extension server (Storidge)
|
||||||
func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (http.Handler, error) {
|
func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (http.Handler, error) {
|
||||||
extensionURL, err := url.Parse(extensionAPIURL)
|
extensionURL, err := url.Parse(extensionAPIURL)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
|
|
||||||
|
@ -21,7 +20,6 @@ type (
|
||||||
Manager struct {
|
Manager struct {
|
||||||
proxyFactory *factory.ProxyFactory
|
proxyFactory *factory.ProxyFactory
|
||||||
endpointProxies cmap.ConcurrentMap
|
endpointProxies cmap.ConcurrentMap
|
||||||
extensionProxies cmap.ConcurrentMap
|
|
||||||
legacyExtensionProxies cmap.ConcurrentMap
|
legacyExtensionProxies cmap.ConcurrentMap
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -30,7 +28,6 @@ type (
|
||||||
func NewManager(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *Manager {
|
func NewManager(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
endpointProxies: cmap.New(),
|
endpointProxies: cmap.New(),
|
||||||
extensionProxies: cmap.New(),
|
|
||||||
legacyExtensionProxies: cmap.New(),
|
legacyExtensionProxies: cmap.New(),
|
||||||
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
|
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
|
||||||
}
|
}
|
||||||
|
@ -63,38 +60,6 @@ func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) {
|
||||||
manager.endpointProxies.Remove(string(endpoint.ID))
|
manager.endpointProxies.Remove(string(endpoint.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and
|
|
||||||
// registers it in the extension map associated to the specified extension identifier
|
|
||||||
func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
|
||||||
proxy, err := manager.proxyFactory.NewExtensionProxy(extensionID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.extensionProxies.Set(strconv.Itoa(int(extensionID)), proxy)
|
|
||||||
return proxy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
|
||||||
func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) http.Handler {
|
|
||||||
proxy, ok := manager.extensionProxies.Get(strconv.Itoa(int(extensionID)))
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy.(http.Handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table
|
|
||||||
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
|
|
||||||
return factory.BuildExtensionURL(extensionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
|
|
||||||
func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID) {
|
|
||||||
manager.extensionProxies.Remove(strconv.Itoa(int(extensionID)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
||||||
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||||
proxy, err := manager.proxyFactory.NewLegacyExtensionProxy(extensionAPIURL)
|
proxy, err := manager.proxyFactory.NewLegacyExtensionProxy(extensionAPIURL)
|
||||||
|
|
|
@ -14,9 +14,8 @@ import (
|
||||||
type (
|
type (
|
||||||
// RequestBouncer represents an entity that manages API request accesses
|
// RequestBouncer represents an entity that manages API request accesses
|
||||||
RequestBouncer struct {
|
RequestBouncer struct {
|
||||||
dataStore portainer.DataStore
|
dataStore portainer.DataStore
|
||||||
jwtService portainer.JWTService
|
jwtService portainer.JWTService
|
||||||
rbacExtensionClient *rbacExtensionClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestrictedRequestContext is a data structure containing information
|
// RestrictedRequestContext is a data structure containing information
|
||||||
|
@ -30,11 +29,10 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRequestBouncer initializes a new RequestBouncer
|
// NewRequestBouncer initializes a new RequestBouncer
|
||||||
func NewRequestBouncer(dataStore portainer.DataStore, jwtService portainer.JWTService, rbacExtensionURL string) *RequestBouncer {
|
func NewRequestBouncer(dataStore portainer.DataStore, jwtService portainer.JWTService) *RequestBouncer {
|
||||||
return &RequestBouncer{
|
return &RequestBouncer{
|
||||||
dataStore: dataStore,
|
dataStore: dataStore,
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
rbacExtensionClient: newRBACExtensionClient(rbacExtensionURL),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +45,7 @@ func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
||||||
|
|
||||||
// AdminAccess defines a security check for API endpoints that require an authorization check.
|
// AdminAccess defines a security check for API endpoints that require an authorization check.
|
||||||
// Authentication is required to access these endpoints.
|
// Authentication is required to access these endpoints.
|
||||||
// If the RBAC extension is enabled, authorizations are required to use these endpoints.
|
// The administrator role is required to use these endpoints.
|
||||||
// If the RBAC extension is not enabled, the administrator role is required to use these endpoints.
|
|
||||||
// The request context will be enhanced with a RestrictedRequestContext object
|
// The request context will be enhanced with a RestrictedRequestContext object
|
||||||
// that might be used later to inside the API operation for extra authorization validation
|
// that might be used later to inside the API operation for extra authorization validation
|
||||||
// and resource filtering.
|
// and resource filtering.
|
||||||
|
@ -61,8 +58,6 @@ func (bouncer *RequestBouncer) AdminAccess(h http.Handler) http.Handler {
|
||||||
|
|
||||||
// RestrictedAccess defines a security check for restricted API endpoints.
|
// RestrictedAccess defines a security check for restricted API endpoints.
|
||||||
// Authentication is required to access these endpoints.
|
// Authentication is required to access these endpoints.
|
||||||
// If the RBAC extension is enabled, authorizations are required to use these endpoints.
|
|
||||||
// If the RBAC extension is not enabled, access is granted to any authenticated user.
|
|
||||||
// The request context will be enhanced with a RestrictedRequestContext object
|
// The request context will be enhanced with a RestrictedRequestContext object
|
||||||
// that might be used later to inside the API operation for extra authorization validation
|
// that might be used later to inside the API operation for extra authorization validation
|
||||||
// and resource filtering.
|
// and resource filtering.
|
||||||
|
@ -86,11 +81,9 @@ func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler
|
||||||
|
|
||||||
// AuthorizedEndpointOperation retrieves the JWT token from the request context and verifies
|
// AuthorizedEndpointOperation retrieves the JWT token from the request context and verifies
|
||||||
// that the user can access the specified endpoint.
|
// that the user can access the specified endpoint.
|
||||||
// If the RBAC extension is enabled and the authorizationCheck flag is set,
|
|
||||||
// it will also validate that the user can execute the specified operation.
|
|
||||||
// An error is returned when access to the endpoint is denied or if the user do not have the required
|
// An error is returned when access to the endpoint is denied or if the user do not have the required
|
||||||
// authorization to execute the operation.
|
// authorization to execute the operation.
|
||||||
func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint, authorizationCheck bool) error {
|
func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||||
tokenData, err := RetrieveTokenData(r)
|
tokenData, err := RetrieveTokenData(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -114,13 +107,6 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp
|
||||||
return httperrors.ErrEndpointAccessDenied
|
return httperrors.ErrEndpointAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
if authorizationCheck {
|
|
||||||
err = bouncer.checkEndpointOperationAuthorization(r, endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return ErrAuthorizationRequired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,38 +128,6 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Request, endpoint *portainer.Endpoint) error {
|
|
||||||
tokenData, err := RetrieveTokenData(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role == portainer.AdministratorRole {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension)
|
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := bouncer.dataStore.User().User(tokenData.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
|
||||||
Path: r.URL.String(),
|
|
||||||
Method: r.Method,
|
|
||||||
Authorizations: user.EndpointAuthorizations[endpoint.ID],
|
|
||||||
}
|
|
||||||
|
|
||||||
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
|
|
||||||
return bouncer.rbacExtensionClient.checkAuthorization(apiOperation)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegistryAccess retrieves the JWT token from the request context and verifies
|
// RegistryAccess retrieves the JWT token from the request context and verifies
|
||||||
// that the user can access the specified registry.
|
// that the user can access the specified registry.
|
||||||
// An error is returned when access is denied.
|
// An error is returned when access is denied.
|
||||||
|
@ -206,9 +160,8 @@ func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// mwCheckPortainerAuthorizations will verify that the user has the required authorization to access
|
// mwCheckPortainerAuthorizations will verify that the user has the required authorization to access
|
||||||
// a specific API endpoint. It will leverage the RBAC extension authorization validation if the extension
|
// a specific API endpoint.
|
||||||
// is enabled.
|
// If the administratorOnly flag is specified, this will prevent non-admin
|
||||||
// If the administratorOnly flag is specified and the RBAC extension is not enabled, this will prevent non-admin
|
|
||||||
// users from accessing the endpoint.
|
// users from accessing the endpoint.
|
||||||
func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, administratorOnly bool) http.Handler {
|
func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, administratorOnly bool) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -223,21 +176,12 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension)
|
if administratorOnly {
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized)
|
||||||
if administratorOnly {
|
|
||||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := bouncer.dataStore.User().User(tokenData.ID)
|
_, err = bouncer.dataStore.User().User(tokenData.ID)
|
||||||
if err != nil && err == bolterrors.ErrObjectNotFound {
|
if err != nil && err == bolterrors.ErrObjectNotFound {
|
||||||
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized)
|
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized)
|
||||||
return
|
return
|
||||||
|
@ -246,19 +190,6 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
|
||||||
Path: r.URL.String(),
|
|
||||||
Method: r.Method,
|
|
||||||
Authorizations: user.PortainerAuthorizations,
|
|
||||||
}
|
|
||||||
|
|
||||||
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
|
|
||||||
err = bouncer.rbacExtensionClient.checkAuthorization(apiOperation)
|
|
||||||
if err != nil {
|
|
||||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", ErrAuthorizationRequired)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
package security
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultHTTPTimeout = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
type rbacExtensionClient struct {
|
|
||||||
httpClient *http.Client
|
|
||||||
extensionURL string
|
|
||||||
licenseKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRBACExtensionClient(extensionURL string) *rbacExtensionClient {
|
|
||||||
return &rbacExtensionClient{
|
|
||||||
extensionURL: extensionURL,
|
|
||||||
httpClient: &http.Client{
|
|
||||||
Timeout: time.Second * time.Duration(defaultHTTPTimeout),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *rbacExtensionClient) setLicenseKey(licenseKey string) {
|
|
||||||
client.licenseKey = licenseKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *rbacExtensionClient) checkAuthorization(authRequest *portainer.APIOperationAuthorizationRequest) error {
|
|
||||||
encodedAuthRequest, err := json.Marshal(authRequest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", client.extensionURL+"/authorized_operation", nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("X-RBAC-AuthorizationRequest", string(encodedAuthRequest))
|
|
||||||
req.Header.Set("X-PortainerExtension-License", client.licenseKey)
|
|
||||||
|
|
||||||
resp, err := client.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
|
||||||
return ErrAuthorizationRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
||||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||||
"github.com/portainer/portainer/api/http/handler/extensions"
|
|
||||||
"github.com/portainer/portainer/api/http/handler/file"
|
"github.com/portainer/portainer/api/http/handler/file"
|
||||||
"github.com/portainer/portainer/api/http/handler/motd"
|
"github.com/portainer/portainer/api/http/handler/motd"
|
||||||
"github.com/portainer/portainer/api/http/handler/registries"
|
"github.com/portainer/portainer/api/http/handler/registries"
|
||||||
|
@ -41,7 +40,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
|
||||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,7 +49,6 @@ type Server struct {
|
||||||
AssetsPath string
|
AssetsPath string
|
||||||
Status *portainer.Status
|
Status *portainer.Status
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ExtensionManager portainer.ExtensionManager
|
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
@ -74,12 +71,10 @@ type Server struct {
|
||||||
|
|
||||||
// Start starts the HTTP server
|
// Start starts the HTTP server
|
||||||
func (server *Server) Start() error {
|
func (server *Server) Start() error {
|
||||||
authorizationService := authorization.NewService(server.DataStore)
|
|
||||||
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
|
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
|
||||||
proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager)
|
proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager)
|
||||||
|
|
||||||
rbacExtensionURL := proxyManager.GetExtensionURL(portainer.RBACExtension)
|
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
|
||||||
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService, rbacExtensionURL)
|
|
||||||
|
|
||||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||||
|
|
||||||
|
@ -89,7 +84,6 @@ func (server *Server) Start() error {
|
||||||
authHandler.JWTService = server.JWTService
|
authHandler.JWTService = server.JWTService
|
||||||
authHandler.LDAPService = server.LDAPService
|
authHandler.LDAPService = server.LDAPService
|
||||||
authHandler.ProxyManager = proxyManager
|
authHandler.ProxyManager = proxyManager
|
||||||
authHandler.AuthorizationService = authorizationService
|
|
||||||
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
||||||
authHandler.OAuthService = server.OAuthService
|
authHandler.OAuthService = server.OAuthService
|
||||||
|
|
||||||
|
@ -122,7 +116,6 @@ func (server *Server) Start() error {
|
||||||
|
|
||||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
||||||
endpointHandler.DataStore = server.DataStore
|
endpointHandler.DataStore = server.DataStore
|
||||||
endpointHandler.AuthorizationService = authorizationService
|
|
||||||
endpointHandler.FileService = server.FileService
|
endpointHandler.FileService = server.FileService
|
||||||
endpointHandler.ProxyManager = proxyManager
|
endpointHandler.ProxyManager = proxyManager
|
||||||
endpointHandler.SnapshotService = server.SnapshotService
|
endpointHandler.SnapshotService = server.SnapshotService
|
||||||
|
@ -136,7 +129,6 @@ func (server *Server) Start() error {
|
||||||
|
|
||||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||||
endpointGroupHandler.DataStore = server.DataStore
|
endpointGroupHandler.DataStore = server.DataStore
|
||||||
endpointGroupHandler.AuthorizationService = authorizationService
|
|
||||||
|
|
||||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||||
endpointProxyHandler.DataStore = server.DataStore
|
endpointProxyHandler.DataStore = server.DataStore
|
||||||
|
@ -147,11 +139,6 @@ func (server *Server) Start() error {
|
||||||
|
|
||||||
var motdHandler = motd.NewHandler(requestBouncer)
|
var motdHandler = motd.NewHandler(requestBouncer)
|
||||||
|
|
||||||
var extensionHandler = extensions.NewHandler(requestBouncer)
|
|
||||||
extensionHandler.DataStore = server.DataStore
|
|
||||||
extensionHandler.ExtensionManager = server.ExtensionManager
|
|
||||||
extensionHandler.AuthorizationService = authorizationService
|
|
||||||
|
|
||||||
var registryHandler = registries.NewHandler(requestBouncer)
|
var registryHandler = registries.NewHandler(requestBouncer)
|
||||||
registryHandler.DataStore = server.DataStore
|
registryHandler.DataStore = server.DataStore
|
||||||
registryHandler.FileService = server.FileService
|
registryHandler.FileService = server.FileService
|
||||||
|
@ -161,7 +148,6 @@ func (server *Server) Start() error {
|
||||||
resourceControlHandler.DataStore = server.DataStore
|
resourceControlHandler.DataStore = server.DataStore
|
||||||
|
|
||||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||||
settingsHandler.AuthorizationService = authorizationService
|
|
||||||
settingsHandler.DataStore = server.DataStore
|
settingsHandler.DataStore = server.DataStore
|
||||||
settingsHandler.FileService = server.FileService
|
settingsHandler.FileService = server.FileService
|
||||||
settingsHandler.JWTService = server.JWTService
|
settingsHandler.JWTService = server.JWTService
|
||||||
|
@ -181,11 +167,9 @@ func (server *Server) Start() error {
|
||||||
|
|
||||||
var teamHandler = teams.NewHandler(requestBouncer)
|
var teamHandler = teams.NewHandler(requestBouncer)
|
||||||
teamHandler.DataStore = server.DataStore
|
teamHandler.DataStore = server.DataStore
|
||||||
teamHandler.AuthorizationService = authorizationService
|
|
||||||
|
|
||||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||||
teamMembershipHandler.DataStore = server.DataStore
|
teamMembershipHandler.DataStore = server.DataStore
|
||||||
teamMembershipHandler.AuthorizationService = authorizationService
|
|
||||||
|
|
||||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||||
|
|
||||||
|
@ -202,7 +186,6 @@ func (server *Server) Start() error {
|
||||||
var userHandler = users.NewHandler(requestBouncer, rateLimiter)
|
var userHandler = users.NewHandler(requestBouncer, rateLimiter)
|
||||||
userHandler.DataStore = server.DataStore
|
userHandler.DataStore = server.DataStore
|
||||||
userHandler.CryptoService = server.CryptoService
|
userHandler.CryptoService = server.CryptoService
|
||||||
userHandler.AuthorizationService = authorizationService
|
|
||||||
|
|
||||||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||||
websocketHandler.DataStore = server.DataStore
|
websocketHandler.DataStore = server.DataStore
|
||||||
|
@ -229,7 +212,6 @@ func (server *Server) Start() error {
|
||||||
EndpointProxyHandler: endpointProxyHandler,
|
EndpointProxyHandler: endpointProxyHandler,
|
||||||
FileHandler: fileHandler,
|
FileHandler: fileHandler,
|
||||||
MOTDHandler: motdHandler,
|
MOTDHandler: motdHandler,
|
||||||
ExtensionHandler: extensionHandler,
|
|
||||||
RegistryHandler: registryHandler,
|
RegistryHandler: registryHandler,
|
||||||
ResourceControlHandler: resourceControlHandler,
|
ResourceControlHandler: resourceControlHandler,
|
||||||
SettingsHandler: settingsHandler,
|
SettingsHandler: settingsHandler,
|
||||||
|
|
|
@ -119,16 +119,10 @@ func DecorateCustomTemplates(templates []portainer.CustomTemplate, resourceContr
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterAuthorizedStacks returns a list of decorated stacks filtered through resource control access checks.
|
// FilterAuthorizedStacks returns a list of decorated stacks filtered through resource control access checks.
|
||||||
func FilterAuthorizedStacks(stacks []portainer.Stack, user *portainer.User, userTeamIDs []portainer.TeamID, rbacEnabled bool) []portainer.Stack {
|
func FilterAuthorizedStacks(stacks []portainer.Stack, user *portainer.User, userTeamIDs []portainer.TeamID) []portainer.Stack {
|
||||||
authorizedStacks := make([]portainer.Stack, 0)
|
authorizedStacks := make([]portainer.Stack, 0)
|
||||||
|
|
||||||
for _, stack := range stacks {
|
for _, stack := range stacks {
|
||||||
_, ok := user.EndpointAuthorizations[stack.EndpointID][portainer.EndpointResourcesAccess]
|
|
||||||
if rbacEnabled && ok {
|
|
||||||
authorizedStacks = append(authorizedStacks, stack)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if stack.ResourceControl != nil && UserCanAccessResource(user.ID, userTeamIDs, stack.ResourceControl) {
|
if stack.ResourceControl != nil && UserCanAccessResource(user.ID, userTeamIDs, stack.ResourceControl) {
|
||||||
authorizedStacks = append(authorizedStacks, stack)
|
authorizedStacks = append(authorizedStacks, stack)
|
||||||
}
|
}
|
||||||
|
|
|
@ -412,7 +412,6 @@ func DefaultPortainerAuthorizations() portainer.Authorizations {
|
||||||
portainer.OperationPortainerEndpointInspect: true,
|
portainer.OperationPortainerEndpointInspect: true,
|
||||||
portainer.OperationPortainerEndpointExtensionAdd: true,
|
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||||
portainer.OperationPortainerEndpointExtensionRemove: true,
|
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||||
portainer.OperationPortainerExtensionList: true,
|
|
||||||
portainer.OperationPortainerMOTD: true,
|
portainer.OperationPortainerMOTD: true,
|
||||||
portainer.OperationPortainerRegistryList: true,
|
portainer.OperationPortainerRegistryList: true,
|
||||||
portainer.OperationPortainerRegistryInspect: true,
|
portainer.OperationPortainerRegistryInspect: true,
|
||||||
|
@ -425,182 +424,6 @@ func DefaultPortainerAuthorizations() portainer.Authorizations {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateVolumeBrowsingAuthorizations will update all the volume browsing authorizations for each role (except endpoint administrator)
|
|
||||||
// based on the specified removeAuthorizations parameter. If removeAuthorizations is set to true, all
|
|
||||||
// the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations
|
|
||||||
// will be reset based for each role.
|
|
||||||
func (service Service) UpdateVolumeBrowsingAuthorizations(remove bool) error {
|
|
||||||
roles, err := service.dataStore.Role().Roles()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, role := range roles {
|
|
||||||
// all roles except endpoint administrator
|
|
||||||
if role.ID != portainer.RoleID(1) {
|
|
||||||
updateRoleVolumeBrowsingAuthorizations(&role, remove)
|
|
||||||
|
|
||||||
err := service.dataStore.Role().UpdateRole(role.ID, &role)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateRoleVolumeBrowsingAuthorizations(role *portainer.Role, removeAuthorizations bool) {
|
|
||||||
if !removeAuthorizations {
|
|
||||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowseDelete)
|
|
||||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowseGet)
|
|
||||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowseList)
|
|
||||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowsePut)
|
|
||||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowseRename)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
role.Authorizations[portainer.OperationDockerAgentBrowseGet] = true
|
|
||||||
role.Authorizations[portainer.OperationDockerAgentBrowseList] = true
|
|
||||||
|
|
||||||
// Standard-user
|
|
||||||
if role.ID == portainer.RoleID(3) {
|
|
||||||
role.Authorizations[portainer.OperationDockerAgentBrowseDelete] = true
|
|
||||||
role.Authorizations[portainer.OperationDockerAgentBrowsePut] = true
|
|
||||||
role.Authorizations[portainer.OperationDockerAgentBrowseRename] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team
|
|
||||||
func (service *Service) RemoveTeamAccessPolicies(teamID portainer.TeamID) error {
|
|
||||||
endpoints, err := service.dataStore.Endpoint().Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
for policyTeamID := range endpoint.TeamAccessPolicies {
|
|
||||||
if policyTeamID == teamID {
|
|
||||||
delete(endpoint.TeamAccessPolicies, policyTeamID)
|
|
||||||
|
|
||||||
err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpointGroup := range endpointGroups {
|
|
||||||
for policyTeamID := range endpointGroup.TeamAccessPolicies {
|
|
||||||
if policyTeamID == teamID {
|
|
||||||
delete(endpointGroup.TeamAccessPolicies, policyTeamID)
|
|
||||||
|
|
||||||
err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registries, err := service.dataStore.Registry().Registries()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, registry := range registries {
|
|
||||||
for policyTeamID := range registry.TeamAccessPolicies {
|
|
||||||
if policyTeamID == teamID {
|
|
||||||
delete(registry.TeamAccessPolicies, policyTeamID)
|
|
||||||
|
|
||||||
err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return service.UpdateUsersAuthorizations()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
|
|
||||||
func (service *Service) RemoveUserAccessPolicies(userID portainer.UserID) error {
|
|
||||||
endpoints, err := service.dataStore.Endpoint().Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
for policyUserID := range endpoint.UserAccessPolicies {
|
|
||||||
if policyUserID == userID {
|
|
||||||
delete(endpoint.UserAccessPolicies, policyUserID)
|
|
||||||
|
|
||||||
err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpointGroup := range endpointGroups {
|
|
||||||
for policyUserID := range endpointGroup.UserAccessPolicies {
|
|
||||||
if policyUserID == userID {
|
|
||||||
delete(endpointGroup.UserAccessPolicies, policyUserID)
|
|
||||||
|
|
||||||
err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registries, err := service.dataStore.Registry().Registries()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, registry := range registries {
|
|
||||||
for policyUserID := range registry.UserAccessPolicies {
|
|
||||||
if policyUserID == userID {
|
|
||||||
delete(registry.UserAccessPolicies, policyUserID)
|
|
||||||
|
|
||||||
err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users.
|
// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users.
|
||||||
func (service *Service) UpdateUsersAuthorizations() error {
|
func (service *Service) UpdateUsersAuthorizations() error {
|
||||||
users, err := service.dataStore.User().Users()
|
users, err := service.dataStore.User().Users()
|
||||||
|
|
|
@ -14,13 +14,6 @@ type (
|
||||||
// AgentPlatform represents a platform type for an Agent
|
// AgentPlatform represents a platform type for an Agent
|
||||||
AgentPlatform int
|
AgentPlatform int
|
||||||
|
|
||||||
// APIOperationAuthorizationRequest represent an request for the authorization to execute an API operation
|
|
||||||
APIOperationAuthorizationRequest struct {
|
|
||||||
Path string
|
|
||||||
Method string
|
|
||||||
Authorizations Authorizations
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticationMethod represents the authentication method used to authenticate a user
|
// AuthenticationMethod represents the authentication method used to authenticate a user
|
||||||
AuthenticationMethod int
|
AuthenticationMethod int
|
||||||
|
|
||||||
|
@ -287,7 +280,7 @@ type (
|
||||||
EdgeStacks map[EdgeStackID]bool
|
EdgeStacks map[EdgeStackID]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension represents a Portainer extension
|
// Extension represents a deprecated Portainer extension
|
||||||
Extension struct {
|
Extension struct {
|
||||||
ID ExtensionID `json:"Id"`
|
ID ExtensionID `json:"Id"`
|
||||||
Enabled bool `json:"Enabled"`
|
Enabled bool `json:"Enabled"`
|
||||||
|
@ -724,10 +717,13 @@ type (
|
||||||
|
|
||||||
// User represents a user account
|
// User represents a user account
|
||||||
User struct {
|
User struct {
|
||||||
ID UserID `json:"Id"`
|
ID UserID `json:"Id"`
|
||||||
Username string `json:"Username"`
|
Username string `json:"Username"`
|
||||||
Password string `json:"Password,omitempty"`
|
Password string `json:"Password,omitempty"`
|
||||||
Role UserRole `json:"Role"`
|
Role UserRole `json:"Role"`
|
||||||
|
|
||||||
|
// Deprecated fields
|
||||||
|
// Deprecated in DBVersion == 25
|
||||||
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
|
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
|
||||||
EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"`
|
EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"`
|
||||||
}
|
}
|
||||||
|
@ -807,7 +803,6 @@ type (
|
||||||
Endpoint() EndpointService
|
Endpoint() EndpointService
|
||||||
EndpointGroup() EndpointGroupService
|
EndpointGroup() EndpointGroupService
|
||||||
EndpointRelation() EndpointRelationService
|
EndpointRelation() EndpointRelationService
|
||||||
Extension() ExtensionService
|
|
||||||
Registry() RegistryService
|
Registry() RegistryService
|
||||||
ResourceControl() ResourceControlService
|
ResourceControl() ResourceControlService
|
||||||
Role() RoleService
|
Role() RoleService
|
||||||
|
@ -899,24 +894,6 @@ type (
|
||||||
DeleteEndpointRelation(EndpointID EndpointID) error
|
DeleteEndpointRelation(EndpointID EndpointID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionManager represents a service used to manage extensions
|
|
||||||
ExtensionManager interface {
|
|
||||||
FetchExtensionDefinitions() ([]Extension, error)
|
|
||||||
InstallExtension(extension *Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error
|
|
||||||
EnableExtension(extension *Extension, licenseKey string) error
|
|
||||||
DisableExtension(extension *Extension) error
|
|
||||||
UpdateExtension(extension *Extension, version string) error
|
|
||||||
StartExtensions() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtensionService represents a service for managing extension data
|
|
||||||
ExtensionService interface {
|
|
||||||
Extension(ID ExtensionID) (*Extension, error)
|
|
||||||
Extensions() ([]Extension, error)
|
|
||||||
Persist(extension *Extension) error
|
|
||||||
DeleteExtension(ID ExtensionID) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileService represents a service for managing files
|
// FileService represents a service for managing files
|
||||||
FileService interface {
|
FileService interface {
|
||||||
GetFileContent(filePath string) ([]byte, error)
|
GetFileContent(filePath string) ([]byte, error)
|
||||||
|
@ -941,7 +918,6 @@ type (
|
||||||
ClearEdgeJobTaskLogs(edgeJobID, taskID string) error
|
ClearEdgeJobTaskLogs(edgeJobID, taskID string) error
|
||||||
GetEdgeJobTaskLogFileContent(edgeJobID, taskID string) (string, error)
|
GetEdgeJobTaskLogFileContent(edgeJobID, taskID string) (string, error)
|
||||||
StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error
|
StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error
|
||||||
ExtractExtensionArchive(data []byte) error
|
|
||||||
GetBinaryFolder() string
|
GetBinaryFolder() string
|
||||||
StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error)
|
StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error)
|
||||||
GetCustomTemplateProjectPath(identifier string) string
|
GetCustomTemplateProjectPath(identifier string) string
|
||||||
|
@ -1143,8 +1119,6 @@ const (
|
||||||
MessageOfTheDayURL = AssetsServerURL + "/motd.json"
|
MessageOfTheDayURL = AssetsServerURL + "/motd.json"
|
||||||
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
||||||
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
||||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
|
||||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-" + APIVersion + ".json"
|
|
||||||
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
||||||
SupportProductsURL = AssetsServerURL + "/support.json"
|
SupportProductsURL = AssetsServerURL + "/support.json"
|
||||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||||
|
@ -1164,12 +1138,8 @@ const (
|
||||||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||||
// to be used when communicating with an agent
|
// to be used when communicating with an agent
|
||||||
PortainerAgentSignatureMessage = "Portainer-App"
|
PortainerAgentSignatureMessage = "Portainer-App"
|
||||||
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
|
||||||
ExtensionServer = "127.0.0.1"
|
|
||||||
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
||||||
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
||||||
// LocalExtensionManifestFile represents the name of the local manifest file for extensions
|
|
||||||
LocalExtensionManifestFile = "/app/extensions.json"
|
|
||||||
// DefaultTemplatesURL represents the URL to the official templates supported by Portainer
|
// DefaultTemplatesURL represents the URL to the official templates supported by Portainer
|
||||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json"
|
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json"
|
||||||
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
|
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
|
||||||
|
@ -1254,16 +1224,6 @@ const (
|
||||||
EdgeAgentOnKubernetesEnvironment
|
EdgeAgentOnKubernetesEnvironment
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
_ ExtensionID = iota
|
|
||||||
// RegistryManagementExtension represents the registry management extension (removed)
|
|
||||||
RegistryManagementExtension
|
|
||||||
// OAuthAuthenticationExtension represents the OAuth authentication extension (Deprecated)
|
|
||||||
OAuthAuthenticationExtension
|
|
||||||
// RBACExtension represents the RBAC extension
|
|
||||||
RBACExtension
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
_ JobType = iota
|
_ JobType = iota
|
||||||
// SnapshotJobType is a system job used to create endpoint snapshots
|
// SnapshotJobType is a system job used to create endpoint snapshots
|
||||||
|
|
190
api/swagger.yaml
190
api/swagger.yaml
|
@ -834,196 +834,6 @@ paths:
|
||||||
description: 'Server error'
|
description: 'Server error'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/GenericError'
|
$ref: '#/definitions/GenericError'
|
||||||
/extensions:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- 'extensions'
|
|
||||||
summary: 'List extensions'
|
|
||||||
description: |
|
|
||||||
List all extensions registered inside Portainer. If the store parameter is set to true,
|
|
||||||
will retrieve extensions details from the online repository.
|
|
||||||
**Access policy**: administrator
|
|
||||||
operationId: 'ExtensionList'
|
|
||||||
produces:
|
|
||||||
- 'application/json'
|
|
||||||
security:
|
|
||||||
- jwt: []
|
|
||||||
parameters:
|
|
||||||
- name: 'store'
|
|
||||||
in: 'query'
|
|
||||||
description: 'Retrieve online information about extensions. Possible values: true or false.'
|
|
||||||
required: false
|
|
||||||
type: 'boolean'
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: 'Success'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ExtensionListResponse'
|
|
||||||
500:
|
|
||||||
description: 'Server error'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- 'extensions'
|
|
||||||
summary: 'Enable an extension'
|
|
||||||
description: |
|
|
||||||
Enable an extension.
|
|
||||||
**Access policy**: administrator
|
|
||||||
operationId: 'ExtensionCreate'
|
|
||||||
consumes:
|
|
||||||
- 'application/json'
|
|
||||||
produces:
|
|
||||||
- 'application/json'
|
|
||||||
security:
|
|
||||||
- jwt: []
|
|
||||||
parameters:
|
|
||||||
- in: 'body'
|
|
||||||
name: 'body'
|
|
||||||
description: 'Extension details'
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ExtensionCreateRequest'
|
|
||||||
responses:
|
|
||||||
204:
|
|
||||||
description: 'Success'
|
|
||||||
400:
|
|
||||||
description: 'Invalid request'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: 'Invalid request data format'
|
|
||||||
500:
|
|
||||||
description: 'Server error'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
/extensions/{id}:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- 'extensions'
|
|
||||||
summary: 'Inspect an extension'
|
|
||||||
description: |
|
|
||||||
Retrieve details abount an extension.
|
|
||||||
**Access policy**: administrator
|
|
||||||
operationId: 'ExtensionInspect'
|
|
||||||
produces:
|
|
||||||
- 'application/json'
|
|
||||||
security:
|
|
||||||
- jwt: []
|
|
||||||
parameters:
|
|
||||||
- name: 'id'
|
|
||||||
in: 'path'
|
|
||||||
description: 'extension identifier'
|
|
||||||
required: true
|
|
||||||
type: 'integer'
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: 'Success'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/Extension'
|
|
||||||
400:
|
|
||||||
description: 'Invalid request'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: 'Invalid request'
|
|
||||||
404:
|
|
||||||
description: 'Extension not found'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: 'Extension not found'
|
|
||||||
500:
|
|
||||||
description: 'Server error'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
put:
|
|
||||||
tags:
|
|
||||||
- 'extensions'
|
|
||||||
summary: 'Update an extension'
|
|
||||||
description: |
|
|
||||||
Update an extension to a specific version of the extension.
|
|
||||||
**Access policy**: administrator
|
|
||||||
operationId: 'ExtensionUpdate'
|
|
||||||
consumes:
|
|
||||||
- 'application/json'
|
|
||||||
produces:
|
|
||||||
- 'application/json'
|
|
||||||
security:
|
|
||||||
- jwt: []
|
|
||||||
parameters:
|
|
||||||
- name: 'id'
|
|
||||||
in: 'path'
|
|
||||||
description: 'Extension identifier'
|
|
||||||
required: true
|
|
||||||
type: 'integer'
|
|
||||||
- in: 'body'
|
|
||||||
name: 'body'
|
|
||||||
description: 'Extension details'
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ExtensionUpdateRequest'
|
|
||||||
responses:
|
|
||||||
204:
|
|
||||||
description: 'Success'
|
|
||||||
400:
|
|
||||||
description: 'Invalid request'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: 'Invalid request data format'
|
|
||||||
404:
|
|
||||||
description: 'Extension not found'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: 'Extension not found'
|
|
||||||
500:
|
|
||||||
description: 'Server error'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
delete:
|
|
||||||
tags:
|
|
||||||
- 'extensions'
|
|
||||||
summary: 'Disable an extension'
|
|
||||||
description: |
|
|
||||||
Disable an extension.
|
|
||||||
**Access policy**: administrator
|
|
||||||
operationId: 'ExtensionDelete'
|
|
||||||
security:
|
|
||||||
- jwt: []
|
|
||||||
parameters:
|
|
||||||
- name: 'id'
|
|
||||||
in: 'path'
|
|
||||||
description: 'Extension identifier'
|
|
||||||
required: true
|
|
||||||
type: 'integer'
|
|
||||||
responses:
|
|
||||||
204:
|
|
||||||
description: 'Success'
|
|
||||||
400:
|
|
||||||
description: 'Invalid request'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: 'Invalid request'
|
|
||||||
404:
|
|
||||||
description: 'Extension not found'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: 'Extension not found'
|
|
||||||
500:
|
|
||||||
description: 'Server error'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/GenericError'
|
|
||||||
/registries:
|
/registries:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -35,7 +35,6 @@ angular.module('portainer', [
|
||||||
'portainer.docker',
|
'portainer.docker',
|
||||||
'portainer.kubernetes',
|
'portainer.kubernetes',
|
||||||
'portainer.edge',
|
'portainer.edge',
|
||||||
'portainer.extensions',
|
|
||||||
'portainer.integrations',
|
'portainer.integrations',
|
||||||
'rzModule',
|
'rzModule',
|
||||||
'moment-picker',
|
'moment-picker',
|
||||||
|
|
|
@ -10,7 +10,6 @@ angular
|
||||||
.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints')
|
.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints')
|
||||||
.constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups')
|
.constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups')
|
||||||
.constant('API_ENDPOINT_MOTD', 'api/motd')
|
.constant('API_ENDPOINT_MOTD', 'api/motd')
|
||||||
.constant('API_ENDPOINT_EXTENSIONS', 'api/extensions')
|
|
||||||
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
|
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
|
||||||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
|
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
|
||||||
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
|
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
|
||||||
|
|
|
@ -30,7 +30,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
'SettingsService',
|
'SettingsService',
|
||||||
'PluginService',
|
'PluginService',
|
||||||
'HttpRequestHelper',
|
'HttpRequestHelper',
|
||||||
'ExtensionService',
|
|
||||||
function (
|
function (
|
||||||
$q,
|
$q,
|
||||||
$scope,
|
$scope,
|
||||||
|
@ -56,8 +55,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
SystemService,
|
SystemService,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
PluginService,
|
PluginService,
|
||||||
HttpRequestHelper,
|
HttpRequestHelper
|
||||||
ExtensionService
|
|
||||||
) {
|
) {
|
||||||
$scope.create = create;
|
$scope.create = create;
|
||||||
|
|
||||||
|
@ -649,7 +647,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$scope.isAdmin = Authentication.isAdmin();
|
$scope.isAdmin = Authentication.isAdmin();
|
||||||
$scope.showDeviceMapping = await shouldShowDevices();
|
$scope.showDeviceMapping = await shouldShowDevices();
|
||||||
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
|
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
|
||||||
$scope.isAdminOrEndpointAdmin = await checkIfAdminOrEndpointAdmin();
|
$scope.isAdminOrEndpointAdmin = Authentication.isAdmin();
|
||||||
|
|
||||||
Volume.query(
|
Volume.query(
|
||||||
{},
|
{},
|
||||||
|
@ -935,35 +933,16 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isAdminOrEndpointAdmin() {
|
|
||||||
const isAdmin = Authentication.isAdmin();
|
|
||||||
if (isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
return rbacEnabled ? Authentication.hasAuthorizations(['EndpointResourcesAccess']) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function shouldShowDevices() {
|
async function shouldShowDevices() {
|
||||||
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
|
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
|
||||||
|
|
||||||
return allowDeviceMappingForRegularUsers || isAdminOrEndpointAdmin();
|
return allowDeviceMappingForRegularUsers || Authentication.isAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkIfContainerCapabilitiesEnabled() {
|
async function checkIfContainerCapabilitiesEnabled() {
|
||||||
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
|
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
|
||||||
|
|
||||||
return allowContainerCapabilitiesForRegularUsers || isAdminOrEndpointAdmin();
|
return allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
|
||||||
}
|
|
||||||
|
|
||||||
async function checkIfAdminOrEndpointAdmin() {
|
|
||||||
if (Authentication.isAdmin()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
return rbacEnabled ? Authentication.hasAuthorizations(['EndpointResourcesAccess']) : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
|
@ -9,7 +9,6 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||||
'$transition$',
|
'$transition$',
|
||||||
'$filter',
|
'$filter',
|
||||||
'$async',
|
'$async',
|
||||||
'ExtensionService',
|
|
||||||
'Commit',
|
'Commit',
|
||||||
'ContainerHelper',
|
'ContainerHelper',
|
||||||
'ContainerService',
|
'ContainerService',
|
||||||
|
@ -30,7 +29,6 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||||
$transition$,
|
$transition$,
|
||||||
$filter,
|
$filter,
|
||||||
$async,
|
$async,
|
||||||
ExtensionService,
|
|
||||||
Commit,
|
Commit,
|
||||||
ContainerHelper,
|
ContainerHelper,
|
||||||
ContainerService,
|
ContainerService,
|
||||||
|
@ -115,9 +113,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||||
!allowHostNamespaceForRegularUsers ||
|
!allowHostNamespaceForRegularUsers ||
|
||||||
!allowPrivilegedModeForRegularUsers;
|
!allowPrivilegedModeForRegularUsers;
|
||||||
|
|
||||||
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then((rbacEnabled) => {
|
$scope.displayRecreateButton = !inSwarm && !autoRemove && (admin || !settingRestrictsRegularUsers);
|
||||||
$scope.displayRecreateButton = !inSwarm && !autoRemove && (settingRestrictsRegularUsers || rbacEnabled ? admin : true);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve container info');
|
Notifications.error('Failure', err, 'Unable to retrieve container info');
|
||||||
|
|
|
@ -12,7 +12,6 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||||
'EndpointService',
|
'EndpointService',
|
||||||
'Notifications',
|
'Notifications',
|
||||||
'EndpointProvider',
|
'EndpointProvider',
|
||||||
'ExtensionService',
|
|
||||||
'StateManager',
|
'StateManager',
|
||||||
function (
|
function (
|
||||||
$scope,
|
$scope,
|
||||||
|
@ -28,7 +27,6 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||||
EndpointService,
|
EndpointService,
|
||||||
Notifications,
|
Notifications,
|
||||||
EndpointProvider,
|
EndpointProvider,
|
||||||
ExtensionService,
|
|
||||||
StateManager
|
StateManager
|
||||||
) {
|
) {
|
||||||
$scope.dismissInformationPanel = function (id) {
|
$scope.dismissInformationPanel = function (id) {
|
||||||
|
@ -75,13 +73,7 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||||
const isAdmin = Authentication.isAdmin();
|
const isAdmin = Authentication.isAdmin();
|
||||||
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
|
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
|
||||||
|
|
||||||
if (isAdmin || allowStackManagementForRegularUsers) {
|
return isAdmin || allowStackManagementForRegularUsers;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
if (rbacEnabled) {
|
|
||||||
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
|
@ -33,7 +33,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||||
'SettingsService',
|
'SettingsService',
|
||||||
'WebhookService',
|
'WebhookService',
|
||||||
'EndpointProvider',
|
'EndpointProvider',
|
||||||
'ExtensionService',
|
|
||||||
function (
|
function (
|
||||||
$q,
|
$q,
|
||||||
$scope,
|
$scope,
|
||||||
|
@ -59,8 +58,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||||
NodeService,
|
NodeService,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
WebhookService,
|
WebhookService,
|
||||||
EndpointProvider,
|
EndpointProvider
|
||||||
ExtensionService
|
|
||||||
) {
|
) {
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
|
@ -592,15 +590,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||||
const settings = await SettingsService.publicSettings();
|
const settings = await SettingsService.publicSettings();
|
||||||
const { AllowBindMountsForRegularUsers } = settings;
|
const { AllowBindMountsForRegularUsers } = settings;
|
||||||
|
|
||||||
if (isAdmin || AllowBindMountsForRegularUsers) {
|
return isAdmin || AllowBindMountsForRegularUsers;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
if (rbacEnabled) {
|
|
||||||
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -9,8 +9,7 @@ angular.module('portainer.docker').controller('VolumesController', [
|
||||||
'HttpRequestHelper',
|
'HttpRequestHelper',
|
||||||
'EndpointProvider',
|
'EndpointProvider',
|
||||||
'Authentication',
|
'Authentication',
|
||||||
'ExtensionService',
|
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication) {
|
||||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ExtensionService) {
|
|
||||||
$scope.removeAction = function (selectedItems) {
|
$scope.removeAction = function (selectedItems) {
|
||||||
var actionCount = selectedItems.length;
|
var actionCount = selectedItems.length;
|
||||||
angular.forEach(selectedItems, function (volume) {
|
angular.forEach(selectedItems, function (volume) {
|
||||||
|
@ -71,16 +70,8 @@ angular.module('portainer.docker').controller('VolumesController', [
|
||||||
function initView() {
|
function initView() {
|
||||||
getVolumes();
|
getVolumes();
|
||||||
|
|
||||||
$scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy;
|
$scope.showBrowseAction =
|
||||||
|
$scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || $scope.applicationState.application.enableVolumeBrowserForNonAdminUsers);
|
||||||
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then(function success(extensionEnabled) {
|
|
||||||
if (!extensionEnabled) {
|
|
||||||
var isAdmin = Authentication.isAdmin();
|
|
||||||
if (!$scope.applicationState.application.enableVolumeBrowserForNonAdminUsers && !isAdmin) {
|
|
||||||
$scope.showBrowseAction = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
angular.module('portainer.extensions', ['portainer.extensions.rbac']);
|
|
|
@ -1,23 +0,0 @@
|
||||||
angular
|
|
||||||
.module('portainer.extensions.rbac', ['ngResource'])
|
|
||||||
.constant('API_ENDPOINT_ROLES', 'api/roles')
|
|
||||||
.config([
|
|
||||||
'$stateRegistryProvider',
|
|
||||||
function ($stateRegistryProvider) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var roles = {
|
|
||||||
name: 'portainer.roles',
|
|
||||||
url: '/roles',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/roles/roles.html',
|
|
||||||
controller: 'RolesController',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
$stateRegistryProvider.register(roles);
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,38 +0,0 @@
|
||||||
<div class="col-sm-12" style="margin-bottom: 0px;">
|
|
||||||
<rd-widget ng-if="ctrl.users">
|
|
||||||
<rd-widget-header icon="fa-user-lock" title-text="Effective access viewer"></rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<form class="form-horizontal">
|
|
||||||
<div class="col-sm-12 form-section-title">
|
|
||||||
User
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<span class="small text-muted" ng-if="ctrl.users.length === 0">
|
|
||||||
No user available
|
|
||||||
</span>
|
|
||||||
<ui-select ng-if="ctrl.users.length > 0" ng-model="ctrl.selectedUser" ng-change="ctrl.onUserSelect()">
|
|
||||||
<ui-select-match placeholder="Select a user">
|
|
||||||
<span>{{ $select.selected.Username }}</span>
|
|
||||||
</ui-select-match>
|
|
||||||
<ui-select-choices repeat="item in (ctrl.users | filter: $select.search)">
|
|
||||||
<span>{{ item.Username }}</span>
|
|
||||||
</ui-select-choices>
|
|
||||||
</ui-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
|
||||||
Access
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="small text-muted" style="margin-bottom: 15px;">
|
|
||||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
|
||||||
Effective role for each endpoint will be displayed for the selected user
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<access-viewer-datatable ng-if="ctrl.users" table-key="access_viewer" dataset="ctrl.userRoles" order-by="EndpointName"> </access-viewer-datatable>
|
|
||||||
</form>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
|
@ -1,5 +0,0 @@
|
||||||
angular.module('portainer.app').component('accessViewer', {
|
|
||||||
templateUrl: './accessViewer.html',
|
|
||||||
controller: 'AccessViewerController',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
});
|
|
|
@ -1,126 +0,0 @@
|
||||||
import _ from 'lodash-es';
|
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import AccessViewerPolicyModel from '../../models/access';
|
|
||||||
|
|
||||||
class AccessViewerController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(Notifications, ExtensionService, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService) {
|
|
||||||
this.Notifications = Notifications;
|
|
||||||
this.ExtensionService = ExtensionService;
|
|
||||||
this.RoleService = RoleService;
|
|
||||||
this.UserService = UserService;
|
|
||||||
this.EndpointService = EndpointService;
|
|
||||||
this.GroupService = GroupService;
|
|
||||||
this.TeamService = TeamService;
|
|
||||||
this.TeamMembershipService = TeamMembershipService;
|
|
||||||
}
|
|
||||||
|
|
||||||
onUserSelect() {
|
|
||||||
this.userRoles = [];
|
|
||||||
const userRoles = {};
|
|
||||||
const user = this.selectedUser;
|
|
||||||
const userMemberships = _.filter(this.teamMemberships, { UserId: user.Id });
|
|
||||||
|
|
||||||
for (const [, endpoint] of _.entries(this.endpoints)) {
|
|
||||||
let role = this.getRoleFromUserEndpointPolicy(user, endpoint);
|
|
||||||
if (role) {
|
|
||||||
userRoles[endpoint.Id] = role;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
role = this.getRoleFromUserEndpointGroupPolicy(user, endpoint);
|
|
||||||
if (role) {
|
|
||||||
userRoles[endpoint.Id] = role;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
role = this.getRoleFromTeamEndpointPolicies(userMemberships, endpoint);
|
|
||||||
if (role) {
|
|
||||||
userRoles[endpoint.Id] = role;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
role = this.getRoleFromTeamEndpointGroupPolicies(userMemberships, endpoint);
|
|
||||||
if (role) {
|
|
||||||
userRoles[endpoint.Id] = role;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.userRoles = _.values(userRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
findLowestRole(policies) {
|
|
||||||
return _.first(_.orderBy(policies, 'RoleId', 'desc'));
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleFromUserEndpointPolicy(user, endpoint) {
|
|
||||||
const policyRoles = [];
|
|
||||||
const policy = endpoint.UserAccessPolicies[user.Id];
|
|
||||||
if (policy) {
|
|
||||||
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, null, null);
|
|
||||||
policyRoles.push(accessPolicy);
|
|
||||||
}
|
|
||||||
return this.findLowestRole(policyRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleFromUserEndpointGroupPolicy(user, endpoint) {
|
|
||||||
const policyRoles = [];
|
|
||||||
const policy = this.groupUserAccessPolicies[endpoint.GroupId][user.Id];
|
|
||||||
if (policy) {
|
|
||||||
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, this.groups[endpoint.GroupId], null);
|
|
||||||
policyRoles.push(accessPolicy);
|
|
||||||
}
|
|
||||||
return this.findLowestRole(policyRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleFromTeamEndpointPolicies(memberships, endpoint) {
|
|
||||||
const policyRoles = [];
|
|
||||||
for (const membership of memberships) {
|
|
||||||
const policy = endpoint.TeamAccessPolicies[membership.TeamId];
|
|
||||||
if (policy) {
|
|
||||||
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, null, this.teams[membership.TeamId]);
|
|
||||||
policyRoles.push(accessPolicy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.findLowestRole(policyRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleFromTeamEndpointGroupPolicies(memberships, endpoint) {
|
|
||||||
const policyRoles = [];
|
|
||||||
for (const membership of memberships) {
|
|
||||||
const policy = this.groupTeamAccessPolicies[endpoint.GroupId][membership.TeamId];
|
|
||||||
if (policy) {
|
|
||||||
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, this.groups[endpoint.GroupId], this.teams[membership.TeamId]);
|
|
||||||
policyRoles.push(accessPolicy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.findLowestRole(policyRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
async $onInit() {
|
|
||||||
try {
|
|
||||||
this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
if (this.rbacEnabled) {
|
|
||||||
this.users = await this.UserService.users();
|
|
||||||
this.endpoints = _.keyBy((await this.EndpointService.endpoints()).value, 'Id');
|
|
||||||
const groups = await this.GroupService.groups();
|
|
||||||
this.groupUserAccessPolicies = {};
|
|
||||||
this.groupTeamAccessPolicies = {};
|
|
||||||
_.forEach(groups, (group) => {
|
|
||||||
this.groupUserAccessPolicies[group.Id] = group.UserAccessPolicies;
|
|
||||||
this.groupTeamAccessPolicies[group.Id] = group.TeamAccessPolicies;
|
|
||||||
});
|
|
||||||
this.groups = _.keyBy(groups, 'Id');
|
|
||||||
this.roles = _.keyBy(await this.RoleService.roles(), 'Id');
|
|
||||||
this.teams = _.keyBy(await this.TeamService.teams(), 'Id');
|
|
||||||
this.teamMemberships = await this.TeamMembershipService.memberships();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve accesses');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AccessViewerController;
|
|
||||||
angular.module('portainer.app').controller('AccessViewerController', AccessViewerController);
|
|
|
@ -1,73 +0,0 @@
|
||||||
<div class="datatable">
|
|
||||||
<div class="searchBar">
|
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
|
||||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover nowrap-cells">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<a ng-click="$ctrl.changeOrderBy('EndpointName')">
|
|
||||||
Endpoint
|
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'EndpointName' && !$ctrl.state.reverseOrder"></i>
|
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'EndpointName' && $ctrl.state.reverseOrder"></i>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<a ng-click="$ctrl.changeOrderBy('RoleName')">
|
|
||||||
Role
|
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && !$ctrl.state.reverseOrder"></i>
|
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && $ctrl.state.reverseOrder"></i>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<th>Access origin</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit)) track by $index"
|
|
||||||
>
|
|
||||||
<td>{{ item.EndpointName }}</td>
|
|
||||||
<td>{{ item.RoleName }}</td>
|
|
||||||
<td
|
|
||||||
>{{ item.TeamName ? 'Team' : 'User' }} <code ng-if="item.TeamName">{{ item.TeamName }}</code> access defined on {{ item.AccessLocation }}
|
|
||||||
<code ng-if="item.GroupName">{{ item.GroupName }}</code>
|
|
||||||
<a ng-if="item.AccessLocation === 'endpoint'" ui-sref="portainer.endpoints.endpoint.access({id: item.EndpointId})"
|
|
||||||
><i style="margin-left: 5px;" class="fa fa-users" aria-hidden="true"></i> Manage access
|
|
||||||
</a>
|
|
||||||
<a ng-if="item.AccessLocation === 'endpoint group'" ui-sref="portainer.groups.group.access({id: item.GroupId})"
|
|
||||||
><i style="margin-left: 5px;" class="fa fa-users" aria-hidden="true"></i> Manage access
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="!$ctrl.dataset">
|
|
||||||
<td colspan="3" class="text-center text-muted">Select a user to show associated access and role</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
|
||||||
<td colspan="3" class="text-center text-muted">The selected user does not have access to any endpoint(s)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="footer" ng-if="$ctrl.dataset">
|
|
||||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
|
||||||
<div class="paginationControls">
|
|
||||||
<form class="form-inline">
|
|
||||||
<span class="limitSelector">
|
|
||||||
<span style="margin-right: 5px;">
|
|
||||||
Items per page
|
|
||||||
</span>
|
|
||||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
|
||||||
<option value="0">All</option>
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,11 +0,0 @@
|
||||||
angular.module('portainer.app').component('accessViewerDatatable', {
|
|
||||||
templateUrl: './accessViewerDatatable.html',
|
|
||||||
controller: 'GenericDatatableController',
|
|
||||||
bindings: {
|
|
||||||
titleText: '@',
|
|
||||||
titleIcon: '@',
|
|
||||||
tableKey: '@',
|
|
||||||
orderBy: '@',
|
|
||||||
dataset: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,79 +0,0 @@
|
||||||
<div class="datatable" ng-class="{ 'portainer-disabled-datatable': !$ctrl.rbacEnabled }">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="toolBar">
|
|
||||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
|
||||||
</div>
|
|
||||||
<div class="searchBar">
|
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="searchInput"
|
|
||||||
ng-model="$ctrl.state.textFilter"
|
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
|
||||||
placeholder="Search..."
|
|
||||||
auto-focus
|
|
||||||
ng-disabled="!$ctrl.rbacEnabled"
|
|
||||||
ng-model-options="{ debounce: 300 }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover nowrap-cells">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
|
||||||
Name
|
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<a ng-click="$ctrl.changeOrderBy('Description')">
|
|
||||||
Description
|
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Description' && !$ctrl.state.reverseOrder"></i>
|
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Description' && $ctrl.state.reverseOrder"></i>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
|
|
||||||
ng-class="{ active: item.Checked }"
|
|
||||||
>
|
|
||||||
<td>{{ item.Name }}</td>
|
|
||||||
<td>{{ item.Description }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="!$ctrl.dataset">
|
|
||||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
|
||||||
<td colspan="2" class="text-center text-muted">No role available.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="footer" ng-if="$ctrl.dataset">
|
|
||||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
|
||||||
<div class="paginationControls">
|
|
||||||
<form class="form-inline">
|
|
||||||
<span class="limitSelector">
|
|
||||||
<span style="margin-right: 5px;">
|
|
||||||
Items per page
|
|
||||||
</span>
|
|
||||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
|
||||||
<option value="0">All</option>
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
|
@ -1,13 +0,0 @@
|
||||||
angular.module('portainer.extensions.rbac').component('rolesDatatable', {
|
|
||||||
templateUrl: './rolesDatatable.html',
|
|
||||||
controller: 'GenericDatatableController',
|
|
||||||
bindings: {
|
|
||||||
titleText: '@',
|
|
||||||
titleIcon: '@',
|
|
||||||
dataset: '<',
|
|
||||||
tableKey: '@',
|
|
||||||
orderBy: '@',
|
|
||||||
reverseOrder: '<',
|
|
||||||
rbacEnabled: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,40 +0,0 @@
|
||||||
angular.module('portainer.extensions.rbac').directive('authorization', [
|
|
||||||
'Authentication',
|
|
||||||
'ExtensionService',
|
|
||||||
'$async',
|
|
||||||
function (Authentication, ExtensionService, $async) {
|
|
||||||
async function linkAsync(scope, elem, attrs) {
|
|
||||||
elem.hide();
|
|
||||||
try {
|
|
||||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
if (!rbacEnabled) {
|
|
||||||
elem.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
elem.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var authorizations = attrs.authorization.split(',');
|
|
||||||
for (var i = 0; i < authorizations.length; i++) {
|
|
||||||
authorizations[i] = authorizations[i].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasAuthorizations = Authentication.hasAuthorizations(authorizations);
|
|
||||||
|
|
||||||
if (hasAuthorizations) {
|
|
||||||
elem.show();
|
|
||||||
} else if (!hasAuthorizations && elem[0].tagName === 'A') {
|
|
||||||
elem.show();
|
|
||||||
elem.addClass('portainer-disabled-link');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
restrict: 'A',
|
|
||||||
link: function (scope, elem, attrs) {
|
|
||||||
return $async(linkAsync, scope, elem, attrs);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,36 +0,0 @@
|
||||||
angular.module('portainer.extensions.rbac').directive('disableAuthorization', [
|
|
||||||
'Authentication',
|
|
||||||
'ExtensionService',
|
|
||||||
'$async',
|
|
||||||
function (Authentication, ExtensionService, $async) {
|
|
||||||
async function linkAsync(scope, elem, attrs) {
|
|
||||||
try {
|
|
||||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
if (!rbacEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var authorizations = attrs.disableAuthorization.split(',');
|
|
||||||
for (var i = 0; i < authorizations.length; i++) {
|
|
||||||
authorizations[i] = authorizations[i].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Authentication.hasAuthorizations(authorizations)) {
|
|
||||||
elem.attr('disabled', true);
|
|
||||||
if (elem.is('Slider')) {
|
|
||||||
elem.css('pointer-events', 'none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
restrict: 'A',
|
|
||||||
link: function (scope, elem, attrs) {
|
|
||||||
return $async(linkAsync, scope, elem, attrs);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,15 +0,0 @@
|
||||||
export default function AccessViewerPolicyModel(policy, endpoint, roles, group, team) {
|
|
||||||
this.EndpointId = endpoint.Id;
|
|
||||||
this.EndpointName = endpoint.Name;
|
|
||||||
this.RoleId = policy.RoleId;
|
|
||||||
this.RoleName = roles[policy.RoleId].Name;
|
|
||||||
if (group) {
|
|
||||||
this.GroupId = group.Id;
|
|
||||||
this.GroupName = group.Name;
|
|
||||||
}
|
|
||||||
if (team) {
|
|
||||||
this.TeamId = team.Id;
|
|
||||||
this.TeamName = team.Name;
|
|
||||||
}
|
|
||||||
this.AccessLocation = group ? 'endpoint group' : 'endpoint';
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export function RoleViewModel(data) {
|
|
||||||
this.ID = data.Id;
|
|
||||||
this.Name = data.Name;
|
|
||||||
this.Description = data.Description;
|
|
||||||
this.Authorizations = data.Authorizations;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
angular.module('portainer.app').factory('Roles', [
|
|
||||||
'$resource',
|
|
||||||
'API_ENDPOINT_ROLES',
|
|
||||||
function RolesFactory($resource, API_ENDPOINT_ROLES) {
|
|
||||||
'use strict';
|
|
||||||
return $resource(
|
|
||||||
API_ENDPOINT_ROLES + '/:id',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
create: { method: 'POST', ignoreLoadingBar: true },
|
|
||||||
query: { method: 'GET', isArray: true },
|
|
||||||
get: { method: 'GET', params: { id: '@id' } },
|
|
||||||
update: { method: 'PUT', params: { id: '@id' } },
|
|
||||||
remove: { method: 'DELETE', params: { id: '@id' } },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,49 +0,0 @@
|
||||||
import {
|
|
||||||
RoleViewModel,
|
|
||||||
// EndpointRoleCreateRequest,
|
|
||||||
// EndpointRoleUpdateRequest
|
|
||||||
} from '../models/role';
|
|
||||||
|
|
||||||
angular.module('portainer.extensions.rbac').factory('RoleService', [
|
|
||||||
'$q',
|
|
||||||
'Roles',
|
|
||||||
function RoleService($q, Roles) {
|
|
||||||
'use strict';
|
|
||||||
var service = {};
|
|
||||||
|
|
||||||
service.role = function (roleId) {
|
|
||||||
var deferred = $q.defer();
|
|
||||||
|
|
||||||
Roles.get({ id: roleId })
|
|
||||||
.$promise.then(function success(data) {
|
|
||||||
var role = new RoleViewModel(data);
|
|
||||||
deferred.resolve(role);
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
deferred.reject({ msg: 'Unable to retrieve role', err: err });
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
service.roles = function () {
|
|
||||||
return Roles.query({}).$promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
// service.createRole = function(model, endpoints) {
|
|
||||||
// var payload = new EndpointRoleCreateRequest(model, endpoints);
|
|
||||||
// return EndpointRoles.create(payload).$promise;
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// service.updateRole = function(model, endpoints) {
|
|
||||||
// var payload = new EndpointRoleUpdateRequest(model, endpoints);
|
|
||||||
// return EndpointRoles.update(payload).$promise;
|
|
||||||
// };
|
|
||||||
|
|
||||||
service.deleteRole = function (roleId) {
|
|
||||||
return Roles.remove({ id: roleId }).$promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,36 +0,0 @@
|
||||||
<rd-header>
|
|
||||||
<rd-header-title title-text="Roles">
|
|
||||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.roles" ui-sref-opts="{reload: true}">
|
|
||||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</rd-header-title>
|
|
||||||
<rd-header-content>Role management</rd-header-content>
|
|
||||||
</rd-header>
|
|
||||||
|
|
||||||
<information-panel ng-if="!ctrl.rbacEnabled" title-text="Information">
|
|
||||||
<span class="small">
|
|
||||||
<p class="text-muted">
|
|
||||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
|
||||||
The
|
|
||||||
<a
|
|
||||||
ui-sref="portainer.extensions.extension({id: 3})"
|
|
||||||
tooltip-append-to-body="true"
|
|
||||||
tooltip-placement="bottom"
|
|
||||||
tooltip-class="portainer-tooltip"
|
|
||||||
uib-tooltip="Feature available via an extension"
|
|
||||||
>Role-Based Access Control extension</a
|
|
||||||
>
|
|
||||||
is required to use this feature.
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
</information-panel>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<roles-datatable title-text="Roles" title-icon="fa-file-code" dataset="ctrl.roles" table-key="roles" order-by="Name" rbac-enabled="ctrl.rbacEnabled"></roles-datatable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<access-viewer ng-if="ctrl.rbacEnabled"> </access-viewer>
|
|
||||||
</div>
|
|
|
@ -1,24 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
class RolesController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(Notifications, RoleService, ExtensionService) {
|
|
||||||
this.Notifications = Notifications;
|
|
||||||
this.RoleService = RoleService;
|
|
||||||
this.ExtensionService = ExtensionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
async $onInit() {
|
|
||||||
this.roles = [];
|
|
||||||
this.rbacEnabled = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
this.roles = await this.RoleService.roles();
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve roles');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default RolesController;
|
|
||||||
angular.module('portainer.extensions.rbac').controller('RolesController', RolesController);
|
|
|
@ -57,21 +57,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KEEP FOR FUTURE RBAC EVOL -->
|
|
||||||
<!-- <div class="form-group" ng-if="ctrl.entityType !== 'registry'">
|
|
||||||
<label class="col-sm-3 col-lg-2 control-label text-left">
|
|
||||||
Role
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9 col-lg-4">
|
|
||||||
<select ng-if="ctrl.rbacEnabled" class="form-control" ng-model="ctrl.formValues.selectedRole"
|
|
||||||
ng-options="role.Name for role in ctrl.roles">
|
|
||||||
</select>
|
|
||||||
<span class="small text-muted" ng-if="!ctrl.rbacEnabled">
|
|
||||||
The <a ui-sref="portainer.extensions.extension({id: 3})">Role-Based Access Control extension</a> is required to select a specific role.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
@ -96,11 +81,6 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<!--
|
|
||||||
rbac-enabled="ctrl.rbacEnabled && ctrl.entityType !== 'registry'"
|
|
||||||
roles="ctrl.roles"
|
|
||||||
update-action="ctrl.updateAction"
|
|
||||||
-->
|
|
||||||
<access-datatable
|
<access-datatable
|
||||||
ng-if="ctrl.authorizedUsersAndTeams"
|
ng-if="ctrl.authorizedUsersAndTeams"
|
||||||
title-text="Access"
|
title-text="Access"
|
||||||
|
|
|
@ -35,9 +35,6 @@ class KubernetesResourcePoolAccessController {
|
||||||
/**
|
/**
|
||||||
* Init
|
* Init
|
||||||
*/
|
*/
|
||||||
// TODO: refactor: roles need to be fetched if RBAC is activated on Portainer
|
|
||||||
// see porAccessManagementController for more details
|
|
||||||
// Extract the fetching code and merge it in AccessService.accesses() function
|
|
||||||
async onInit() {
|
async onInit() {
|
||||||
this.state = {
|
this.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
|
|
|
@ -281,28 +281,6 @@ angular.module('portainer.app', ['portainer.oauth']).config([
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var extensions = {
|
|
||||||
name: 'portainer.extensions',
|
|
||||||
url: '/extensions',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/extensions/extensions.html',
|
|
||||||
controller: 'ExtensionsController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var extension = {
|
|
||||||
name: 'portainer.extensions.extension',
|
|
||||||
url: '/extension/:id',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/extensions/inspect/extension.html',
|
|
||||||
controller: 'ExtensionController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var registries = {
|
var registries = {
|
||||||
name: 'portainer.registries',
|
name: 'portainer.registries',
|
||||||
url: '/registries',
|
url: '/registries',
|
||||||
|
@ -469,8 +447,6 @@ angular.module('portainer.app', ['portainer.oauth']).config([
|
||||||
$stateRegistryProvider.register(init);
|
$stateRegistryProvider.register(init);
|
||||||
$stateRegistryProvider.register(initEndpoint);
|
$stateRegistryProvider.register(initEndpoint);
|
||||||
$stateRegistryProvider.register(initAdmin);
|
$stateRegistryProvider.register(initAdmin);
|
||||||
$stateRegistryProvider.register(extensions);
|
|
||||||
$stateRegistryProvider.register(extension);
|
|
||||||
$stateRegistryProvider.register(registries);
|
$stateRegistryProvider.register(registries);
|
||||||
$stateRegistryProvider.register(registry);
|
$stateRegistryProvider.register(registry);
|
||||||
$stateRegistryProvider.register(registryAccess);
|
$stateRegistryProvider.register(registryAccess);
|
||||||
|
|
|
@ -10,15 +10,6 @@
|
||||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
ng-if="$ctrl.rbacEnabled"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-primary"
|
|
||||||
ng-disabled="($ctrl.dataset | filter:{ Updated: true}).length === 0 "
|
|
||||||
ng-click="$ctrl.updateAction()"
|
|
||||||
>
|
|
||||||
<i class="fa fa-check space-right" aria-hidden="true"></i>Update
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar">
|
<div class="searchBar">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
@ -53,13 +44,6 @@
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th ng-if="$ctrl.rbacEnabled">
|
|
||||||
<a ng-click="$ctrl.changeOrderBy('RoleName')">
|
|
||||||
Role
|
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && !$ctrl.state.reverseOrder"></i>
|
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && $ctrl.state.reverseOrder"></i>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -79,18 +63,6 @@
|
||||||
<span ng-if="$ctrl.inheritFrom && item.Override" class="text-muted small" style="margin-left: 2px;"><code style="font-size: 85% !important;">override</code></span>
|
<span ng-if="$ctrl.inheritFrom && item.Override" class="text-muted small" style="margin-left: 2px;"><code style="font-size: 85% !important;">override</code></span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.Type }}</td>
|
<td>{{ item.Type }}</td>
|
||||||
<td ng-if="$ctrl.rbacEnabled">
|
|
||||||
<span ng-if="!item.Updated">
|
|
||||||
{{ item.Role.Name }}
|
|
||||||
<a ng-if="!item.Inherited" class="interactive" ng-click="item.Updated = true; item.OldRole = item.Role; $event.stopPropagation();">
|
|
||||||
<i class="fa fa-edit" aria-hidden="true"></i> Edit
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span ng-if="item.Updated">
|
|
||||||
<select ng-model="item.Role" ng-options="role.Name for role in $ctrl.roles"> </select>
|
|
||||||
<a class="interactive" ng-click="item.Updated = false; item.Role = item.OldRole; item.OldRole = null; $event.stopPropagation();"><i class="fa fa-times"></i></a>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<tr ng-if="!$ctrl.dataset">
|
||||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||||
|
|
|
@ -11,7 +11,6 @@ angular.module('portainer.app').component('accessDatatable', {
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
updateAction: '<',
|
updateAction: '<',
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
rbacEnabled: '<',
|
|
||||||
inheritFrom: '<',
|
inheritFrom: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,17 +27,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="ctrl.entityType !== 'registry'">
|
|
||||||
<label class="col-sm-3 col-lg-2 control-label text-left">
|
|
||||||
Role
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9 col-lg-4">
|
|
||||||
<select ng-if="ctrl.rbacEnabled" class="form-control" ng-model="ctrl.formValues.selectedRole" ng-options="role.Name for role in ctrl.roles"> </select>
|
|
||||||
<span class="small text-muted" ng-if="!ctrl.rbacEnabled">
|
|
||||||
The <a ui-sref="portainer.extensions.extension({id: 3})">Role-Based Access Control extension</a> is required to select a specific role.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
@ -67,10 +57,8 @@
|
||||||
title-icon="fa-user-lock"
|
title-icon="fa-user-lock"
|
||||||
table-key="{{ 'access_' + ctrl.entityType }}"
|
table-key="{{ 'access_' + ctrl.entityType }}"
|
||||||
order-by="Name"
|
order-by="Name"
|
||||||
rbac-enabled="ctrl.rbacEnabled && ctrl.entityType !== 'registry'"
|
|
||||||
inherit-from="ctrl.inheritFrom"
|
inherit-from="ctrl.inheritFrom"
|
||||||
dataset="ctrl.authorizedUsersAndTeams"
|
dataset="ctrl.authorizedUsersAndTeams"
|
||||||
roles="ctrl.roles"
|
|
||||||
update-action="ctrl.updateAction"
|
update-action="ctrl.updateAction"
|
||||||
remove-action="ctrl.unauthorizeAccess"
|
remove-action="ctrl.unauthorizeAccess"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,11 +4,9 @@ import angular from 'angular';
|
||||||
|
|
||||||
class PorAccessManagementController {
|
class PorAccessManagementController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(Notifications, ExtensionService, AccessService, RoleService) {
|
constructor(Notifications, AccessService) {
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.ExtensionService = ExtensionService;
|
|
||||||
this.AccessService = AccessService;
|
this.AccessService = AccessService;
|
||||||
this.RoleService = RoleService;
|
|
||||||
|
|
||||||
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
||||||
this.updateAction = this.updateAction.bind(this);
|
this.updateAction = this.updateAction.bind(this);
|
||||||
|
@ -31,11 +29,10 @@ class PorAccessManagementController {
|
||||||
const entity = this.accessControlledEntity;
|
const entity = this.accessControlledEntity;
|
||||||
const oldUserAccessPolicies = entity.UserAccessPolicies;
|
const oldUserAccessPolicies = entity.UserAccessPolicies;
|
||||||
const oldTeamAccessPolicies = entity.TeamAccessPolicies;
|
const oldTeamAccessPolicies = entity.TeamAccessPolicies;
|
||||||
const selectedRoleId = this.rbacEnabled ? this.formValues.selectedRole.Id : 0;
|
|
||||||
const selectedUserAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'user');
|
const selectedUserAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'user');
|
||||||
const selectedTeamAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'team');
|
const selectedTeamAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'team');
|
||||||
|
|
||||||
const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId);
|
const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, 0);
|
||||||
this.accessControlledEntity.UserAccessPolicies = accessPolicies.userAccessPolicies;
|
this.accessControlledEntity.UserAccessPolicies = accessPolicies.userAccessPolicies;
|
||||||
this.accessControlledEntity.TeamAccessPolicies = accessPolicies.teamAccessPolicies;
|
this.accessControlledEntity.TeamAccessPolicies = accessPolicies.teamAccessPolicies;
|
||||||
this.updateAccess();
|
this.updateAccess();
|
||||||
|
@ -56,18 +53,7 @@ class PorAccessManagementController {
|
||||||
try {
|
try {
|
||||||
const entity = this.accessControlledEntity;
|
const entity = this.accessControlledEntity;
|
||||||
const parent = this.inheritFrom;
|
const parent = this.inheritFrom;
|
||||||
// TODO: refactor
|
|
||||||
// extract this code and locate it in AccessService.accesses() function
|
|
||||||
// see resourcePoolAccessController for another usage of AccessService.accesses()
|
|
||||||
// which needs RBAC support
|
|
||||||
this.roles = [];
|
|
||||||
this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC);
|
|
||||||
if (this.rbacEnabled) {
|
|
||||||
this.roles = await this.RoleService.roles();
|
|
||||||
this.formValues = {
|
|
||||||
selectedRole: this.roles[0],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const data = await this.AccessService.accesses(entity, parent, this.roles);
|
const data = await this.AccessService.accesses(entity, parent, this.roles);
|
||||||
this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc');
|
this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc');
|
||||||
this.authorizedUsersAndTeams = data.authorizedUsersAndTeams;
|
this.authorizedUsersAndTeams = data.authorizedUsersAndTeams;
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
angular.module('portainer.app').component('extensionItem', {
|
|
||||||
templateUrl: './extensionItem.html',
|
|
||||||
controller: 'ExtensionItemController',
|
|
||||||
bindings: {
|
|
||||||
model: '<',
|
|
||||||
currentDate: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,47 +0,0 @@
|
||||||
<!-- extension -->
|
|
||||||
<div class="blocklist-item" ng-click="$ctrl.goToExtensionView()" ng-class="{ 'blocklist-item--disabled': !$ctrl.model.Available }">
|
|
||||||
<div class="blocklist-item-box">
|
|
||||||
<!-- extension-image -->
|
|
||||||
<span ng-if="$ctrl.model.Logo" style="width: 75px; text-align: center;">
|
|
||||||
<!-- <img class="blocklist-item-logo" ng-src="{{ $ctrl.model.Logo }}" /> -->
|
|
||||||
<i class="{{ $ctrl.model.Logo }} fa fa-4x blue-icon" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
<span class="blocklist-item-logo" ng-if="!$ctrl.model.Logo">
|
|
||||||
<i class="fa fa-bolt fa-4x blue-icon" style="margin-left: 14px;" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
<!-- !extension-image -->
|
|
||||||
<!-- extension-details -->
|
|
||||||
<span class="col-sm-12">
|
|
||||||
<!-- blocklist-item-line1 -->
|
|
||||||
<div class="blocklist-item-line">
|
|
||||||
<span>
|
|
||||||
<span class="blocklist-item-title">
|
|
||||||
{{ $ctrl.model.Name }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<span class="label label-primary" ng-if="!$ctrl.model.Enabled && !$ctrl.model.Available">coming soon</span>
|
|
||||||
<span class="label label-warning" ng-if="!$ctrl.model.Enabled && $ctrl.model.Deal && !$ctrl.model.License.Expiration">deal</span>
|
|
||||||
<span class="label label-danger" ng-if="!$ctrl.model.Enabled && $ctrl.model.License.Expiration && !$ctrl.model.License.Valid">expired</span>
|
|
||||||
<span class="label label-success" ng-if="$ctrl.model.Enabled && $ctrl.model.License.Valid">enabled</span>
|
|
||||||
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.License.Valid && $ctrl.model.UpdateAvailable">update available</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- !blocklist-item-line1 -->
|
|
||||||
<!-- blocklist-item-line2 -->
|
|
||||||
<div class="blocklist-item-line">
|
|
||||||
<span>
|
|
||||||
<span class="blocklist-item-desc">
|
|
||||||
{{ $ctrl.model.ShortDescription }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span ng-if="$ctrl.model.License.Company">
|
|
||||||
<span class="small text-muted">Licensed to {{ $ctrl.model.License.Company }} - Expires on {{ $ctrl.model.License.Expiration }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- !blocklist-item-line2 -->
|
|
||||||
</span>
|
|
||||||
<!-- !extension-details -->
|
|
||||||
</div>
|
|
||||||
<!-- !extension -->
|
|
||||||
</div>
|
|
|
@ -1,13 +0,0 @@
|
||||||
angular.module('portainer.app').controller('ExtensionItemController', [
|
|
||||||
'$state',
|
|
||||||
function ($state) {
|
|
||||||
var ctrl = this;
|
|
||||||
ctrl.goToExtensionView = goToExtensionView;
|
|
||||||
|
|
||||||
function goToExtensionView() {
|
|
||||||
if (ctrl.model.Available) {
|
|
||||||
$state.go('portainer.extensions.extension', { id: ctrl.model.Id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,7 +0,0 @@
|
||||||
angular.module('portainer.app').component('extensionList', {
|
|
||||||
templateUrl: './extensionList.html',
|
|
||||||
bindings: {
|
|
||||||
extensions: '<',
|
|
||||||
currentDate: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,13 +0,0 @@
|
||||||
<div class="datatable">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="toolBar">
|
|
||||||
<div class="toolBarTitle"> <i class="fa fa-bolt" aria-hidden="true" style="margin-right: 2px;"></i> Available extensions </div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="blocklist">
|
|
||||||
<extension-item ng-repeat="extension in $ctrl.extensions" model="extension" current-date="$ctrl.currentDate"></extension-item>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
|
@ -1,17 +0,0 @@
|
||||||
export function ExtensionViewModel(data) {
|
|
||||||
this.Id = data.Id;
|
|
||||||
this.Name = data.Name;
|
|
||||||
this.Enabled = data.Enabled;
|
|
||||||
this.Description = data.Description;
|
|
||||||
this.Price = data.Price;
|
|
||||||
this.PriceDescription = data.PriceDescription;
|
|
||||||
this.Available = data.Available;
|
|
||||||
this.Deal = data.Deal;
|
|
||||||
this.ShortDescription = data.ShortDescription;
|
|
||||||
this.License = data.License;
|
|
||||||
this.Version = data.Version;
|
|
||||||
this.UpdateAvailable = data.UpdateAvailable;
|
|
||||||
this.ShopURL = data.ShopURL;
|
|
||||||
this.Images = data.Images;
|
|
||||||
this.Logo = data.Logo;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
angular.module('portainer.app').factory('Extension', [
|
|
||||||
'$resource',
|
|
||||||
'API_ENDPOINT_EXTENSIONS',
|
|
||||||
function ExtensionFactory($resource, API_ENDPOINT_EXTENSIONS) {
|
|
||||||
'use strict';
|
|
||||||
return $resource(
|
|
||||||
API_ENDPOINT_EXTENSIONS + '/:id/:action',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
create: { method: 'POST' },
|
|
||||||
query: { method: 'GET', isArray: true },
|
|
||||||
get: { method: 'GET', params: { id: '@id' } },
|
|
||||||
delete: { method: 'DELETE', params: { id: '@id' } },
|
|
||||||
update: { method: 'POST', params: { id: '@id', action: 'update' } },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -11,15 +11,7 @@ angular.module('portainer.app').factory('AccessService', [
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
function _getRole(roles, roleId) {
|
function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies) {
|
||||||
if (roles.length) {
|
|
||||||
const role = _.find(roles, (role) => role.Id === roleId);
|
|
||||||
return role ? role : { Id: 0, Name: '-' };
|
|
||||||
}
|
|
||||||
return { Id: 0, Name: '-' };
|
|
||||||
}
|
|
||||||
|
|
||||||
function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies, roles) {
|
|
||||||
var availableAccesses = [];
|
var availableAccesses = [];
|
||||||
var authorizedAccesses = [];
|
var authorizedAccesses = [];
|
||||||
|
|
||||||
|
@ -30,14 +22,11 @@ angular.module('portainer.app').factory('AccessService', [
|
||||||
const inherited = inheritedPolicies && inheritedPolicies[access.Id];
|
const inherited = inheritedPolicies && inheritedPolicies[access.Id];
|
||||||
|
|
||||||
if (authorized && inherited) {
|
if (authorized && inherited) {
|
||||||
access.Role = _getRole(roles, authorizedPolicies[access.Id].RoleId);
|
|
||||||
access.Override = true;
|
access.Override = true;
|
||||||
authorizedAccesses.push(access);
|
authorizedAccesses.push(access);
|
||||||
} else if (authorized && !inherited) {
|
} else if (authorized && !inherited) {
|
||||||
access.Role = _getRole(roles, authorizedPolicies[access.Id].RoleId);
|
|
||||||
authorizedAccesses.push(access);
|
authorizedAccesses.push(access);
|
||||||
} else if (!authorized && inherited) {
|
} else if (!authorized && inherited) {
|
||||||
access.Role = _getRole(roles, inheritedPolicies[access.Id].RoleId);
|
|
||||||
access.Inherited = true;
|
access.Inherited = true;
|
||||||
authorizedAccesses.push(access);
|
authorizedAccesses.push(access);
|
||||||
availableAccesses.push(access);
|
availableAccesses.push(access);
|
||||||
|
@ -52,7 +41,7 @@ angular.module('portainer.app').factory('AccessService', [
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAccesses(authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies, roles) {
|
function getAccesses(authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
|
@ -67,8 +56,8 @@ angular.module('portainer.app').factory('AccessService', [
|
||||||
return new TeamAccessViewModel(team);
|
return new TeamAccessViewModel(team);
|
||||||
});
|
});
|
||||||
|
|
||||||
var userAccessData = _mapAccessData(userAccesses, authorizedUserPolicies, inheritedUserPolicies, roles);
|
var userAccessData = _mapAccessData(userAccesses, authorizedUserPolicies, inheritedUserPolicies);
|
||||||
var teamAccessData = _mapAccessData(teamAccesses, authorizedTeamPolicies, inheritedTeamPolicies, roles);
|
var teamAccessData = _mapAccessData(teamAccesses, authorizedTeamPolicies, inheritedTeamPolicies);
|
||||||
|
|
||||||
var accessData = {
|
var accessData = {
|
||||||
availableUsersAndTeams: userAccessData.available.concat(teamAccessData.available),
|
availableUsersAndTeams: userAccessData.available.concat(teamAccessData.available),
|
||||||
|
@ -84,7 +73,7 @@ angular.module('portainer.app').factory('AccessService', [
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function accessesAsync(entity, parent, roles) {
|
async function accessesAsync(entity, parent) {
|
||||||
try {
|
try {
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
throw { msg: 'Unable to retrieve accesses' };
|
throw { msg: 'Unable to retrieve accesses' };
|
||||||
|
@ -101,14 +90,14 @@ angular.module('portainer.app').factory('AccessService', [
|
||||||
if (parent && !parent.TeamAccessPolicies) {
|
if (parent && !parent.TeamAccessPolicies) {
|
||||||
parent.TeamAccessPolicies = {};
|
parent.TeamAccessPolicies = {};
|
||||||
}
|
}
|
||||||
return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {}, roles);
|
return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function accesses(entity, parent, roles) {
|
function accesses(entity, parent) {
|
||||||
return $async(accessesAsync, entity, parent, roles);
|
return $async(accessesAsync, entity, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
service.accesses = accesses;
|
service.accesses = accesses;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue