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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (store *Store) Registry() portainer.RegistryService {
|
||||
return store.RegistryService
|
||||
|
|
|
@ -3,7 +3,6 @@ package bolt
|
|||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
"github.com/portainer/portainer/api/git"
|
||||
"github.com/portainer/portainer/api/http"
|
||||
"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/jwt"
|
||||
"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)
|
||||
}
|
||||
|
||||
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) {
|
||||
timer1 := time.NewTimer(5 * time.Minute)
|
||||
<-timer1.C
|
||||
|
@ -372,11 +360,6 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
extensionManager, err := initExtensionManager(fileService, dataStore)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
reverseTunnelService := chisel.NewService(dataStore)
|
||||
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
|
@ -439,10 +422,9 @@ func main() {
|
|||
if len(users) == 0 {
|
||||
log.Println("Created admin user with the given password.")
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
}
|
||||
err := dataStore.User().CreateUser(user)
|
||||
if err != nil {
|
||||
|
@ -469,7 +451,6 @@ func main() {
|
|||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
ExtensionManager: extensionManager,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
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/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
|
@ -96,12 +95,6 @@ func (service *Service) GetBinaryFolder() string {
|
|||
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.
|
||||
func (service *Service) RemoveDirectory(directoryPath string) error {
|
||||
return os.RemoveAll(directoryPath)
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
|
@ -103,9 +97,8 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
|||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type oauthPayload struct {
|
||||
|
@ -76,9 +75,8 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
|||
|
||||
if user == nil {
|
||||
user = &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle authentication operations.
|
||||
|
@ -21,7 +20,6 @@ type Handler struct {
|
|||
LDAPService portainer.LDAPService
|
||||
OAuthService portainer.OAuthService
|
||||
ProxyManager *proxy.Manager
|
||||
AuthorizationService *authorization.Service
|
||||
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}
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
|
||||
updateAuthorizations = true
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
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 {
|
||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||
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) {
|
||||
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
|
||||
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
if updateAuthorizations {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
if tagsChanged {
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
|
|
|
@ -7,14 +7,12 @@ import (
|
|||
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 endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
|
|
@ -445,15 +445,6 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
|
|||
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 {
|
||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||
if err != nil {
|
||||
|
|
|
@ -40,13 +40,6 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
|||
|
||||
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)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
|
||||
endpoint.UserAccessPolicies = payload.UserAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) {
|
||||
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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) {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"net/http"
|
||||
|
||||
|
@ -24,7 +23,6 @@ type Handler struct {
|
|||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
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/endpointproxy"
|
||||
"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/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
|
@ -50,7 +49,6 @@ type Handler struct {
|
|||
EndpointProxyHandler *endpointproxy.Handler
|
||||
FileHandler *file.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
ExtensionHandler *extensions.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
RoleHandler *roles.Handler
|
||||
|
@ -104,8 +102,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
default:
|
||||
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"):
|
||||
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||
|
|
|
@ -3,8 +3,6 @@ package settings
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
|
@ -19,12 +17,11 @@ func hideFields(settings *portainer.Settings) {
|
|||
// Handler is the HTTP handler used to handle settings operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService portainer.SnapshotService
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService portainer.SnapshotService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
|
@ -116,10 +115,8 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
||||
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
settings.SnapshotInterval = snapshotInterval
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"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/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) {
|
||||
isAdmin := user.Role == portainer.AdministratorRole
|
||||
if isAdmin {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
rbacExtension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
||||
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
|
||||
return isAdmin, nil
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"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)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled)
|
||||
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs)
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
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"
|
||||
|
||||
"net/http"
|
||||
|
||||
|
@ -14,8 +13,7 @@ import (
|
|||
// Handler is the HTTP handler used to handle team membership operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -13,8 +12,7 @@ import (
|
|||
// Handler is the HTTP handler used to handle team operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type adminInitPayload struct {
|
||||
|
@ -45,9 +44,8 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe
|
|||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
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"
|
||||
|
||||
"net/http"
|
||||
|
||||
|
@ -28,9 +27,8 @@ func hideFields(user *portainer.User) {
|
|||
// Handler is the HTTP handler used to handle user operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage user operations.
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type userCreatePayload struct {
|
||||
|
@ -62,9 +61,8 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
|||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.UserRole(payload.Role),
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: payload.Username,
|
||||
Role: portainer.UserRole(payload.Role),
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
@ -402,16 +401,6 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
|||
}
|
||||
|
||||
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 {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
|
@ -419,28 +408,10 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
|||
}
|
||||
|
||||
if !settings.AllowVolumeBrowserForRegularUsers {
|
||||
if rbacExtension == nil {
|
||||
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()
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -713,25 +684,5 @@ func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool,
|
|||
return false, err
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
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
|
||||
return tokenData.Role == portainer.AdministratorRole, nil
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
@ -16,12 +15,8 @@ import (
|
|||
|
||||
const azureAPIBaseURL = "https://management.azure.com"
|
||||
|
||||
var extensionPorts = map[portainer.ExtensionID]string{
|
||||
portainer.RBACExtension: "7003",
|
||||
}
|
||||
|
||||
type (
|
||||
// ProxyFactory is a factory to create reverse proxies to Docker endpoints and extensions
|
||||
// ProxyFactory is a factory to create reverse proxies
|
||||
ProxyFactory struct {
|
||||
dataStore portainer.DataStore
|
||||
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)
|
||||
func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (http.Handler, error) {
|
||||
extensionURL, err := url.Parse(extensionAPIURL)
|
||||
|
|
|
@ -2,7 +2,6 @@ package proxy
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
|
||||
|
@ -21,7 +20,6 @@ type (
|
|||
Manager struct {
|
||||
proxyFactory *factory.ProxyFactory
|
||||
endpointProxies cmap.ConcurrentMap
|
||||
extensionProxies 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 {
|
||||
return &Manager{
|
||||
endpointProxies: cmap.New(),
|
||||
extensionProxies: cmap.New(),
|
||||
legacyExtensionProxies: cmap.New(),
|
||||
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))
|
||||
}
|
||||
|
||||
// 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
|
||||
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||
proxy, err := manager.proxyFactory.NewLegacyExtensionProxy(extensionAPIURL)
|
||||
|
|
|
@ -14,9 +14,8 @@ import (
|
|||
type (
|
||||
// RequestBouncer represents an entity that manages API request accesses
|
||||
RequestBouncer struct {
|
||||
dataStore portainer.DataStore
|
||||
jwtService portainer.JWTService
|
||||
rbacExtensionClient *rbacExtensionClient
|
||||
dataStore portainer.DataStore
|
||||
jwtService portainer.JWTService
|
||||
}
|
||||
|
||||
// RestrictedRequestContext is a data structure containing information
|
||||
|
@ -30,11 +29,10 @@ type (
|
|||
)
|
||||
|
||||
// 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{
|
||||
dataStore: dataStore,
|
||||
jwtService: jwtService,
|
||||
rbacExtensionClient: newRBACExtensionClient(rbacExtensionURL),
|
||||
dataStore: dataStore,
|
||||
jwtService: jwtService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
// 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, the administrator role is required to use these endpoints.
|
||||
// The administrator role is required to use these endpoints.
|
||||
// The request context will be enhanced with a RestrictedRequestContext object
|
||||
// that might be used later to inside the API operation for extra authorization validation
|
||||
// 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.
|
||||
// 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
|
||||
// that might be used later to inside the API operation for extra authorization validation
|
||||
// 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
|
||||
// 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
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -114,13 +107,6 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp
|
|||
return httperrors.ErrEndpointAccessDenied
|
||||
}
|
||||
|
||||
if authorizationCheck {
|
||||
err = bouncer.checkEndpointOperationAuthorization(r, endpoint)
|
||||
if err != nil {
|
||||
return ErrAuthorizationRequired
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -142,38 +128,6 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
|
|||
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
|
||||
// that the user can access the specified registry.
|
||||
// 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
|
||||
// a specific API endpoint. It will leverage the RBAC extension authorization validation if the extension
|
||||
// is enabled.
|
||||
// If the administratorOnly flag is specified and the RBAC extension is not enabled, this will prevent non-admin
|
||||
// a specific API endpoint.
|
||||
// If the administratorOnly flag is specified, this will prevent non-admin
|
||||
// users from accessing the endpoint.
|
||||
func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, administratorOnly bool) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -223,21 +176,12 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler,
|
|||
return
|
||||
}
|
||||
|
||||
extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
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)
|
||||
if administratorOnly {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := bouncer.dataStore.User().User(tokenData.ID)
|
||||
_, err = bouncer.dataStore.User().User(tokenData.ID)
|
||||
if err != nil && err == bolterrors.ErrObjectNotFound {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized)
|
||||
return
|
||||
|
@ -246,19 +190,6 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler,
|
|||
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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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/endpointproxy"
|
||||
"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/motd"
|
||||
"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/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
|
@ -51,7 +49,6 @@ type Server struct {
|
|||
AssetsPath string
|
||||
Status *portainer.Status
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionManager portainer.ExtensionManager
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
CryptoService portainer.CryptoService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
|
@ -74,12 +71,10 @@ type Server struct {
|
|||
|
||||
// Start starts the HTTP server
|
||||
func (server *Server) Start() error {
|
||||
authorizationService := authorization.NewService(server.DataStore)
|
||||
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
|
||||
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, rbacExtensionURL)
|
||||
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
|
||||
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
|
@ -89,7 +84,6 @@ func (server *Server) Start() error {
|
|||
authHandler.JWTService = server.JWTService
|
||||
authHandler.LDAPService = server.LDAPService
|
||||
authHandler.ProxyManager = proxyManager
|
||||
authHandler.AuthorizationService = authorizationService
|
||||
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
||||
authHandler.OAuthService = server.OAuthService
|
||||
|
||||
|
@ -122,7 +116,6 @@ func (server *Server) Start() error {
|
|||
|
||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
||||
endpointHandler.DataStore = server.DataStore
|
||||
endpointHandler.AuthorizationService = authorizationService
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
endpointHandler.SnapshotService = server.SnapshotService
|
||||
|
@ -136,7 +129,6 @@ func (server *Server) Start() error {
|
|||
|
||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||
endpointGroupHandler.DataStore = server.DataStore
|
||||
endpointGroupHandler.AuthorizationService = authorizationService
|
||||
|
||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||
endpointProxyHandler.DataStore = server.DataStore
|
||||
|
@ -147,11 +139,6 @@ func (server *Server) Start() error {
|
|||
|
||||
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)
|
||||
registryHandler.DataStore = server.DataStore
|
||||
registryHandler.FileService = server.FileService
|
||||
|
@ -161,7 +148,6 @@ func (server *Server) Start() error {
|
|||
resourceControlHandler.DataStore = server.DataStore
|
||||
|
||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||
settingsHandler.AuthorizationService = authorizationService
|
||||
settingsHandler.DataStore = server.DataStore
|
||||
settingsHandler.FileService = server.FileService
|
||||
settingsHandler.JWTService = server.JWTService
|
||||
|
@ -181,11 +167,9 @@ func (server *Server) Start() error {
|
|||
|
||||
var teamHandler = teams.NewHandler(requestBouncer)
|
||||
teamHandler.DataStore = server.DataStore
|
||||
teamHandler.AuthorizationService = authorizationService
|
||||
|
||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.DataStore = server.DataStore
|
||||
teamMembershipHandler.AuthorizationService = authorizationService
|
||||
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
|
||||
|
@ -202,7 +186,6 @@ func (server *Server) Start() error {
|
|||
var userHandler = users.NewHandler(requestBouncer, rateLimiter)
|
||||
userHandler.DataStore = server.DataStore
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
userHandler.AuthorizationService = authorizationService
|
||||
|
||||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||
websocketHandler.DataStore = server.DataStore
|
||||
|
@ -229,7 +212,6 @@ func (server *Server) Start() error {
|
|||
EndpointProxyHandler: endpointProxyHandler,
|
||||
FileHandler: fileHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
ExtensionHandler: extensionHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
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.
|
||||
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)
|
||||
|
||||
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) {
|
||||
authorizedStacks = append(authorizedStacks, stack)
|
||||
}
|
||||
|
|
|
@ -412,7 +412,6 @@ func DefaultPortainerAuthorizations() portainer.Authorizations {
|
|||
portainer.OperationPortainerEndpointInspect: true,
|
||||
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||
portainer.OperationPortainerExtensionList: true,
|
||||
portainer.OperationPortainerMOTD: true,
|
||||
portainer.OperationPortainerRegistryList: 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.
|
||||
func (service *Service) UpdateUsersAuthorizations() error {
|
||||
users, err := service.dataStore.User().Users()
|
||||
|
|
|
@ -14,13 +14,6 @@ type (
|
|||
// AgentPlatform represents a platform type for an Agent
|
||||
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 int
|
||||
|
||||
|
@ -287,7 +280,7 @@ type (
|
|||
EdgeStacks map[EdgeStackID]bool
|
||||
}
|
||||
|
||||
// Extension represents a Portainer extension
|
||||
// Extension represents a deprecated Portainer extension
|
||||
Extension struct {
|
||||
ID ExtensionID `json:"Id"`
|
||||
Enabled bool `json:"Enabled"`
|
||||
|
@ -724,10 +717,13 @@ type (
|
|||
|
||||
// User represents a user account
|
||||
User struct {
|
||||
ID UserID `json:"Id"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
Role UserRole `json:"Role"`
|
||||
ID UserID `json:"Id"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
Role UserRole `json:"Role"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 25
|
||||
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
|
||||
EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"`
|
||||
}
|
||||
|
@ -807,7 +803,6 @@ type (
|
|||
Endpoint() EndpointService
|
||||
EndpointGroup() EndpointGroupService
|
||||
EndpointRelation() EndpointRelationService
|
||||
Extension() ExtensionService
|
||||
Registry() RegistryService
|
||||
ResourceControl() ResourceControlService
|
||||
Role() RoleService
|
||||
|
@ -899,24 +894,6 @@ type (
|
|||
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 interface {
|
||||
GetFileContent(filePath string) ([]byte, error)
|
||||
|
@ -941,7 +918,6 @@ type (
|
|||
ClearEdgeJobTaskLogs(edgeJobID, taskID string) error
|
||||
GetEdgeJobTaskLogFileContent(edgeJobID, taskID string) (string, error)
|
||||
StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error
|
||||
ExtractExtensionArchive(data []byte) error
|
||||
GetBinaryFolder() string
|
||||
StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error)
|
||||
GetCustomTemplateProjectPath(identifier string) string
|
||||
|
@ -1143,8 +1119,6 @@ const (
|
|||
MessageOfTheDayURL = AssetsServerURL + "/motd.json"
|
||||
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
||||
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-" + APIVersion + ".json"
|
||||
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
||||
SupportProductsURL = AssetsServerURL + "/support.json"
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
|
@ -1164,12 +1138,8 @@ const (
|
|||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||
// to be used when communicating with an agent
|
||||
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 = 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 = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json"
|
||||
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
|
||||
|
@ -1254,16 +1224,6 @@ const (
|
|||
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 (
|
||||
_ JobType = iota
|
||||
// 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'
|
||||
schema:
|
||||
$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:
|
||||
get:
|
||||
tags:
|
||||
|
|
|
@ -35,7 +35,6 @@ angular.module('portainer', [
|
|||
'portainer.docker',
|
||||
'portainer.kubernetes',
|
||||
'portainer.edge',
|
||||
'portainer.extensions',
|
||||
'portainer.integrations',
|
||||
'rzModule',
|
||||
'moment-picker',
|
||||
|
|
|
@ -10,7 +10,6 @@ angular
|
|||
.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints')
|
||||
.constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups')
|
||||
.constant('API_ENDPOINT_MOTD', 'api/motd')
|
||||
.constant('API_ENDPOINT_EXTENSIONS', 'api/extensions')
|
||||
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
|
||||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
|
||||
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
|
||||
|
|
|
@ -30,7 +30,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
'SettingsService',
|
||||
'PluginService',
|
||||
'HttpRequestHelper',
|
||||
'ExtensionService',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
|
@ -56,8 +55,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
SystemService,
|
||||
SettingsService,
|
||||
PluginService,
|
||||
HttpRequestHelper,
|
||||
ExtensionService
|
||||
HttpRequestHelper
|
||||
) {
|
||||
$scope.create = create;
|
||||
|
||||
|
@ -649,7 +647,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$scope.showDeviceMapping = await shouldShowDevices();
|
||||
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
|
||||
$scope.isAdminOrEndpointAdmin = await checkIfAdminOrEndpointAdmin();
|
||||
$scope.isAdminOrEndpointAdmin = Authentication.isAdmin();
|
||||
|
||||
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() {
|
||||
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return allowDeviceMappingForRegularUsers || isAdminOrEndpointAdmin();
|
||||
return allowDeviceMappingForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
async function checkIfContainerCapabilitiesEnabled() {
|
||||
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return allowContainerCapabilitiesForRegularUsers || isAdminOrEndpointAdmin();
|
||||
}
|
||||
|
||||
async function checkIfAdminOrEndpointAdmin() {
|
||||
if (Authentication.isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||
return rbacEnabled ? Authentication.hasAuthorizations(['EndpointResourcesAccess']) : false;
|
||||
return allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
initView();
|
||||
|
|
|
@ -9,7 +9,6 @@ angular.module('portainer.docker').controller('ContainerController', [
|
|||
'$transition$',
|
||||
'$filter',
|
||||
'$async',
|
||||
'ExtensionService',
|
||||
'Commit',
|
||||
'ContainerHelper',
|
||||
'ContainerService',
|
||||
|
@ -30,7 +29,6 @@ angular.module('portainer.docker').controller('ContainerController', [
|
|||
$transition$,
|
||||
$filter,
|
||||
$async,
|
||||
ExtensionService,
|
||||
Commit,
|
||||
ContainerHelper,
|
||||
ContainerService,
|
||||
|
@ -115,9 +113,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
|||
!allowHostNamespaceForRegularUsers ||
|
||||
!allowPrivilegedModeForRegularUsers;
|
||||
|
||||
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then((rbacEnabled) => {
|
||||
$scope.displayRecreateButton = !inSwarm && !autoRemove && (settingRestrictsRegularUsers || rbacEnabled ? admin : true);
|
||||
});
|
||||
$scope.displayRecreateButton = !inSwarm && !autoRemove && (admin || !settingRestrictsRegularUsers);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container info');
|
||||
|
|
|
@ -12,7 +12,6 @@ angular.module('portainer.docker').controller('DashboardController', [
|
|||
'EndpointService',
|
||||
'Notifications',
|
||||
'EndpointProvider',
|
||||
'ExtensionService',
|
||||
'StateManager',
|
||||
function (
|
||||
$scope,
|
||||
|
@ -28,7 +27,6 @@ angular.module('portainer.docker').controller('DashboardController', [
|
|||
EndpointService,
|
||||
Notifications,
|
||||
EndpointProvider,
|
||||
ExtensionService,
|
||||
StateManager
|
||||
) {
|
||||
$scope.dismissInformationPanel = function (id) {
|
||||
|
@ -75,13 +73,7 @@ angular.module('portainer.docker').controller('DashboardController', [
|
|||
const isAdmin = Authentication.isAdmin();
|
||||
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
if (isAdmin || allowStackManagementForRegularUsers) {
|
||||
return true;
|
||||
}
|
||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||
if (rbacEnabled) {
|
||||
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
||||
}
|
||||
return isAdmin || allowStackManagementForRegularUsers;
|
||||
}
|
||||
|
||||
initView();
|
||||
|
|
|
@ -33,7 +33,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||
'SettingsService',
|
||||
'WebhookService',
|
||||
'EndpointProvider',
|
||||
'ExtensionService',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
|
@ -59,8 +58,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||
NodeService,
|
||||
SettingsService,
|
||||
WebhookService,
|
||||
EndpointProvider,
|
||||
ExtensionService
|
||||
EndpointProvider
|
||||
) {
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
|
@ -592,15 +590,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||
const settings = await SettingsService.publicSettings();
|
||||
const { AllowBindMountsForRegularUsers } = settings;
|
||||
|
||||
if (isAdmin || AllowBindMountsForRegularUsers) {
|
||||
return true;
|
||||
}
|
||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||
if (rbacEnabled) {
|
||||
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
||||
}
|
||||
|
||||
return false;
|
||||
return isAdmin || AllowBindMountsForRegularUsers;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -9,8 +9,7 @@ angular.module('portainer.docker').controller('VolumesController', [
|
|||
'HttpRequestHelper',
|
||||
'EndpointProvider',
|
||||
'Authentication',
|
||||
'ExtensionService',
|
||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ExtensionService) {
|
||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication) {
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (volume) {
|
||||
|
@ -71,16 +70,8 @@ angular.module('portainer.docker').controller('VolumesController', [
|
|||
function initView() {
|
||||
getVolumes();
|
||||
|
||||
$scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy;
|
||||
|
||||
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then(function success(extensionEnabled) {
|
||||
if (!extensionEnabled) {
|
||||
var isAdmin = Authentication.isAdmin();
|
||||
if (!$scope.applicationState.application.enableVolumeBrowserForNonAdminUsers && !isAdmin) {
|
||||
$scope.showBrowseAction = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
$scope.showBrowseAction =
|
||||
$scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || $scope.applicationState.application.enableVolumeBrowserForNonAdminUsers);
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
@ -96,11 +81,6 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<!--
|
||||
rbac-enabled="ctrl.rbacEnabled && ctrl.entityType !== 'registry'"
|
||||
roles="ctrl.roles"
|
||||
update-action="ctrl.updateAction"
|
||||
-->
|
||||
<access-datatable
|
||||
ng-if="ctrl.authorizedUsersAndTeams"
|
||||
title-text="Access"
|
||||
|
|
|
@ -35,9 +35,6 @@ class KubernetesResourcePoolAccessController {
|
|||
/**
|
||||
* 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() {
|
||||
this.state = {
|
||||
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 = {
|
||||
name: 'portainer.registries',
|
||||
url: '/registries',
|
||||
|
@ -469,8 +447,6 @@ angular.module('portainer.app', ['portainer.oauth']).config([
|
|||
$stateRegistryProvider.register(init);
|
||||
$stateRegistryProvider.register(initEndpoint);
|
||||
$stateRegistryProvider.register(initAdmin);
|
||||
$stateRegistryProvider.register(extensions);
|
||||
$stateRegistryProvider.register(extension);
|
||||
$stateRegistryProvider.register(registries);
|
||||
$stateRegistryProvider.register(registry);
|
||||
$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)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</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 class="searchBar">
|
||||
<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>
|
||||
</a>
|
||||
</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>
|
||||
</thead>
|
||||
<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>
|
||||
</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 ng-if="!$ctrl.dataset">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
|
|
|
@ -11,7 +11,6 @@ angular.module('portainer.app').component('accessDatatable', {
|
|||
removeAction: '<',
|
||||
updateAction: '<',
|
||||
reverseOrder: '<',
|
||||
rbacEnabled: '<',
|
||||
inheritFrom: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -27,17 +27,7 @@
|
|||
</span>
|
||||
</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 -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
@ -67,10 +57,8 @@
|
|||
title-icon="fa-user-lock"
|
||||
table-key="{{ 'access_' + ctrl.entityType }}"
|
||||
order-by="Name"
|
||||
rbac-enabled="ctrl.rbacEnabled && ctrl.entityType !== 'registry'"
|
||||
inherit-from="ctrl.inheritFrom"
|
||||
dataset="ctrl.authorizedUsersAndTeams"
|
||||
roles="ctrl.roles"
|
||||
update-action="ctrl.updateAction"
|
||||
remove-action="ctrl.unauthorizeAccess"
|
||||
>
|
||||
|
|
|
@ -4,11 +4,9 @@ import angular from 'angular';
|
|||
|
||||
class PorAccessManagementController {
|
||||
/* @ngInject */
|
||||
constructor(Notifications, ExtensionService, AccessService, RoleService) {
|
||||
constructor(Notifications, AccessService) {
|
||||
this.Notifications = Notifications;
|
||||
this.ExtensionService = ExtensionService;
|
||||
this.AccessService = AccessService;
|
||||
this.RoleService = RoleService;
|
||||
|
||||
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
||||
this.updateAction = this.updateAction.bind(this);
|
||||
|
@ -31,11 +29,10 @@ class PorAccessManagementController {
|
|||
const entity = this.accessControlledEntity;
|
||||
const oldUserAccessPolicies = entity.UserAccessPolicies;
|
||||
const oldTeamAccessPolicies = entity.TeamAccessPolicies;
|
||||
const selectedRoleId = this.rbacEnabled ? this.formValues.selectedRole.Id : 0;
|
||||
const selectedUserAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'user');
|
||||
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.TeamAccessPolicies = accessPolicies.teamAccessPolicies;
|
||||
this.updateAccess();
|
||||
|
@ -56,18 +53,7 @@ class PorAccessManagementController {
|
|||
try {
|
||||
const entity = this.accessControlledEntity;
|
||||
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);
|
||||
this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc');
|
||||
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';
|
||||
var service = {};
|
||||
|
||||
function _getRole(roles, roleId) {
|
||||
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) {
|
||||
function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies) {
|
||||
var availableAccesses = [];
|
||||
var authorizedAccesses = [];
|
||||
|
||||
|
@ -30,14 +22,11 @@ angular.module('portainer.app').factory('AccessService', [
|
|||
const inherited = inheritedPolicies && inheritedPolicies[access.Id];
|
||||
|
||||
if (authorized && inherited) {
|
||||
access.Role = _getRole(roles, authorizedPolicies[access.Id].RoleId);
|
||||
access.Override = true;
|
||||
authorizedAccesses.push(access);
|
||||
} else if (authorized && !inherited) {
|
||||
access.Role = _getRole(roles, authorizedPolicies[access.Id].RoleId);
|
||||
authorizedAccesses.push(access);
|
||||
} else if (!authorized && inherited) {
|
||||
access.Role = _getRole(roles, inheritedPolicies[access.Id].RoleId);
|
||||
access.Inherited = true;
|
||||
authorizedAccesses.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();
|
||||
|
||||
$q.all({
|
||||
|
@ -67,8 +56,8 @@ angular.module('portainer.app').factory('AccessService', [
|
|||
return new TeamAccessViewModel(team);
|
||||
});
|
||||
|
||||
var userAccessData = _mapAccessData(userAccesses, authorizedUserPolicies, inheritedUserPolicies, roles);
|
||||
var teamAccessData = _mapAccessData(teamAccesses, authorizedTeamPolicies, inheritedTeamPolicies, roles);
|
||||
var userAccessData = _mapAccessData(userAccesses, authorizedUserPolicies, inheritedUserPolicies);
|
||||
var teamAccessData = _mapAccessData(teamAccesses, authorizedTeamPolicies, inheritedTeamPolicies);
|
||||
|
||||
var accessData = {
|
||||
availableUsersAndTeams: userAccessData.available.concat(teamAccessData.available),
|
||||
|
@ -84,7 +73,7 @@ angular.module('portainer.app').factory('AccessService', [
|
|||
return deferred.promise;
|
||||
}
|
||||
|
||||
async function accessesAsync(entity, parent, roles) {
|
||||
async function accessesAsync(entity, parent) {
|
||||
try {
|
||||
if (!entity) {
|
||||
throw { msg: 'Unable to retrieve accesses' };
|
||||
|
@ -101,14 +90,14 @@ angular.module('portainer.app').factory('AccessService', [
|
|||
if (parent && !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) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function accesses(entity, parent, roles) {
|
||||
return $async(accessesAsync, entity, parent, roles);
|
||||
function accesses(entity, parent) {
|
||||
return $async(accessesAsync, entity, parent);
|
||||
}
|
||||
|
||||
service.accesses = accesses;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue