1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

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
This commit is contained in:
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: