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 module
pull/4196/head
Chaim Lev-Ari 2020-08-11 08:41:37 +03:00 committed by GitHub
parent 8629738e34
commit 9d18d47194
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
117 changed files with 98 additions and 3487 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, &registry)
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, &registry)
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()

View File

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

View File

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

View File

@ -35,7 +35,6 @@ angular.module('portainer', [
'portainer.docker',
'portainer.kubernetes',
'portainer.edge',
'portainer.extensions',
'portainer.integrations',
'rzModule',
'moment-picker',

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}
},
]);

View File

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

View File

@ -1 +0,0 @@
angular.module('portainer.extensions', ['portainer.extensions.rbac']);

View File

@ -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);
},
]);

View File

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

View File

@ -1,5 +0,0 @@
angular.module('portainer.app').component('accessViewer', {
templateUrl: './accessViewer.html',
controller: 'AccessViewerController',
controllerAs: 'ctrl',
});

View File

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

View File

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

View File

@ -1,11 +0,0 @@
angular.module('portainer.app').component('accessViewerDatatable', {
templateUrl: './accessViewerDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
tableKey: '@',
orderBy: '@',
dataset: '<',
},
});

View File

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

View File

@ -1,13 +0,0 @@
angular.module('portainer.extensions.rbac').component('rolesDatatable', {
templateUrl: './rolesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
rbacEnabled: '<',
},
});

View File

@ -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);
},
};
},
]);

View File

@ -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);
},
};
},
]);

View File

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

View File

@ -1,6 +0,0 @@
export function RoleViewModel(data) {
this.ID = data.Id;
this.Name = data.Name;
this.Description = data.Description;
this.Authorizations = data.Authorizations;
}

View File

@ -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' } },
}
);
},
]);

View File

@ -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;
},
]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ angular.module('portainer.app').component('accessDatatable', {
removeAction: '<',
updateAction: '<',
reverseOrder: '<',
rbacEnabled: '<',
inheritFrom: '<',
},
});

View File

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

View File

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

View File

@ -1,8 +0,0 @@
angular.module('portainer.app').component('extensionItem', {
templateUrl: './extensionItem.html',
controller: 'ExtensionItemController',
bindings: {
model: '<',
currentDate: '<',
},
});

View File

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

View File

@ -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 });
}
}
},
]);

View File

@ -1,7 +0,0 @@
angular.module('portainer.app').component('extensionList', {
templateUrl: './extensionList.html',
bindings: {
extensions: '<',
currentDate: '<',
},
});

View File

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

View File

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

View File

@ -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' } },
}
);
},
]);

View File

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