mirror of https://github.com/portainer/portainer
feat(endpoint-groups): add endpoint-groups (#1837)
parent
2ffcb946b1
commit
1162549209
|
@ -20,6 +20,7 @@ type Store struct {
|
||||||
TeamService *TeamService
|
TeamService *TeamService
|
||||||
TeamMembershipService *TeamMembershipService
|
TeamMembershipService *TeamMembershipService
|
||||||
EndpointService *EndpointService
|
EndpointService *EndpointService
|
||||||
|
EndpointGroupService *EndpointGroupService
|
||||||
ResourceControlService *ResourceControlService
|
ResourceControlService *ResourceControlService
|
||||||
VersionService *VersionService
|
VersionService *VersionService
|
||||||
SettingsService *SettingsService
|
SettingsService *SettingsService
|
||||||
|
@ -38,6 +39,7 @@ const (
|
||||||
teamBucketName = "teams"
|
teamBucketName = "teams"
|
||||||
teamMembershipBucketName = "team_membership"
|
teamMembershipBucketName = "team_membership"
|
||||||
endpointBucketName = "endpoints"
|
endpointBucketName = "endpoints"
|
||||||
|
endpointGroupBucketName = "endpoint_groups"
|
||||||
resourceControlBucketName = "resource_control"
|
resourceControlBucketName = "resource_control"
|
||||||
settingsBucketName = "settings"
|
settingsBucketName = "settings"
|
||||||
registryBucketName = "registries"
|
registryBucketName = "registries"
|
||||||
|
@ -53,6 +55,7 @@ func NewStore(storePath string) (*Store, error) {
|
||||||
TeamService: &TeamService{},
|
TeamService: &TeamService{},
|
||||||
TeamMembershipService: &TeamMembershipService{},
|
TeamMembershipService: &TeamMembershipService{},
|
||||||
EndpointService: &EndpointService{},
|
EndpointService: &EndpointService{},
|
||||||
|
EndpointGroupService: &EndpointGroupService{},
|
||||||
ResourceControlService: &ResourceControlService{},
|
ResourceControlService: &ResourceControlService{},
|
||||||
VersionService: &VersionService{},
|
VersionService: &VersionService{},
|
||||||
SettingsService: &SettingsService{},
|
SettingsService: &SettingsService{},
|
||||||
|
@ -64,6 +67,7 @@ func NewStore(storePath string) (*Store, error) {
|
||||||
store.TeamService.store = store
|
store.TeamService.store = store
|
||||||
store.TeamMembershipService.store = store
|
store.TeamMembershipService.store = store
|
||||||
store.EndpointService.store = store
|
store.EndpointService.store = store
|
||||||
|
store.EndpointGroupService.store = store
|
||||||
store.ResourceControlService.store = store
|
store.ResourceControlService.store = store
|
||||||
store.VersionService.store = store
|
store.VersionService.store = store
|
||||||
store.SettingsService.store = store
|
store.SettingsService.store = store
|
||||||
|
@ -94,7 +98,7 @@ func (store *Store) Open() error {
|
||||||
store.db = db
|
store.db = db
|
||||||
|
|
||||||
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
|
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
|
||||||
resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
|
endpointGroupBucketName, resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
|
||||||
registryBucketName, dockerhubBucketName, stackBucketName}
|
registryBucketName, dockerhubBucketName, stackBucketName}
|
||||||
|
|
||||||
return db.Update(func(tx *bolt.Tx) error {
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
@ -110,6 +114,28 @@ func (store *Store) Open() error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init creates the default data set.
|
||||||
|
func (store *Store) Init() error {
|
||||||
|
groups, err := store.EndpointGroupService.EndpointGroups()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groups) == 0 {
|
||||||
|
unassignedGroup := &portainer.EndpointGroup{
|
||||||
|
Name: "Unassigned",
|
||||||
|
Description: "Unassigned endpoints",
|
||||||
|
Labels: []portainer.Pair{},
|
||||||
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Close closes the BoltDB database.
|
// Close closes the BoltDB database.
|
||||||
func (store *Store) Close() error {
|
func (store *Store) Close() error {
|
||||||
if store.db != nil {
|
if store.db != nil {
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/bolt/internal"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EndpointGroupService represents a service for managing endpoint groups.
|
||||||
|
type EndpointGroupService struct {
|
||||||
|
store *Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndpointGroup returns an endpoint group by ID.
|
||||||
|
func (service *EndpointGroupService) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
|
||||||
|
var data []byte
|
||||||
|
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointGroupBucketName))
|
||||||
|
value := bucket.Get(internal.Itob(int(ID)))
|
||||||
|
if value == nil {
|
||||||
|
return portainer.ErrEndpointGroupNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
data = make([]byte, len(value))
|
||||||
|
copy(data, value)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpointGroup portainer.EndpointGroup
|
||||||
|
err = internal.UnmarshalEndpointGroup(data, &endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &endpointGroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndpointGroups return an array containing all the endpoint groups.
|
||||||
|
func (service *EndpointGroupService) EndpointGroups() ([]portainer.EndpointGroup, error) {
|
||||||
|
var endpointGroups = make([]portainer.EndpointGroup, 0)
|
||||||
|
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointGroupBucketName))
|
||||||
|
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
var endpointGroup portainer.EndpointGroup
|
||||||
|
err := internal.UnmarshalEndpointGroup(v, &endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
endpointGroups = append(endpointGroups, endpointGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpointGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
|
||||||
|
func (service *EndpointGroupService) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
|
||||||
|
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointGroupBucketName))
|
||||||
|
|
||||||
|
id, _ := bucket.NextSequence()
|
||||||
|
endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||||
|
|
||||||
|
data, err := internal.MarshalEndpointGroup(endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bucket.Put(internal.Itob(int(endpointGroup.ID)), data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEndpointGroup updates an endpoint group.
|
||||||
|
func (service *EndpointGroupService) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
|
||||||
|
data, err := internal.MarshalEndpointGroup(endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointGroupBucketName))
|
||||||
|
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEndpointGroup deletes an endpoint group.
|
||||||
|
func (service *EndpointGroupService) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
|
||||||
|
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointGroupBucketName))
|
||||||
|
err := bucket.Delete(internal.Itob(int(ID)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -47,6 +47,16 @@ func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
|
||||||
return json.Unmarshal(data, endpoint)
|
return json.Unmarshal(data, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalEndpointGroup encodes an endpoint group to binary format.
|
||||||
|
func MarshalEndpointGroup(group *portainer.EndpointGroup) ([]byte, error) {
|
||||||
|
return json.Marshal(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalEndpointGroup decodes an endpoint group from a binary data.
|
||||||
|
func UnmarshalEndpointGroup(data []byte, group *portainer.EndpointGroup) error {
|
||||||
|
return json.Unmarshal(data, group)
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalStack encodes a stack to binary format.
|
// MarshalStack encodes a stack to binary format.
|
||||||
func MarshalStack(stack *portainer.Stack) ([]byte, error) {
|
func MarshalStack(stack *portainer.Stack) ([]byte, error) {
|
||||||
return json.Marshal(stack)
|
return json.Marshal(stack)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
|
func (m *Migrator) updateEndpointsToVersion9() error {
|
||||||
|
legacyEndpoints, err := m.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range legacyEndpoints {
|
||||||
|
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||||
|
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -96,6 +96,14 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https: //github.com/portainer/portainer/issues/1396
|
||||||
|
if m.CurrentDBVersion < 9 {
|
||||||
|
err := m.updateEndpointsToVersion9()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -49,6 +49,11 @@ func initStore(dataStorePath string) *bolt.Store {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = store.Init()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
err = store.MigrateData()
|
err = store.MigrateData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -275,6 +280,7 @@ func main() {
|
||||||
TeamService: store.TeamService,
|
TeamService: store.TeamService,
|
||||||
TeamMembershipService: store.TeamMembershipService,
|
TeamMembershipService: store.TeamMembershipService,
|
||||||
EndpointService: store.EndpointService,
|
EndpointService: store.EndpointService,
|
||||||
|
EndpointGroupService: store.EndpointGroupService,
|
||||||
ResourceControlService: store.ResourceControlService,
|
ResourceControlService: store.ResourceControlService,
|
||||||
SettingsService: store.SettingsService,
|
SettingsService: store.SettingsService,
|
||||||
RegistryService: store.RegistryService,
|
RegistryService: store.RegistryService,
|
||||||
|
|
|
@ -28,7 +28,7 @@ const (
|
||||||
// TeamMembership errors.
|
// TeamMembership errors.
|
||||||
const (
|
const (
|
||||||
ErrTeamMembershipNotFound = Error("Team membership not found")
|
ErrTeamMembershipNotFound = Error("Team membership not found")
|
||||||
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team.")
|
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResourceControl errors.
|
// ResourceControl errors.
|
||||||
|
@ -44,6 +44,12 @@ const (
|
||||||
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Endpoint group errors.
|
||||||
|
const (
|
||||||
|
ErrEndpointGroupNotFound = Error("Endpoint group not found")
|
||||||
|
ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group")
|
||||||
|
)
|
||||||
|
|
||||||
// Registry errors.
|
// Registry errors.
|
||||||
const (
|
const (
|
||||||
ErrRegistryNotFound = Error("Registry not found")
|
ErrRegistryNotFound = Error("Registry not found")
|
||||||
|
|
|
@ -20,6 +20,7 @@ type DockerHandler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
}
|
}
|
||||||
|
@ -64,9 +65,17 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
|
if tokenData.Role != portainer.AdministratorRole {
|
||||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||||
return
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !security.AuthorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
|
|
|
@ -28,6 +28,7 @@ type EndpointHandler struct {
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
authorizeEndpointManagement bool
|
authorizeEndpointManagement bool
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
}
|
}
|
||||||
|
@ -75,6 +76,7 @@ type (
|
||||||
Name string `valid:"-"`
|
Name string `valid:"-"`
|
||||||
URL string `valid:"-"`
|
URL string `valid:"-"`
|
||||||
PublicURL string `valid:"-"`
|
PublicURL string `valid:"-"`
|
||||||
|
GroupID int `valid:"-"`
|
||||||
TLS bool `valid:"-"`
|
TLS bool `valid:"-"`
|
||||||
TLSSkipVerify bool `valid:"-"`
|
TLSSkipVerify bool `valid:"-"`
|
||||||
TLSSkipClientVerify bool `valid:"-"`
|
TLSSkipClientVerify bool `valid:"-"`
|
||||||
|
@ -84,6 +86,7 @@ type (
|
||||||
name string
|
name string
|
||||||
url string
|
url string
|
||||||
publicURL string
|
publicURL string
|
||||||
|
groupID int
|
||||||
useTLS bool
|
useTLS bool
|
||||||
skipTLSServerVerification bool
|
skipTLSServerVerification bool
|
||||||
skipTLSClientVerification bool
|
skipTLSClientVerification bool
|
||||||
|
@ -107,7 +110,13 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredEndpoints, err := security.FilterEndpoints(endpoints, securityContext)
|
groups, err := handler.EndpointGroupService.EndpointGroups()
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredEndpoints, err := security.FilterEndpoints(endpoints, groups, securityContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
@ -154,6 +163,7 @@ func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPa
|
||||||
endpoint := &portainer.Endpoint{
|
endpoint := &portainer.Endpoint{
|
||||||
Name: payload.name,
|
Name: payload.name,
|
||||||
URL: payload.url,
|
URL: payload.url,
|
||||||
|
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||||
PublicURL: payload.publicURL,
|
PublicURL: payload.publicURL,
|
||||||
TLSConfig: portainer.TLSConfiguration{
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLS: payload.useTLS,
|
TLS: payload.useTLS,
|
||||||
|
@ -225,6 +235,7 @@ func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPay
|
||||||
endpoint := &portainer.Endpoint{
|
endpoint := &portainer.Endpoint{
|
||||||
Name: payload.name,
|
Name: payload.name,
|
||||||
URL: payload.url,
|
URL: payload.url,
|
||||||
|
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||||
PublicURL: payload.publicURL,
|
PublicURL: payload.publicURL,
|
||||||
TLSConfig: portainer.TLSConfiguration{
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLS: false,
|
TLS: false,
|
||||||
|
@ -259,6 +270,17 @@ func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload,
|
||||||
return nil, ErrInvalidRequestFormat
|
return nil, ErrInvalidRequestFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawGroupID := r.FormValue("GroupID")
|
||||||
|
if rawGroupID == "" {
|
||||||
|
payload.groupID = 1
|
||||||
|
} else {
|
||||||
|
groupID, err := strconv.Atoi(rawGroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payload.groupID = groupID
|
||||||
|
}
|
||||||
|
|
||||||
payload.useTLS = r.FormValue("TLS") == "true"
|
payload.useTLS = r.FormValue("TLS") == "true"
|
||||||
|
|
||||||
if payload.useTLS {
|
if payload.useTLS {
|
||||||
|
@ -439,6 +461,10 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
||||||
endpoint.PublicURL = req.PublicURL
|
endpoint.PublicURL = req.PublicURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.GroupID != 0 {
|
||||||
|
endpoint.GroupID = portainer.EndpointGroupID(req.GroupID)
|
||||||
|
}
|
||||||
|
|
||||||
folder := strconv.Itoa(int(endpoint.ID))
|
folder := strconv.Itoa(int(endpoint.ID))
|
||||||
if req.TLS {
|
if req.TLS {
|
||||||
endpoint.TLSConfig.TLS = true
|
endpoint.TLSConfig.TLS = true
|
||||||
|
|
|
@ -0,0 +1,364 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
httperror "github.com/portainer/portainer/http/error"
|
||||||
|
"github.com/portainer/portainer/http/security"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EndpointGroupHandler represents an HTTP API handler for managing endpoint groups.
|
||||||
|
type EndpointGroupHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
EndpointService portainer.EndpointService
|
||||||
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEndpointGroupHandler returns a new instance of EndpointGroupHandler.
|
||||||
|
func NewEndpointGroupHandler(bouncer *security.RequestBouncer) *EndpointGroupHandler {
|
||||||
|
h := &EndpointGroupHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
}
|
||||||
|
h.Handle("/endpoint_groups",
|
||||||
|
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpointGroups))).Methods(http.MethodPost)
|
||||||
|
h.Handle("/endpoint_groups",
|
||||||
|
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpointGroups))).Methods(http.MethodGet)
|
||||||
|
h.Handle("/endpoint_groups/{id}",
|
||||||
|
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpointGroup))).Methods(http.MethodGet)
|
||||||
|
h.Handle("/endpoint_groups/{id}",
|
||||||
|
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointGroup))).Methods(http.MethodPut)
|
||||||
|
h.Handle("/endpoint_groups/{id}/access",
|
||||||
|
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointGroupAccess))).Methods(http.MethodPut)
|
||||||
|
h.Handle("/endpoint_groups/{id}",
|
||||||
|
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpointGroup))).Methods(http.MethodDelete)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
postEndpointGroupsResponse struct {
|
||||||
|
ID int `json:"Id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
postEndpointGroupsRequest struct {
|
||||||
|
Name string `valid:"required"`
|
||||||
|
Description string `valid:"-"`
|
||||||
|
Labels []portainer.Pair `valid:""`
|
||||||
|
AssociatedEndpoints []portainer.EndpointID `valid:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
putEndpointGroupAccessRequest struct {
|
||||||
|
AuthorizedUsers []int `valid:"-"`
|
||||||
|
AuthorizedTeams []int `valid:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
putEndpointGroupsRequest struct {
|
||||||
|
Name string `valid:"-"`
|
||||||
|
Description string `valid:"-"`
|
||||||
|
Labels []portainer.Pair `valid:""`
|
||||||
|
AssociatedEndpoints []portainer.EndpointID `valid:""`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleGetEndpointGroups handles GET requests on /endpoint_groups
|
||||||
|
func (handler *EndpointGroupHandler) handleGetEndpointGroups(w http.ResponseWriter, r *http.Request) {
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredEndpointGroups, err := security.FilterEndpointGroups(endpointGroups, securityContext)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, filteredEndpointGroups, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostEndpointGroups handles POST requests on /endpoint_groups
|
||||||
|
func (handler *EndpointGroupHandler) handlePostEndpointGroups(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req postEndpointGroupsRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointGroup := &portainer.EndpointGroup{
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
Labels: req.Labels,
|
||||||
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := handler.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||||
|
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, req.AssociatedEndpoints)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, &postEndpointGroupsResponse{ID: int(endpointGroup.ID)}, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetEndpointGroup handles GET requests on /endpoint_groups/:id
|
||||||
|
func (handler *EndpointGroupHandler) handleGetEndpointGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
endpointGroupID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||||
|
if err == portainer.ErrEndpointGroupNotFound {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, endpointGroup, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePutEndpointGroupAccess handles PUT requests on /endpoint_groups/:id/access
|
||||||
|
func (handler *EndpointGroupHandler) handlePutEndpointGroupAccess(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
endpointGroupID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req putEndpointGroupAccessRequest
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||||
|
if err == portainer.ErrEndpointGroupNotFound {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.AuthorizedUsers != nil {
|
||||||
|
authorizedUserIDs := []portainer.UserID{}
|
||||||
|
for _, value := range req.AuthorizedUsers {
|
||||||
|
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||||
|
}
|
||||||
|
endpointGroup.AuthorizedUsers = authorizedUserIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.AuthorizedTeams != nil {
|
||||||
|
authorizedTeamIDs := []portainer.TeamID{}
|
||||||
|
for _, value := range req.AuthorizedTeams {
|
||||||
|
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||||
|
}
|
||||||
|
endpointGroup.AuthorizedTeams = authorizedTeamIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePutEndpointGroup handles PUT requests on /endpoint_groups/:id
|
||||||
|
func (handler *EndpointGroupHandler) handlePutEndpointGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
endpointGroupID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req putEndpointGroupsRequest
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
groupID := portainer.EndpointGroupID(endpointGroupID)
|
||||||
|
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(groupID)
|
||||||
|
if err == portainer.ErrEndpointGroupNotFound {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != "" {
|
||||||
|
endpointGroup.Name = req.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Description != "" {
|
||||||
|
endpointGroup.Description = req.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointGroup.Labels = req.Labels
|
||||||
|
|
||||||
|
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := handler.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
err = handler.updateEndpointGroup(endpoint, groupID, req.AssociatedEndpoints)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *EndpointGroupHandler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||||
|
if endpoint.GroupID == groupID {
|
||||||
|
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
|
||||||
|
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||||
|
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *EndpointGroupHandler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
|
||||||
|
for _, id := range associatedEndpoints {
|
||||||
|
if id == endpoint.ID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||||
|
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *EndpointGroupHandler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||||
|
for _, id := range associatedEndpoints {
|
||||||
|
|
||||||
|
if id == endpoint.ID {
|
||||||
|
endpoint.GroupID = groupID
|
||||||
|
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDeleteEndpointGroup handles DELETE requests on /endpoint_groups/:id
|
||||||
|
func (handler *EndpointGroupHandler) handleDeleteEndpointGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
endpointGroupID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpointGroupID == 1 {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveDefaultGroup, http.StatusForbidden, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
groupID := portainer.EndpointGroupID(endpointGroupID)
|
||||||
|
_, err = handler.EndpointGroupService.EndpointGroup(groupID)
|
||||||
|
if err == portainer.ErrEndpointGroupNotFound {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.EndpointGroupService.DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := handler.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if endpoint.GroupID == groupID {
|
||||||
|
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||||
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ type StoridgeHandler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
}
|
}
|
||||||
|
@ -64,9 +65,17 @@ func (handler *StoridgeHandler) proxyRequestsToStoridgeAPI(w http.ResponseWriter
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
|
if tokenData.Role != portainer.AdministratorRole {
|
||||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||||
return
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !security.AuthorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var storidgeExtension *portainer.EndpointExtension
|
var storidgeExtension *portainer.EndpointExtension
|
||||||
|
|
|
@ -19,6 +19,7 @@ type Handler struct {
|
||||||
TeamHandler *TeamHandler
|
TeamHandler *TeamHandler
|
||||||
TeamMembershipHandler *TeamMembershipHandler
|
TeamMembershipHandler *TeamMembershipHandler
|
||||||
EndpointHandler *EndpointHandler
|
EndpointHandler *EndpointHandler
|
||||||
|
EndpointGroupHandler *EndpointGroupHandler
|
||||||
RegistryHandler *RegistryHandler
|
RegistryHandler *RegistryHandler
|
||||||
DockerHubHandler *DockerHubHandler
|
DockerHubHandler *DockerHubHandler
|
||||||
ExtensionHandler *ExtensionHandler
|
ExtensionHandler *ExtensionHandler
|
||||||
|
@ -51,6 +52,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
||||||
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"):
|
||||||
|
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(r.URL.Path, "/docker/"):
|
case strings.Contains(r.URL.Path, "/docker/"):
|
||||||
|
|
|
@ -124,34 +124,37 @@ func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedReques
|
||||||
|
|
||||||
// AuthorizedEndpointAccess ensure that the user can access the specified endpoint.
|
// AuthorizedEndpointAccess ensure that the user can access the specified endpoint.
|
||||||
// It will check if the user is part of the authorized users or part of a team that is
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
|
// listed in the authorized teams of the endpoint and the associated group.
|
||||||
|
func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
|
groupAccess := authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
||||||
|
if !groupAccess {
|
||||||
|
return authorizedAccess(userID, memberships, endpoint.AuthorizedUsers, endpoint.AuthorizedTeams)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
|
||||||
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams.
|
// listed in the authorized teams.
|
||||||
func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
func AuthorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
||||||
if authorizedUserID == userID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, membership := range memberships {
|
|
||||||
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
|
|
||||||
if membership.TeamID == authorizedTeamID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedRegistryAccess ensure that the user can access the specified registry.
|
// AuthorizedRegistryAccess ensure that the user can access the specified registry.
|
||||||
// It will check if the user is part of the authorized users or part of a team that is
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams.
|
// listed in the authorized teams.
|
||||||
func AuthorizedRegistryAccess(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
func AuthorizedRegistryAccess(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
for _, authorizedUserID := range registry.AuthorizedUsers {
|
return authorizedAccess(userID, memberships, registry.AuthorizedUsers, registry.AuthorizedTeams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, authorizedUsers []portainer.UserID, authorizedTeams []portainer.TeamID) bool {
|
||||||
|
for _, authorizedUserID := range authorizedUsers {
|
||||||
if authorizedUserID == userID {
|
if authorizedUserID == userID {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, membership := range memberships {
|
for _, membership := range memberships {
|
||||||
for _, authorizedTeamID := range registry.AuthorizedTeams {
|
for _, authorizedTeamID := range authorizedTeams {
|
||||||
if membership.TeamID == authorizedTeamID {
|
if membership.TeamID == authorizedTeamID {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,15 +79,17 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterEndpoints filters endpoints based on user role and team memberships.
|
// FilterEndpoints filters endpoints based on user role and team memberships.
|
||||||
// Non administrator users only have access to authorized endpoints.
|
// Non administrator users only have access to authorized endpoints (can be inherited via endoint groups).
|
||||||
func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestContext) ([]portainer.Endpoint, error) {
|
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) ([]portainer.Endpoint, error) {
|
||||||
filteredEndpoints := endpoints
|
filteredEndpoints := endpoints
|
||||||
|
|
||||||
if !context.IsAdmin {
|
if !context.IsAdmin {
|
||||||
filteredEndpoints = make([]portainer.Endpoint, 0)
|
filteredEndpoints = make([]portainer.Endpoint, 0)
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if AuthorizedEndpointAccess(&endpoint, context.UserID, context.UserMemberships) {
|
endpointGroup := getAssociatedGroup(&endpoint, groups)
|
||||||
|
|
||||||
|
if AuthorizedEndpointAccess(&endpoint, endpointGroup, context.UserID, context.UserMemberships) {
|
||||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,3 +97,30 @@ func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestC
|
||||||
|
|
||||||
return filteredEndpoints, nil
|
return filteredEndpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilterEndpointGroups filters endpoint groups based on user role and team memberships.
|
||||||
|
// Non administrator users only have access to authorized endpoint groups.
|
||||||
|
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) ([]portainer.EndpointGroup, error) {
|
||||||
|
filteredEndpointGroups := endpointGroups
|
||||||
|
|
||||||
|
if !context.IsAdmin {
|
||||||
|
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||||
|
|
||||||
|
for _, group := range endpointGroups {
|
||||||
|
if AuthorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
|
||||||
|
filteredEndpointGroups = append(filteredEndpointGroups, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredEndpointGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAssociatedGroup(endpoint *portainer.Endpoint, groups []portainer.EndpointGroup) *portainer.EndpointGroup {
|
||||||
|
for _, group := range groups {
|
||||||
|
if group.ID == endpoint.GroupID {
|
||||||
|
return &group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ type Server struct {
|
||||||
TeamService portainer.TeamService
|
TeamService portainer.TeamService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
ResourceControlService portainer.ResourceControlService
|
ResourceControlService portainer.ResourceControlService
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
|
@ -72,14 +73,19 @@ func (server *Server) Start() error {
|
||||||
templatesHandler.SettingsService = server.SettingsService
|
templatesHandler.SettingsService = server.SettingsService
|
||||||
var dockerHandler = handler.NewDockerHandler(requestBouncer)
|
var dockerHandler = handler.NewDockerHandler(requestBouncer)
|
||||||
dockerHandler.EndpointService = server.EndpointService
|
dockerHandler.EndpointService = server.EndpointService
|
||||||
|
dockerHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
dockerHandler.TeamMembershipService = server.TeamMembershipService
|
dockerHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
dockerHandler.ProxyManager = proxyManager
|
dockerHandler.ProxyManager = proxyManager
|
||||||
var websocketHandler = handler.NewWebSocketHandler()
|
var websocketHandler = handler.NewWebSocketHandler()
|
||||||
websocketHandler.EndpointService = server.EndpointService
|
websocketHandler.EndpointService = server.EndpointService
|
||||||
var endpointHandler = handler.NewEndpointHandler(requestBouncer, server.EndpointManagement)
|
var endpointHandler = handler.NewEndpointHandler(requestBouncer, server.EndpointManagement)
|
||||||
endpointHandler.EndpointService = server.EndpointService
|
endpointHandler.EndpointService = server.EndpointService
|
||||||
|
endpointHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
endpointHandler.FileService = server.FileService
|
endpointHandler.FileService = server.FileService
|
||||||
endpointHandler.ProxyManager = proxyManager
|
endpointHandler.ProxyManager = proxyManager
|
||||||
|
var endpointGroupHandler = handler.NewEndpointGroupHandler(requestBouncer)
|
||||||
|
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
|
endpointGroupHandler.EndpointService = server.EndpointService
|
||||||
var registryHandler = handler.NewRegistryHandler(requestBouncer)
|
var registryHandler = handler.NewRegistryHandler(requestBouncer)
|
||||||
registryHandler.RegistryService = server.RegistryService
|
registryHandler.RegistryService = server.RegistryService
|
||||||
var dockerHubHandler = handler.NewDockerHubHandler(requestBouncer)
|
var dockerHubHandler = handler.NewDockerHubHandler(requestBouncer)
|
||||||
|
@ -102,6 +108,7 @@ func (server *Server) Start() error {
|
||||||
extensionHandler.ProxyManager = proxyManager
|
extensionHandler.ProxyManager = proxyManager
|
||||||
var storidgeHandler = extensions.NewStoridgeHandler(requestBouncer)
|
var storidgeHandler = extensions.NewStoridgeHandler(requestBouncer)
|
||||||
storidgeHandler.EndpointService = server.EndpointService
|
storidgeHandler.EndpointService = server.EndpointService
|
||||||
|
storidgeHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
storidgeHandler.TeamMembershipService = server.TeamMembershipService
|
storidgeHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
storidgeHandler.ProxyManager = proxyManager
|
storidgeHandler.ProxyManager = proxyManager
|
||||||
|
|
||||||
|
@ -111,6 +118,7 @@ func (server *Server) Start() error {
|
||||||
TeamHandler: teamHandler,
|
TeamHandler: teamHandler,
|
||||||
TeamMembershipHandler: teamMembershipHandler,
|
TeamMembershipHandler: teamMembershipHandler,
|
||||||
EndpointHandler: endpointHandler,
|
EndpointHandler: endpointHandler,
|
||||||
|
EndpointGroupHandler: endpointGroupHandler,
|
||||||
RegistryHandler: registryHandler,
|
RegistryHandler: registryHandler,
|
||||||
DockerHubHandler: dockerHubHandler,
|
DockerHubHandler: dockerHubHandler,
|
||||||
ResourceHandler: resourceHandler,
|
ResourceHandler: resourceHandler,
|
||||||
|
|
|
@ -174,6 +174,7 @@ type (
|
||||||
ID EndpointID `json:"Id"`
|
ID EndpointID `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
URL string `json:"URL"`
|
URL string `json:"URL"`
|
||||||
|
GroupID EndpointGroupID `json:"GroupId"`
|
||||||
PublicURL string `json:"PublicURL"`
|
PublicURL string `json:"PublicURL"`
|
||||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
|
@ -188,6 +189,19 @@ type (
|
||||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EndpointGroupID represents an endpoint group identifier.
|
||||||
|
EndpointGroupID int
|
||||||
|
|
||||||
|
// EndpointGroup represents a group of endpoints.
|
||||||
|
EndpointGroup struct {
|
||||||
|
ID EndpointGroupID `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Description string `json:"Description"`
|
||||||
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
|
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||||
|
Labels []Pair `json:"Labels"`
|
||||||
|
}
|
||||||
|
|
||||||
// EndpointExtension represents a extension associated to an endpoint.
|
// EndpointExtension represents a extension associated to an endpoint.
|
||||||
EndpointExtension struct {
|
EndpointExtension struct {
|
||||||
Type EndpointExtensionType `json:"Type"`
|
Type EndpointExtensionType `json:"Type"`
|
||||||
|
@ -248,6 +262,7 @@ type (
|
||||||
// DataStore defines the interface to manage the data.
|
// DataStore defines the interface to manage the data.
|
||||||
DataStore interface {
|
DataStore interface {
|
||||||
Open() error
|
Open() error
|
||||||
|
Init() error
|
||||||
Close() error
|
Close() error
|
||||||
MigrateData() error
|
MigrateData() error
|
||||||
}
|
}
|
||||||
|
@ -301,6 +316,15 @@ type (
|
||||||
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
|
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EndpointGroupService represents a service for managing endpoint group data.
|
||||||
|
EndpointGroupService interface {
|
||||||
|
EndpointGroup(ID EndpointGroupID) (*EndpointGroup, error)
|
||||||
|
EndpointGroups() ([]EndpointGroup, error)
|
||||||
|
CreateEndpointGroup(group *EndpointGroup) error
|
||||||
|
UpdateEndpointGroup(ID EndpointGroupID, group *EndpointGroup) error
|
||||||
|
DeleteEndpointGroup(ID EndpointGroupID) error
|
||||||
|
}
|
||||||
|
|
||||||
// RegistryService represents a service for managing registry data.
|
// RegistryService represents a service for managing registry data.
|
||||||
RegistryService interface {
|
RegistryService interface {
|
||||||
Registry(ID RegistryID) (*Registry, error)
|
Registry(ID RegistryID) (*Registry, error)
|
||||||
|
@ -403,7 +427,7 @@ const (
|
||||||
// APIVersion is the version number of the Portainer API.
|
// APIVersion is the version number of the Portainer API.
|
||||||
APIVersion = "1.16.5"
|
APIVersion = "1.16.5"
|
||||||
// DBVersion is the version number of the Portainer database.
|
// DBVersion is the version number of the Portainer database.
|
||||||
DBVersion = 8
|
DBVersion = 9
|
||||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ angular.module('portainer')
|
||||||
.constant('API_ENDPOINT_AUTH', 'api/auth')
|
.constant('API_ENDPOINT_AUTH', 'api/auth')
|
||||||
.constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub')
|
.constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub')
|
||||||
.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints')
|
.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints')
|
||||||
|
.constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups')
|
||||||
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
|
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
|
||||||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
|
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
|
||||||
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
|
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
|
||||||
|
|
|
@ -132,6 +132,50 @@ angular.module('portainer.app', [])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var groups = {
|
||||||
|
name: 'portainer.groups',
|
||||||
|
url: '/groups',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/portainer/views/groups/groups.html',
|
||||||
|
controller: 'GroupsController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var group = {
|
||||||
|
name: 'portainer.groups.group',
|
||||||
|
url: '/:id',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/portainer/views/groups/edit/group.html',
|
||||||
|
controller: 'GroupController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var groupCreation = {
|
||||||
|
name: 'portainer.groups.new',
|
||||||
|
url: '/new',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/portainer/views/groups/create/creategroup.html',
|
||||||
|
controller: 'CreateGroupController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var groupAccess = {
|
||||||
|
name: 'portainer.groups.group.access',
|
||||||
|
url: '/access',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/portainer/views/groups/access/groupAccess.html',
|
||||||
|
controller: 'GroupAccessController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var registries = {
|
var registries = {
|
||||||
name: 'portainer.registries',
|
name: 'portainer.registries',
|
||||||
url: '/registries',
|
url: '/registries',
|
||||||
|
@ -253,6 +297,10 @@ angular.module('portainer.app', [])
|
||||||
$stateRegistryProvider.register(endpoints);
|
$stateRegistryProvider.register(endpoints);
|
||||||
$stateRegistryProvider.register(endpoint);
|
$stateRegistryProvider.register(endpoint);
|
||||||
$stateRegistryProvider.register(endpointAccess);
|
$stateRegistryProvider.register(endpointAccess);
|
||||||
|
$stateRegistryProvider.register(groups);
|
||||||
|
$stateRegistryProvider.register(group);
|
||||||
|
$stateRegistryProvider.register(groupAccess);
|
||||||
|
$stateRegistryProvider.register(groupCreation);
|
||||||
$stateRegistryProvider.register(registries);
|
$stateRegistryProvider.register(registries);
|
||||||
$stateRegistryProvider.register(registry);
|
$stateRegistryProvider.register(registry);
|
||||||
$stateRegistryProvider.register(registryAccess);
|
$stateRegistryProvider.register(registryAccess);
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
angular.module('portainer.app').component('accessTable', {
|
||||||
|
templateUrl: 'app/portainer/components/access-table/accessTable.html',
|
||||||
|
controller: function() {
|
||||||
|
this.state = {
|
||||||
|
orderBy: 'Name',
|
||||||
|
reverseOrder: false,
|
||||||
|
paginatedItemLimit: '10',
|
||||||
|
textFilter: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.changeOrderBy = function(orderField) {
|
||||||
|
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||||
|
this.state.orderBy = orderField;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
bindings: {
|
||||||
|
dataset: '<',
|
||||||
|
entryClick: '<',
|
||||||
|
emptyDatasetMessage: '@'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
<div class="datatable">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
|
||||||
|
</div>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||||
|
Name
|
||||||
|
<span ng-show="$ctrl.state.orderBy == 'Name' && !$ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="$ctrl.state.orderBy == 'Name' && $ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Type')">
|
||||||
|
Type
|
||||||
|
<span ng-show="$ctrl.state.orderBy == 'Type' && !$ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="$ctrl.state.orderBy == 'Type' && $ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-click="!item.Inherited && $ctrl.entryClick(item)" ng-class="{ 'interactive': !item.Inherited }" dir-paginate="item in $ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit">
|
||||||
|
<td>
|
||||||
|
{{ item.Name }}
|
||||||
|
<!-- <span class="image-tag label label-">inherited</span> -->
|
||||||
|
<span ng-if="item.Inherited" class="text-muted small" style="margin-left: 2px;"><code style="font-size: 85% !important;">inherited</code></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="fa" ng-class="item.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
{{ item.Type }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="!$ctrl.dataset">
|
||||||
|
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.dataset.length === 0 || ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit).length === 0">
|
||||||
|
<td colspan="2" class="text-center text-muted">{{ $ctrl.emptyDatasetMessage }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="footer" ng-if="$ctrl.dataset">
|
||||||
|
<div class="paginationControls">
|
||||||
|
<form class="form-inline">
|
||||||
|
<span class="limitSelector">
|
||||||
|
<span style="margin-right: 5px;">
|
||||||
|
Items per page
|
||||||
|
</span>
|
||||||
|
<select ng-model="$ctrl.state.paginatedItemLimit">
|
||||||
|
<option value="0">All</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -3,6 +3,8 @@ angular.module('portainer.app').component('porAccessManagement', {
|
||||||
controller: 'porAccessManagementController',
|
controller: 'porAccessManagementController',
|
||||||
bindings: {
|
bindings: {
|
||||||
accessControlledEntity: '<',
|
accessControlledEntity: '<',
|
||||||
|
inheritFrom: '<',
|
||||||
|
entityType: '@',
|
||||||
updateAccess: '&'
|
updateAccess: '&'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,134 +1,46 @@
|
||||||
<div class="row">
|
<rd-widget>
|
||||||
<div class="col-sm-6">
|
<rd-widget-header icon="fa-users" title="Access management"></rd-widget-header>
|
||||||
<rd-widget>
|
<rd-widget-body>
|
||||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users and teams">
|
<form class="form-horizontal">
|
||||||
<div class="pull-md-right pull-lg-right">
|
<div class="small text-muted">
|
||||||
Items per page:
|
<p>You can select which user or team can access this {{ $ctrl.entityType }} by moving them to the authorized accesses table. Simply click
|
||||||
<select ng-model="$ctrl.state.pagination_count_accesses" ng-change="$ctrl.changePaginationCountAccesses()">
|
on a user or team entry to move it from one table to the other.</p>
|
||||||
<option value="0">All</option>
|
<p ng-if="$ctrl.inheritFrom">
|
||||||
<option value="10">10</option>
|
<b>Note</b>: accesses tagged as <code>inherited</code> are inherited from the group accesses and cannot be remove at the endpoint level.
|
||||||
<option value="25">25</option>
|
</p>
|
||||||
<option value="50">50</option>
|
</div>
|
||||||
<option value="100">100</option>
|
<div class="form-group" style="margin-top: 20px;">
|
||||||
</select>
|
<!-- available-endpoints -->
|
||||||
</div>
|
<div class="col-sm-6">
|
||||||
</rd-widget-header>
|
<div class="text-center small text-muted">Users and teams</div>
|
||||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
<div class="text-center small text-muted" style="margin-top: 5px;">
|
||||||
<div class="col-sm-12 col-md-6 nopadding">
|
<button class="btn btn-primary btn-sm" ng-click="$ctrl.authorizeAllAccesses()" ng-disabled="$ctrl.accesses.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all</button>
|
||||||
<button class="btn btn-primary btn-sm" ng-click="$ctrl.authorizeAllAccesses()" ng-disabled="$ctrl.accesses.length === 0 || $ctrl.filteredUsers.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all</button>
|
</div>
|
||||||
</div>
|
<div style="margin-top: 10px;">
|
||||||
<div class="col-sm-12 col-md-6 nopadding">
|
<access-table
|
||||||
<input type="text" id="filter" ng-model="$ctrl.state.filterUsers" placeholder="Filter..." class="form-control input-sm" />
|
dataset="$ctrl.accesses"
|
||||||
</div>
|
entry-click="$ctrl.authorizeAccess"
|
||||||
</rd-widget-taskbar>
|
empty-dataset-message="No user or team available"
|
||||||
<rd-widget-body classes="no-padding">
|
></access-table>
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<a ng-click="$ctrl.orderAccesses('Name')">
|
|
||||||
Name
|
|
||||||
<span ng-show="$ctrl.state.sortAccessesBy == 'Name' && !$ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
|
|
||||||
<span ng-show="$ctrl.state.sortAccessesBy == 'Name' && $ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<a ng-click="$ctrl.orderAccesses('Type')">
|
|
||||||
Type
|
|
||||||
<span ng-show="$ctrl.state.sortAccessesBy == 'Type' && !$ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
|
|
||||||
<span ng-show="$ctrl.state.sortAccessesBy == 'Type' && $ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr ng-click="$ctrl.authorizeAccess(user)" class="interactive" dir-paginate="user in $ctrl.accesses | filter:$ctrl.state.filterUsers | orderBy:$ctrl.state.sortAccessesBy:$ctrl.state.sortAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_accesses">
|
|
||||||
<td>{{ user.Name }}</td>
|
|
||||||
<td>
|
|
||||||
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
|
|
||||||
{{ user.Type }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="!$ctrl.accesses">
|
|
||||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="$ctrl.accesses.length === 0 || ($ctrl.accesses | filter:$ctrl.state.filterUsers | orderBy:$ctrl.state.sortAccessesBy:$ctrl.state.sortAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_accesses).length === 0">
|
|
||||||
<td colspan="2" class="text-center text-muted">No user or team available.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div ng-if="$ctrl.accesses" class="pull-left pagination-controls">
|
|
||||||
<dir-pagination-controls></dir-pagination-controls>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</rd-widget-body>
|
<!-- !available-endpoints -->
|
||||||
</rd-widget>
|
<!-- associated-endpoints -->
|
||||||
</div>
|
<div class="col-sm-6">
|
||||||
<div class="col-sm-6">
|
<div class="text-center small text-muted">Authorized users and teams</div>
|
||||||
<rd-widget>
|
<div class="text-center small text-muted" style="margin-top: 5px;">
|
||||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users and teams">
|
<button class="btn btn-primary btn-sm" ng-click="$ctrl.unauthorizeAllAccesses()" ng-disabled="$ctrl.authorizedAccesses.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all</button>
|
||||||
<div class="pull-md-right pull-lg-right">
|
</div>
|
||||||
Items per page:
|
<div style="margin-top: 10px;">
|
||||||
<select ng-model="$ctrl.state.pagination_count_authorizedAccesses" ng-change="$ctrl.changePaginationCountAuthorizedAccesses()">
|
<access-table
|
||||||
<option value="0">All</option>
|
dataset="$ctrl.authorizedAccesses"
|
||||||
<option value="10">10</option>
|
entry-click="$ctrl.unauthorizeAccess"
|
||||||
<option value="25">25</option>
|
empty-dataset-message="No authorized user or team"
|
||||||
<option value="50">50</option>
|
></access-table>
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</rd-widget-header>
|
|
||||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
|
||||||
<div class="col-sm-12 col-md-6 nopadding">
|
|
||||||
<button class="btn btn-primary btn-sm" ng-click="$ctrl.unauthorizeAllAccesses()" ng-disabled="$ctrl.authorizedAccesses.length === 0 || $ctrl.filteredAuthorizedUsers.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 col-md-6 nopadding">
|
|
||||||
<input type="text" id="filter" ng-model="$ctrl.state.filterAuthorizedUsers" placeholder="Filter..." class="form-control input-sm" />
|
|
||||||
</div>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<a ng-click="$ctrl.orderAuthorizedAccesses('Name')">
|
|
||||||
Name
|
|
||||||
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Name' && !$ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
|
|
||||||
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Name' && $ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<a ng-click="$ctrl.orderAuthorizedAccesses('Type')">
|
|
||||||
Type
|
|
||||||
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Type' && !$ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
|
|
||||||
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Type' && $ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr ng-click="$ctrl.unauthorizeAccess(user)" class="interactive" pagination-id="table_authaccess" dir-paginate="user in $ctrl.authorizedAccesses | filter:$ctrl.state.filterAuthorizedUsers | orderBy:$ctrl.state.sortAuthorizedAccessesBy:$ctrl.state.sortAuthorizedAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_authorizedAccesses">
|
|
||||||
<td>{{ user.Name }}</td>
|
|
||||||
<td>
|
|
||||||
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
|
|
||||||
{{ user.Type }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="!$ctrl.authorizedAccesses">
|
|
||||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="$ctrl.authorizedAccesses.length === 0 || (authorizedAccesses | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedAccesses:sortReverseAuthorizedAccesses | itemsPerPage: state.pagination_count_authorizedAccesses).length === 0">
|
|
||||||
<td colspan="2" class="text-center text-muted">No authorized user or team.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div ng-if="$ctrl.authorizedAccesses" class="pull-left pagination-controls">
|
|
||||||
<dir-pagination-controls pagination-id="table_authaccess"></dir-pagination-controls>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</rd-widget-body>
|
<!-- !associated-endpoints -->
|
||||||
</rd-widget>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
|
|
@ -1,40 +1,13 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('porAccessManagementController', ['AccessService', 'PaginationService', 'Notifications',
|
.controller('porAccessManagementController', ['AccessService', 'Notifications',
|
||||||
function (AccessService, PaginationService, Notifications) {
|
function (AccessService, Notifications) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
ctrl.state = {
|
|
||||||
pagination_count_accesses: PaginationService.getPaginationLimit('access_management_accesses'),
|
|
||||||
pagination_count_authorizedAccesses: PaginationService.getPaginationLimit('access_management_AuthorizedAccesses'),
|
|
||||||
sortAccessesBy: 'Type',
|
|
||||||
sortAccessesReverse: false,
|
|
||||||
sortAuthorizedAccessesBy: 'Type',
|
|
||||||
sortAuthorizedAccessesReverse: false
|
|
||||||
};
|
|
||||||
|
|
||||||
ctrl.orderAccesses = function(sortBy) {
|
|
||||||
ctrl.state.sortAccessesReverse = (ctrl.state.sortAccessesBy === sortBy) ? !ctrl.state.sortAccessesReverse : false;
|
|
||||||
ctrl.state.sortAccessesBy = sortBy;
|
|
||||||
};
|
|
||||||
|
|
||||||
ctrl.orderAuthorizedAccesses = function(sortBy) {
|
|
||||||
ctrl.state.sortAuthorizedAccessesReverse = (ctrl.state.sortAuthorizedAccessesBy === sortBy) ? !ctrl.state.sortAuthorizedAccessesReverse : false;
|
|
||||||
ctrl.state.sortAuthorizedAccessesBy = sortBy;
|
|
||||||
};
|
|
||||||
|
|
||||||
ctrl.changePaginationCountAuthorizedAccesses = function() {
|
|
||||||
PaginationService.setPaginationLimit('access_management_AuthorizedAccesses', ctrl.state.pagination_count_authorizedAccesses);
|
|
||||||
};
|
|
||||||
|
|
||||||
ctrl.changePaginationCountAccesses = function() {
|
|
||||||
PaginationService.setPaginationLimit('access_management_accesses', ctrl.state.pagination_count_accesses);
|
|
||||||
};
|
|
||||||
|
|
||||||
function dispatchUserAndTeamIDs(accesses, users, teams) {
|
function dispatchUserAndTeamIDs(accesses, users, teams) {
|
||||||
angular.forEach(accesses, function (access) {
|
angular.forEach(accesses, function (access) {
|
||||||
if (access.Type === 'user') {
|
if (access.Type === 'user' && !access.Inherited) {
|
||||||
users.push(access.Id);
|
users.push(access.Id);
|
||||||
} else if (access.Type === 'team') {
|
} else if (access.Type === 'team' && !access.Inherited) {
|
||||||
teams.push(access.Id);
|
teams.push(access.Id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -111,11 +84,20 @@ function (AccessService, PaginationService, Notifications) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function moveAccesses(source, target) {
|
||||||
|
for (var i = 0; i < source.length; i++) {
|
||||||
|
var access = source[i];
|
||||||
|
if (!access.Inherited) {
|
||||||
|
target.push(access);
|
||||||
|
source.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctrl.unauthorizeAllAccesses = function() {
|
ctrl.unauthorizeAllAccesses = function() {
|
||||||
ctrl.updateAccess({ userAccesses: [], teamAccesses: [] })
|
ctrl.updateAccess({ userAccesses: [], teamAccesses: [] })
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
ctrl.accesses = ctrl.accesses.concat(ctrl.authorizedAccesses);
|
moveAccesses(ctrl.authorizedAccesses, ctrl.accesses);
|
||||||
ctrl.authorizedAccesses = [];
|
|
||||||
Notifications.success('Accesses successfully updated');
|
Notifications.success('Accesses successfully updated');
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -130,8 +112,7 @@ function (AccessService, PaginationService, Notifications) {
|
||||||
|
|
||||||
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
|
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
ctrl.authorizedAccesses = ctrl.authorizedAccesses.concat(ctrl.accesses);
|
moveAccesses(ctrl.accesses, ctrl.authorizedAccesses);
|
||||||
ctrl.accesses = [];
|
|
||||||
Notifications.success('Accesses successfully updated');
|
Notifications.success('Accesses successfully updated');
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -141,7 +122,8 @@ function (AccessService, PaginationService, Notifications) {
|
||||||
|
|
||||||
function initComponent() {
|
function initComponent() {
|
||||||
var entity = ctrl.accessControlledEntity;
|
var entity = ctrl.accessControlledEntity;
|
||||||
AccessService.accesses(entity.AuthorizedUsers, entity.AuthorizedTeams)
|
var parent = ctrl.inheritFrom;
|
||||||
|
AccessService.accesses(entity.AuthorizedUsers, entity.AuthorizedTeams, parent ? parent.AuthorizedUsers: [], parent ? parent.AuthorizedTeams : [])
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
ctrl.accesses = data.accesses;
|
ctrl.accesses = data.accesses;
|
||||||
ctrl.authorizedAccesses = data.authorizedAccesses;
|
ctrl.authorizedAccesses = data.authorizedAccesses;
|
||||||
|
|
|
@ -43,6 +43,13 @@
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'URL' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'URL' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('GroupName')">
|
||||||
|
Group
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'GroupName' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'GroupName' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -57,6 +64,7 @@
|
||||||
<span ng-if="!$ctrl.endpointManagement">{{ item.Name }}</span>
|
<span ng-if="!$ctrl.endpointManagement">{{ item.Name }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.URL | stripprotocol }}</td>
|
<td>{{ item.URL | stripprotocol }}</td>
|
||||||
|
<td>{{ item.GroupName }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a ui-sref="portainer.endpoints.endpoint.access({id: item.Id})" ng-if="$ctrl.accessManagement">
|
<a ui-sref="portainer.endpoints.endpoint.access({id: item.Id})" ng-if="$ctrl.accessManagement">
|
||||||
<i class="fa fa-users" aria-hidden="true"></i> Manage access
|
<i class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
<div class="datatable">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<div class="toolBar">
|
||||||
|
<div class="toolBarTitle">
|
||||||
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
|
||||||
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
|
||||||
|
<i class="fa fa-search" aria-hidden="true"></i> Search
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actionBar">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.groups.new">
|
||||||
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||||
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span class="md-checkbox">
|
||||||
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
|
<label for="select_all"></label>
|
||||||
|
</span>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||||
|
Name
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
|
<td>
|
||||||
|
<span class="md-checkbox">
|
||||||
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
|
<label for="select_{{ $index }}"></label>
|
||||||
|
</span>
|
||||||
|
<a ui-sref="portainer.groups.group({id: item.Id})">{{ item.Name }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a ui-sref="portainer.groups.group.access({id: item.Id})" ng-if="$ctrl.accessManagement">
|
||||||
|
<i class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="!$ctrl.dataset">
|
||||||
|
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||||
|
<td colspan="3" class="text-center text-muted">No group available.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer" ng-if="$ctrl.dataset">
|
||||||
|
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||||
|
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||||
|
</div>
|
||||||
|
<div class="paginationControls">
|
||||||
|
<form class="form-inline">
|
||||||
|
<span class="limitSelector">
|
||||||
|
<span style="margin-right: 5px;">
|
||||||
|
Items per page
|
||||||
|
</span>
|
||||||
|
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||||
|
<option value="0">All</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
|
@ -0,0 +1,15 @@
|
||||||
|
angular.module('portainer.app').component('groupsDatatable', {
|
||||||
|
templateUrl: 'app/portainer/components/datatables/groups-datatable/groupsDatatable.html',
|
||||||
|
controller: 'GenericDatatableController',
|
||||||
|
bindings: {
|
||||||
|
title: '@',
|
||||||
|
titleIcon: '@',
|
||||||
|
dataset: '<',
|
||||||
|
tableKey: '@',
|
||||||
|
orderBy: '@',
|
||||||
|
reverseOrder: '<',
|
||||||
|
showTextFilter: '<',
|
||||||
|
accessManagement: '<',
|
||||||
|
removeAction: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
angular.module('portainer.app').component('endpointSelector', {
|
||||||
|
templateUrl: 'app/portainer/components/endpoint-selector/endpointSelector.html',
|
||||||
|
controller: 'EndpointSelectorController',
|
||||||
|
bindings: {
|
||||||
|
'endpoints': '<',
|
||||||
|
'groups': '<',
|
||||||
|
'selectEndpoint': '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
<div ng-if="$ctrl.endpoints.length > 1">
|
||||||
|
<div ng-if="!$ctrl.state.show">
|
||||||
|
<li class="sidebar-title">
|
||||||
|
<span class="interactive" style="color: #fff;" ng-click="$ctrl.state.show = true;">
|
||||||
|
<span class="fa fa-plug space-right"></span>Change environment
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.state.show">
|
||||||
|
<div ng-if="$ctrl.availableGroups.length > 1">
|
||||||
|
<li class="sidebar-title"><span>Group</span></li>
|
||||||
|
<li class="sidebar-title">
|
||||||
|
<select class="select-endpoint form-control" ng-options="group.Name for group in $ctrl.availableGroups" ng-model="$ctrl.state.selectedGroup" ng-change="$ctrl.selectGroup()">
|
||||||
|
<option value="">Select a group</option>
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.state.selectedGroup || $ctrl.availableGroups.length <= 1">
|
||||||
|
<li class="sidebar-title"><span>Endpoint</span></li>
|
||||||
|
<li class="sidebar-title">
|
||||||
|
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in $ctrl.availableEndpoints" ng-model="$ctrl.state.selectedEndpoint" ng-change="$ctrl.selectEndpoint($ctrl.state.selectedEndpoint)">
|
||||||
|
<option value="">Select an endpoint</option>
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,34 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('EndpointSelectorController', function () {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
show: false,
|
||||||
|
selectedGroup: null,
|
||||||
|
selectedEndpoint: null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectGroup = function() {
|
||||||
|
this.availableEndpoints = this.endpoints.filter(function f(endpoint) {
|
||||||
|
return endpoint.GroupId === ctrl.state.selectedGroup.Id;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onInit = function() {
|
||||||
|
this.availableGroups = filterEmptyGroups(this.groups, this.endpoints);
|
||||||
|
this.availableEndpoints = this.endpoints;
|
||||||
|
};
|
||||||
|
|
||||||
|
function filterEmptyGroups(groups, endpoints) {
|
||||||
|
return groups.filter(function f(group) {
|
||||||
|
for (var i = 0; i < endpoints.length; i++) {
|
||||||
|
|
||||||
|
var endpoint = endpoints[i];
|
||||||
|
if (endpoint.GroupId === group.Id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
angular.module('portainer.app').component('groupForm', {
|
||||||
|
templateUrl: 'app/portainer/components/forms/group-form/groupForm.html',
|
||||||
|
controller: function() {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
this.associateEndpoint = function(endpoint) {
|
||||||
|
ctrl.associatedEndpoints.push(endpoint);
|
||||||
|
_.remove(ctrl.availableEndpoints, function(n) {
|
||||||
|
return n.Id === endpoint.Id;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dissociateEndpoint = function(endpoint) {
|
||||||
|
ctrl.availableEndpoints.push(endpoint);
|
||||||
|
_.remove(ctrl.associatedEndpoints, function(n) {
|
||||||
|
return n.Id === endpoint.Id;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
},
|
||||||
|
bindings: {
|
||||||
|
model: '=',
|
||||||
|
availableEndpoints: '=',
|
||||||
|
associatedEndpoints: '=',
|
||||||
|
addLabelAction: '<',
|
||||||
|
removeLabelAction: '<',
|
||||||
|
formAction: '<',
|
||||||
|
formActionLabel: '@',
|
||||||
|
actionInProgress: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,120 @@
|
||||||
|
<form class="form-horizontal" name="endpointGroupForm">
|
||||||
|
<!-- name-input -->
|
||||||
|
<div class="form-group" ng-class="{ 'has-error': endpointGroupForm.group_name.$invalid }">
|
||||||
|
<label for="group_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" name="group_name" ng-model="$ctrl.model.Name" placeholder="e.g. my-group" required auto-focus>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="endpointGroupForm.group_name.$invalid">
|
||||||
|
<div class="col-sm-12 small text-danger">
|
||||||
|
<div ng-messages="endpointGroupForm.group_name.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input -->
|
||||||
|
<!-- description-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="group_description" class="col-sm-3 col-lg-2 control-label text-left">Description</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="group_description" ng-model="$ctrl.model.Description" placeholder="e.g. production environments...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !description-input -->
|
||||||
|
<!-- labels -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12" style="margin-top: 5px;">
|
||||||
|
<label class="control-label text-left">Labels</label>
|
||||||
|
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addLabelAction()">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- labels-input-list -->
|
||||||
|
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||||
|
<div ng-repeat="label in $ctrl.model.Labels" style="margin-top: 2px;">
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">name</span>
|
||||||
|
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. organization">
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">value</span>
|
||||||
|
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. acme">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabelAction($index)">
|
||||||
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !labels-input-list -->
|
||||||
|
</div>
|
||||||
|
<!-- !labels -->
|
||||||
|
<!-- endpoints -->
|
||||||
|
<div ng-if="$ctrl.model.Id !== 1">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Associated endpoints
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12 small text-muted">
|
||||||
|
You can select which endpoint should be part of this group by moving them to the associated endpoints table. Simply click
|
||||||
|
on any endpoint entry to move it from one table to the other.
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12" style="margin-top: 20px;">
|
||||||
|
<!-- available-endpoints -->
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="text-center small text-muted">Available endpoints</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<group-association-table
|
||||||
|
dataset="$ctrl.availableEndpoints"
|
||||||
|
entry-click="$ctrl.associateEndpoint"
|
||||||
|
empty-dataset-message="No endpoint available"
|
||||||
|
></group-association-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !available-endpoints -->
|
||||||
|
<!-- associated-endpoints -->
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="text-center small text-muted">Associated endpoints</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<group-association-table
|
||||||
|
dataset="$ctrl.associatedEndpoints"
|
||||||
|
entry-click="$ctrl.dissociateEndpoint"
|
||||||
|
empty-dataset-message="No associated endpoint"
|
||||||
|
></group-association-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !associated-endpoints -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.model.Id === 1">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Unassociated endpoints
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.associatedEndpoints.length > 0">
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<group-association-table
|
||||||
|
dataset="$ctrl.associatedEndpoints"
|
||||||
|
empty-dataset-message="No endpoint available"
|
||||||
|
></group-association-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<span class="text-muted small">All the endpoints are assigned to a group.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !endpoints -->
|
||||||
|
<!-- actions -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.formAction()" ng-disabled="$ctrl.actionInProgress || !endpointGroupForm.$valid" button-spinner="$ctrl.actionInProgress">
|
||||||
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
</form>
|
|
@ -0,0 +1,21 @@
|
||||||
|
angular.module('portainer.app').component('groupAssociationTable', {
|
||||||
|
templateUrl: 'app/portainer/components/group-association-table/groupAssociationTable.html',
|
||||||
|
controller: function() {
|
||||||
|
this.state = {
|
||||||
|
orderBy: 'Name',
|
||||||
|
reverseOrder: false,
|
||||||
|
paginatedItemLimit: '10',
|
||||||
|
textFilter: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.changeOrderBy = function(orderField) {
|
||||||
|
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||||
|
this.state.orderBy = orderField;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
bindings: {
|
||||||
|
dataset: '<',
|
||||||
|
entryClick: '<',
|
||||||
|
emptyDatasetMessage: '@'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
<div class="datatable">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
|
||||||
|
</div>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||||
|
Name
|
||||||
|
<span ng-show="$ctrl.state.orderBy == 'Name' && !$ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="$ctrl.state.orderBy == 'Name' && $ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-click="$ctrl.entryClick(item)" class="interactive" dir-paginate="item in $ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit">
|
||||||
|
<td>{{ item.Name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="!$ctrl.dataset">
|
||||||
|
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.dataset.length === 0 || ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit).length === 0">
|
||||||
|
<td colspan="2" class="text-center text-muted">{{ $ctrl.emptyDatasetMessage }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="footer" ng-if="$ctrl.dataset">
|
||||||
|
<div class="paginationControls">
|
||||||
|
<form class="form-inline">
|
||||||
|
<span class="limitSelector">
|
||||||
|
<span style="margin-right: 5px;">
|
||||||
|
Items per page
|
||||||
|
</span>
|
||||||
|
<select ng-model="$ctrl.state.paginatedItemLimit">
|
||||||
|
<option value="0">All</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('EndpointHelper', [function EndpointHelperFactory() {
|
||||||
|
'use strict';
|
||||||
|
var helper = {};
|
||||||
|
|
||||||
|
function findAssociatedGroup(endpoint, groups) {
|
||||||
|
return _.find(groups, function(group) {
|
||||||
|
return group.Id === endpoint.GroupId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.mapGroupNameToEndpoint = function(endpoints, groups) {
|
||||||
|
for (var i = 0; i < endpoints.length; i++) {
|
||||||
|
var endpoint = endpoints[i];
|
||||||
|
var group = findAssociatedGroup(endpoint, groups);
|
||||||
|
if (group) {
|
||||||
|
endpoint.GroupName = group.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return helper;
|
||||||
|
}]);
|
|
@ -2,10 +2,12 @@ function UserAccessViewModel(data) {
|
||||||
this.Id = data.Id;
|
this.Id = data.Id;
|
||||||
this.Name = data.Username;
|
this.Name = data.Username;
|
||||||
this.Type = 'user';
|
this.Type = 'user';
|
||||||
|
this.Inherited = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TeamAccessViewModel(data) {
|
function TeamAccessViewModel(data) {
|
||||||
this.Id = data.Id;
|
this.Id = data.Id;
|
||||||
this.Name = data.Name;
|
this.Name = data.Name;
|
||||||
this.Type = 'team';
|
this.Type = 'team';
|
||||||
|
this.Inherited = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
function EndpointGroupDefaultModel() {
|
||||||
|
this.Name = '';
|
||||||
|
this.Description = '';
|
||||||
|
this.Labels = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function EndpointGroupModel(data) {
|
||||||
|
this.Id = data.Id;
|
||||||
|
this.Name = data.Name;
|
||||||
|
this.Description = data.Description;
|
||||||
|
this.Labels = data.Labels;
|
||||||
|
this.AuthorizedUsers = data.AuthorizedUsers;
|
||||||
|
this.AuthorizedTeams = data.AuthorizedTeams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EndpointGroupCreateRequest(model, endpoints) {
|
||||||
|
this.Name = model.Name;
|
||||||
|
this.Description = model.Description;
|
||||||
|
this.Labels = model.Labels;
|
||||||
|
this.AssociatedEndpoints = endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EndpointGroupUpdateRequest(model, endpoints) {
|
||||||
|
this.id = model.Id;
|
||||||
|
this.Name = model.Name;
|
||||||
|
this.Description = model.Description;
|
||||||
|
this.Labels = model.Labels;
|
||||||
|
this.AssociatedEndpoints = endpoints;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('EndpointGroups', ['$resource', 'API_ENDPOINT_ENDPOINT_GROUPS', function EndpointGroupsFactory($resource, API_ENDPOINT_ENDPOINT_GROUPS) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_ENDPOINT_GROUPS + '/:id/:action', {}, {
|
||||||
|
create: { method: 'POST', ignoreLoadingBar: true },
|
||||||
|
query: { method: 'GET', isArray: true },
|
||||||
|
get: { method: 'GET', params: { id: '@id' } },
|
||||||
|
update: { method: 'PUT', params: { id: '@id' } },
|
||||||
|
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
||||||
|
remove: { method: 'DELETE', params: { id: '@id'} }
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -3,33 +3,30 @@ angular.module('portainer.app')
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
function mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs) {
|
function mapAccessData(accesses, authorizedIDs, inheritedIDs) {
|
||||||
var accesses = [];
|
var availableAccesses = [];
|
||||||
var authorizedAccesses = [];
|
var authorizedAccesses = [];
|
||||||
|
|
||||||
angular.forEach(userAccesses, function(access) {
|
for (var i = 0; i < accesses.length; i++) {
|
||||||
if (_.includes(authorizedUserIDs, access.Id)) {
|
|
||||||
authorizedAccesses.push(access);
|
|
||||||
} else {
|
|
||||||
accesses.push(access);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
angular.forEach(teamAccesses, function(access) {
|
var access = accesses[i];
|
||||||
if (_.includes(authorizedTeamIDs, access.Id)) {
|
if (_.includes(inheritedIDs, access.Id)) {
|
||||||
|
access.Inherited = true;
|
||||||
|
authorizedAccesses.push(access);
|
||||||
|
} else if (_.includes(authorizedIDs, access.Id)) {
|
||||||
authorizedAccesses.push(access);
|
authorizedAccesses.push(access);
|
||||||
} else {
|
} else {
|
||||||
accesses.push(access);
|
availableAccesses.push(access);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accesses: accesses,
|
accesses: availableAccesses,
|
||||||
authorizedAccesses: authorizedAccesses
|
authorizedAccesses: authorizedAccesses
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
service.accesses = function(authorizedUserIDs, authorizedTeamIDs) {
|
service.accesses = function(authorizedUserIDs, authorizedTeamIDs, inheritedUserIDs, inheritedTeamIDs) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
|
@ -44,7 +41,14 @@ angular.module('portainer.app')
|
||||||
return new TeamAccessViewModel(team);
|
return new TeamAccessViewModel(team);
|
||||||
});
|
});
|
||||||
|
|
||||||
var accessData = mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs);
|
var userAccessData = mapAccessData(userAccesses, authorizedUserIDs, inheritedUserIDs);
|
||||||
|
var teamAccessData = mapAccessData(teamAccesses, authorizedTeamIDs, inheritedTeamIDs);
|
||||||
|
|
||||||
|
var accessData = {
|
||||||
|
accesses: userAccessData.accesses.concat(teamAccessData.accesses),
|
||||||
|
authorizedAccesses: userAccessData.authorizedAccesses.concat(teamAccessData.authorizedAccesses)
|
||||||
|
};
|
||||||
|
|
||||||
deferred.resolve(accessData);
|
deferred.resolve(accessData);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -12,6 +12,23 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return Endpoints.query({}).$promise;
|
return Endpoints.query({}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.endpointsByGroup = function(groupId) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Endpoints.query({}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var endpoints = data.filter(function (endpoint) {
|
||||||
|
return endpoint.GroupId === groupId;
|
||||||
|
});
|
||||||
|
deferred.resolve(endpoints);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject({msg: 'Unable to retrieve endpoints', err: err});
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
service.updateAccess = function(id, authorizedUserIDs, authorizedTeamIDs) {
|
service.updateAccess = function(id, authorizedUserIDs, authorizedTeamIDs) {
|
||||||
return Endpoints.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
|
return Endpoints.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
|
||||||
};
|
};
|
||||||
|
@ -20,6 +37,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
var query = {
|
var query = {
|
||||||
name: endpointParams.name,
|
name: endpointParams.name,
|
||||||
PublicURL: endpointParams.PublicURL,
|
PublicURL: endpointParams.PublicURL,
|
||||||
|
GroupId: endpointParams.GroupId,
|
||||||
TLS: endpointParams.TLS,
|
TLS: endpointParams.TLS,
|
||||||
TLSSkipVerify: endpointParams.TLSSkipVerify,
|
TLSSkipVerify: endpointParams.TLSSkipVerify,
|
||||||
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
|
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
|
||||||
|
@ -49,10 +67,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return Endpoints.remove({id: endpointID}).$promise;
|
return Endpoints.remove({id: endpointID}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.createLocalEndpoint = function(name, URL, TLS, active) {
|
service.createLocalEndpoint = function() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
FileUploadService.createEndpoint('local', 'unix:///var/run/docker.sock', '', false)
|
FileUploadService.createEndpoint('local', 'unix:///var/run/docker.sock', '', 1, false)
|
||||||
.then(function success(response) {
|
.then(function success(response) {
|
||||||
deferred.resolve(response.data);
|
deferred.resolve(response.data);
|
||||||
})
|
})
|
||||||
|
@ -63,10 +81,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
service.createRemoteEndpoint = function(name, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
FileUploadService.createEndpoint(name, 'tcp://' + URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
FileUploadService.createEndpoint(name, 'tcp://' + URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||||
.then(function success(response) {
|
.then(function success(response) {
|
||||||
deferred.resolve(response.data);
|
deferred.resolve(response.data);
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('GroupService', ['$q', 'EndpointGroups',
|
||||||
|
function GroupService($q, EndpointGroups) {
|
||||||
|
'use strict';
|
||||||
|
var service = {};
|
||||||
|
|
||||||
|
service.group = function(groupId) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
EndpointGroups.get({ id: groupId }).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var group = new EndpointGroupModel(data);
|
||||||
|
deferred.resolve(group);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject({ msg: 'Unable to retrieve group', err: err });
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.groups = function() {
|
||||||
|
return EndpointGroups.query({}).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.createGroup = function(model, endpoints) {
|
||||||
|
var payload = new EndpointGroupCreateRequest(model, endpoints);
|
||||||
|
return EndpointGroups.create(payload).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.updateGroup = function(model, endpoints) {
|
||||||
|
var payload = new EndpointGroupUpdateRequest(model, endpoints);
|
||||||
|
return EndpointGroups.update(payload).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.updateAccess = function(groupId, authorizedUserIDs, authorizedTeamIDs) {
|
||||||
|
return EndpointGroups.updateAccess({ id: groupId }, { authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs }).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.deleteGroup = function(groupId) {
|
||||||
|
return EndpointGroups.remove({ id: groupId }).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}]);
|
|
@ -42,13 +42,14 @@ angular.module('portainer.app')
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
service.createEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
service.createEndpoint = function(name, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
return Upload.upload({
|
return Upload.upload({
|
||||||
url: 'api/endpoints',
|
url: 'api/endpoints',
|
||||||
data: {
|
data: {
|
||||||
Name: name,
|
Name: name,
|
||||||
URL: URL,
|
URL: URL,
|
||||||
PublicURL: PublicURL,
|
PublicURL: PublicURL,
|
||||||
|
GroupID: groupID,
|
||||||
TLS: TLS,
|
TLS: TLS,
|
||||||
TLSSkipVerify: TLSSkipVerify,
|
TLSSkipVerify: TLSSkipVerify,
|
||||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row" ng-if="endpoint">
|
<div class="row" ng-if="endpoint">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-plug" title="Endpoint"></rd-widget-header>
|
<rd-widget-header icon="fa-plug" title="Endpoint"></rd-widget-header>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
|
@ -25,11 +25,9 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td>Group</td>
|
||||||
<span class="small text-muted">
|
<td>
|
||||||
You can select which user or team can access this endpoint by moving them to the authorized accesses table. Simply click
|
<a ui-sref="portainer.groups.group({ id: group.Id })">{{ group.Name }}</a>
|
||||||
on a user or team entry to move it from one table to the other.
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -39,5 +37,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<por-access-management ng-if="endpoint" access-controlled-entity="endpoint" update-access="updateAccess(userAccesses, teamAccesses)">
|
<div class="row" ng-if="endpoint && group">
|
||||||
</por-access-management>
|
<div class="col-sm-12">
|
||||||
|
<por-access-management
|
||||||
|
access-controlled-entity="endpoint" entity-type="endpoint" inherit-from="group" update-access="updateAccess(userAccesses, teamAccesses)"
|
||||||
|
></por-access-management>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('EndpointAccessController', ['$scope', '$transition$', 'EndpointService', 'Notifications',
|
.controller('EndpointAccessController', ['$scope', '$transition$', 'EndpointService', 'GroupService', 'Notifications',
|
||||||
function ($scope, $transition$, EndpointService, Notifications) {
|
function ($scope, $transition$, EndpointService, GroupService, Notifications) {
|
||||||
|
|
||||||
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
|
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
|
||||||
return EndpointService.updateAccess($transition$.params().id, authorizedUsers, authorizedTeams);
|
return EndpointService.updateAccess($transition$.params().id, authorizedUsers, authorizedTeams);
|
||||||
|
@ -9,7 +9,12 @@ function ($scope, $transition$, EndpointService, Notifications) {
|
||||||
function initView() {
|
function initView() {
|
||||||
EndpointService.endpoint($transition$.params().id)
|
EndpointService.endpoint($transition$.params().id)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.endpoint = data;
|
var endpoint = data;
|
||||||
|
$scope.endpoint = endpoint;
|
||||||
|
return GroupService.group(endpoint.GroupId);
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.group = data;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||||
|
|
|
@ -43,6 +43,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-public-url-input -->
|
<!-- !endpoint-public-url-input -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Grouping
|
||||||
|
</div>
|
||||||
|
<!-- group -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Group
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<select ng-options="group.Id as group.Name for group in groups" ng-model="endpoint.GroupId" id="endpoint_group" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !group -->
|
||||||
<!-- endpoint-security -->
|
<!-- endpoint-security -->
|
||||||
<div ng-if="endpointType === 'remote'">
|
<div ng-if="endpointType === 'remote'">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('EndpointController', ['$scope', '$state', '$transition$', '$filter', 'EndpointService', 'Notifications',
|
.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'EndpointService', 'GroupService', 'EndpointProvider', 'Notifications',
|
||||||
function ($scope, $state, $transition$, $filter, EndpointService, Notifications) {
|
function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupService, EndpointProvider, Notifications) {
|
||||||
|
|
||||||
if (!$scope.applicationState.application.endpointManagement) {
|
if (!$scope.applicationState.application.endpointManagement) {
|
||||||
$state.go('portainer.endpoints');
|
$state.go('portainer.endpoints');
|
||||||
|
@ -27,6 +27,7 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
||||||
name: endpoint.Name,
|
name: endpoint.Name,
|
||||||
URL: endpoint.URL,
|
URL: endpoint.URL,
|
||||||
PublicURL: endpoint.PublicURL,
|
PublicURL: endpoint.PublicURL,
|
||||||
|
GroupId: endpoint.GroupId,
|
||||||
TLS: TLS,
|
TLS: TLS,
|
||||||
TLSSkipVerify: TLSSkipVerify,
|
TLSSkipVerify: TLSSkipVerify,
|
||||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||||
|
@ -40,7 +41,8 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
||||||
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
|
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||||
$state.go('portainer.endpoints');
|
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||||
|
$state.go('portainer.endpoints', {}, {reload: true});
|
||||||
}, function error(err) {
|
}, function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to update endpoint');
|
Notifications.error('Failure', err, 'Unable to update endpoint');
|
||||||
$scope.state.actionInProgress = false;
|
$scope.state.actionInProgress = false;
|
||||||
|
@ -52,9 +54,12 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
||||||
};
|
};
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
EndpointService.endpoint($transition$.params().id)
|
$q.all({
|
||||||
|
endpoint: EndpointService.endpoint($transition$.params().id),
|
||||||
|
groups: GroupService.groups()
|
||||||
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpoint = data;
|
var endpoint = data.endpoint;
|
||||||
if (endpoint.URL.indexOf('unix://') === 0) {
|
if (endpoint.URL.indexOf('unix://') === 0) {
|
||||||
$scope.endpointType = 'local';
|
$scope.endpointType = 'local';
|
||||||
} else {
|
} else {
|
||||||
|
@ -62,6 +67,7 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
||||||
}
|
}
|
||||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||||
$scope.endpoint = endpoint;
|
$scope.endpoint = endpoint;
|
||||||
|
$scope.groups = data.groups;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||||
|
|
|
@ -59,6 +59,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-public-url-input -->
|
<!-- !endpoint-public-url-input -->
|
||||||
|
<!-- group -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Group
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<select ng-options="group.Id as group.Name for group in groups" ng-model="formValues.GroupId" id="endpoint_group" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !group -->
|
||||||
<!-- endpoint-security -->
|
<!-- endpoint-security -->
|
||||||
<por-endpoint-security form-data="formValues.SecurityFormData"></por-endpoint-security>
|
<por-endpoint-security form-data="formValues.SecurityFormData"></por-endpoint-security>
|
||||||
<!-- !endpoint-security -->
|
<!-- !endpoint-security -->
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications',
|
.controller('EndpointsController', ['$q', '$scope', '$state', '$filter', 'EndpointService', 'GroupService', 'EndpointHelper', 'Notifications',
|
||||||
function ($scope, $state, $filter, EndpointService, Notifications) {
|
function ($q, $scope, $state, $filter, EndpointService, GroupService, EndpointHelper, Notifications) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
uploadInProgress: false,
|
uploadInProgress: false,
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
|
@ -10,6 +10,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
||||||
Name: '',
|
Name: '',
|
||||||
URL: '',
|
URL: '',
|
||||||
PublicURL: '',
|
PublicURL: '',
|
||||||
|
GroupId: 1,
|
||||||
SecurityFormData: new EndpointSecurityFormData()
|
SecurityFormData: new EndpointSecurityFormData()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
||||||
if (PublicURL === '') {
|
if (PublicURL === '') {
|
||||||
PublicURL = URL.split(':')[0];
|
PublicURL = URL.split(':')[0];
|
||||||
}
|
}
|
||||||
|
var groupId = $scope.formValues.GroupId;
|
||||||
|
|
||||||
var securityData = $scope.formValues.SecurityFormData;
|
var securityData = $scope.formValues.SecurityFormData;
|
||||||
var TLS = securityData.TLS;
|
var TLS = securityData.TLS;
|
||||||
|
@ -31,7 +33,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
||||||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
EndpointService.createRemoteEndpoint(name, URL, PublicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Endpoint created', name);
|
Notifications.success('Endpoint created', name);
|
||||||
$state.reload();
|
$state.reload();
|
||||||
|
@ -65,16 +67,22 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function fetchEndpoints() {
|
function initView() {
|
||||||
EndpointService.endpoints()
|
$q.all({
|
||||||
|
endpoints: EndpointService.endpoints(),
|
||||||
|
groups: GroupService.groups()
|
||||||
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.endpoints = data;
|
var endpoints = data.endpoints;
|
||||||
|
var groups = data.groups;
|
||||||
|
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||||
|
$scope.groups = groups;
|
||||||
|
$scope.endpoints = endpoints;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
Notifications.error('Failure', err, 'Unable to load view');
|
||||||
$scope.endpoints = [];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchEndpoints();
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Endpoint group access"></rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
<a ui-sref="portainer.groups">Groups</a> > <a ui-sref="portainer.groups.group({id: group.Id})">{{ group.Name }}</a> > Access management
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row" ng-if="group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-object-group" title="Group"></rd-widget-header>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>
|
||||||
|
{{ group.Name }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" ng-if="group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<por-access-management
|
||||||
|
ng-if="group" access-controlled-entity="group" entity-type="group" update-access="updateAccess(userAccesses, teamAccesses)"
|
||||||
|
></por-access-management>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,22 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('GroupAccessController', ['$scope', '$transition$', 'GroupService', 'Notifications',
|
||||||
|
function ($scope, $transition$, GroupService, Notifications) {
|
||||||
|
|
||||||
|
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
|
||||||
|
return GroupService.updateAccess($transition$.params().id, authorizedUsers, authorizedTeams);
|
||||||
|
};
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
var groupId = $transition$.params().id;
|
||||||
|
|
||||||
|
GroupService.group(groupId)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.group = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to load view');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}]);
|
|
@ -0,0 +1,54 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('CreateGroupController', ['$scope', '$state', 'GroupService', 'EndpointService', 'Notifications',
|
||||||
|
function ($scope, $state, GroupService, EndpointService, Notifications) {
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
actionInProgress: false
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addLabel = function() {
|
||||||
|
$scope.model.Labels.push({ name: '', value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeLabel = function(index) {
|
||||||
|
$scope.model.Labels.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.create = function() {
|
||||||
|
var model = $scope.model;
|
||||||
|
|
||||||
|
var associatedEndpoints = [];
|
||||||
|
for (var i = 0; i < $scope.associatedEndpoints.length; i++) {
|
||||||
|
var endpoint = $scope.associatedEndpoints[i];
|
||||||
|
associatedEndpoints.push(endpoint.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.state.actionInProgress = true;
|
||||||
|
GroupService.createGroup(model, associatedEndpoints)
|
||||||
|
.then(function success() {
|
||||||
|
Notifications.success('Group successfully created');
|
||||||
|
$state.go('portainer.groups', {}, {reload: true});
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to create group');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$scope.state.actionInProgress = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
$scope.model = new EndpointGroupDefaultModel();
|
||||||
|
|
||||||
|
EndpointService.endpointsByGroup(1)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.availableEndpoints = data;
|
||||||
|
$scope.associatedEndpoints = [];
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}]);
|
|
@ -0,0 +1,25 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Create endpoint group"></rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
<a ui-sref="portainer.groups">Endpoint groups</a> > Add group
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body>
|
||||||
|
<group-form
|
||||||
|
model="model"
|
||||||
|
available-endpoints="availableEndpoints"
|
||||||
|
associated-endpoints="associatedEndpoints"
|
||||||
|
add-label-action="addLabel"
|
||||||
|
remove-label-action="removeLabel"
|
||||||
|
form-action="create"
|
||||||
|
form-action-label="Create the group"
|
||||||
|
action-in-progress="state.actionInProgress"
|
||||||
|
></group-form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Endpoint group details"></rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
<a ui-sref="portainer.groups">Groups</a> > <a ui-sref="portainer.groups.group({id: group.Id})">{{ group.Name }}</a>
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body>
|
||||||
|
<group-form
|
||||||
|
model="group"
|
||||||
|
available-endpoints="availableEndpoints"
|
||||||
|
associated-endpoints="associatedEndpoints"
|
||||||
|
add-label-action="addLabel"
|
||||||
|
remove-label-action="removeLabel"
|
||||||
|
form-action="update"
|
||||||
|
form-action-label="Update the group"
|
||||||
|
action-in-progress="state.actionInProgress"
|
||||||
|
></group-form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,70 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'EndpointService', 'Notifications',
|
||||||
|
function ($q, $scope, $state, $transition$, GroupService, EndpointService, Notifications) {
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
actionInProgress: false
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addLabel = function() {
|
||||||
|
$scope.group.Labels.push({ name: '', value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeLabel = function(index) {
|
||||||
|
$scope.group.Labels.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.update = function() {
|
||||||
|
var model = $scope.group;
|
||||||
|
|
||||||
|
var associatedEndpoints = [];
|
||||||
|
for (var i = 0; i < $scope.associatedEndpoints.length; i++) {
|
||||||
|
var endpoint = $scope.associatedEndpoints[i];
|
||||||
|
associatedEndpoints.push(endpoint.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.state.actionInProgress = true;
|
||||||
|
GroupService.updateGroup(model, associatedEndpoints)
|
||||||
|
.then(function success(data) {
|
||||||
|
Notifications.success('Group successfully updated');
|
||||||
|
$state.go('portainer.groups', {}, {reload: true});
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to update group');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$scope.state.actionInProgress = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
var groupId = $transition$.params().id;
|
||||||
|
|
||||||
|
$q.all({
|
||||||
|
group: GroupService.group(groupId),
|
||||||
|
endpoints: EndpointService.endpoints()
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.group = data.group;
|
||||||
|
|
||||||
|
var availableEndpoints = [];
|
||||||
|
var associatedEndpoints = [];
|
||||||
|
for (var i = 0; i < data.endpoints.length; i++) {
|
||||||
|
var endpoint = data.endpoints[i];
|
||||||
|
if (endpoint.GroupId === +groupId) {
|
||||||
|
associatedEndpoints.push(endpoint);
|
||||||
|
} else if (endpoint.GroupId === 1) {
|
||||||
|
availableEndpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.availableEndpoints = availableEndpoints;
|
||||||
|
$scope.associatedEndpoints = associatedEndpoints;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to load view');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}]);
|
|
@ -0,0 +1,20 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Endpoint groups">
|
||||||
|
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.groups" ui-sref-opts="{reload: true}">
|
||||||
|
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</rd-header-title>
|
||||||
|
<rd-header-content>Endpoint group management</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<groups-datatable
|
||||||
|
title="Endpoint groups" title-icon="fa-object-group"
|
||||||
|
dataset="groups" table-key="groups"
|
||||||
|
order-by="Name" show-text-filter="true"
|
||||||
|
access-management="applicationState.application.authentication"
|
||||||
|
remove-action="removeAction"
|
||||||
|
></groups-datatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,38 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('GroupsController', ['$scope', '$state', '$filter', 'GroupService', 'Notifications',
|
||||||
|
function ($scope, $state, $filter, GroupService, Notifications) {
|
||||||
|
|
||||||
|
$scope.removeAction = function (selectedItems) {
|
||||||
|
var actionCount = selectedItems.length;
|
||||||
|
angular.forEach(selectedItems, function (group) {
|
||||||
|
GroupService.deleteGroup(group.Id)
|
||||||
|
.then(function success() {
|
||||||
|
Notifications.success('Endpoint group successfully removed', group.Name);
|
||||||
|
var index = $scope.groups.indexOf(group);
|
||||||
|
$scope.groups.splice(index, 1);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to remove group');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
--actionCount;
|
||||||
|
if (actionCount === 0) {
|
||||||
|
$state.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
GroupService.groups()
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.groups = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint groups');
|
||||||
|
$scope.groups = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}]);
|
|
@ -31,7 +31,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
var endpointID = 1;
|
var endpointID = 1;
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.createLocalEndpoint(name, URL, false, true)
|
EndpointService.createLocalEndpoint()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
endpointID = data.Id;
|
endpointID = data.Id;
|
||||||
EndpointProvider.setEndpointID(endpointID);
|
EndpointProvider.setEndpointID(endpointID);
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<ul class="sidebar">
|
<ul class="sidebar">
|
||||||
<li class="sidebar-title"><span>Active endpoint</span></li>
|
<endpoint-selector ng-if="endpoints && groups"
|
||||||
<li class="sidebar-title">
|
endpoints="endpoints"
|
||||||
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)">
|
groups="groups"
|
||||||
</select>
|
select-endpoint="switchEndpoint"
|
||||||
</li>
|
></endpoint-selector>
|
||||||
<li class="sidebar-title"><span>Endpoint actions</span></li>
|
<li class="sidebar-title"><span>{{ activeEndpoint.Name }}</span></li>
|
||||||
<docker-sidebar-content
|
<docker-sidebar-content
|
||||||
endpoint-api-version="applicationState.endpoint.apiVersion"
|
endpoint-api-version="applicationState.endpoint.apiVersion"
|
||||||
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
||||||
|
@ -47,6 +47,9 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||||
<a ui-sref="portainer.endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>
|
<a ui-sref="portainer.endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>
|
||||||
|
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.endpoints' || $state.current.name === 'portainer.endpoints.endpoint' || $state.current.name === 'portainer.endpoints.endpoint.access' || $state.current.name === 'portainer.groups' || $state.current.name === 'portainer.groups.group' || $state.current.name === 'portainer.groups.group.access' || $state.current.name === 'portainer.groups.new')">
|
||||||
|
<a ui-sref="portainer.groups" ui-sref-active="active">Groups</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||||
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager',
|
.controller('SidebarController', ['$q', '$scope', '$state', 'EndpointService', 'GroupService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager',
|
||||||
function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) {
|
function ($q, $scope, $state, EndpointService, GroupService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) {
|
||||||
|
|
||||||
$scope.switchEndpoint = function(endpoint) {
|
$scope.switchEndpoint = function(endpoint) {
|
||||||
var activeEndpointID = EndpointProvider.endpointID();
|
var activeEndpointID = EndpointProvider.endpointID();
|
||||||
|
@ -25,16 +25,6 @@ function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointP
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function setActiveEndpoint(endpoints) {
|
|
||||||
var activeEndpointID = EndpointProvider.endpointID();
|
|
||||||
angular.forEach(endpoints, function (endpoint) {
|
|
||||||
if (endpoint.Id === activeEndpointID) {
|
|
||||||
$scope.activeEndpoint = endpoint;
|
|
||||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPermissions(memberships) {
|
function checkPermissions(memberships) {
|
||||||
var isLeader = false;
|
var isLeader = false;
|
||||||
angular.forEach(memberships, function(membership) {
|
angular.forEach(memberships, function(membership) {
|
||||||
|
@ -49,13 +39,24 @@ function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointP
|
||||||
$scope.uiVersion = StateManager.getState().application.version;
|
$scope.uiVersion = StateManager.getState().application.version;
|
||||||
$scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
|
$scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
$scope.endpoints = [];
|
|
||||||
|
|
||||||
EndpointService.endpoints()
|
$q.all({
|
||||||
|
endpoints: EndpointService.endpoints(),
|
||||||
|
groups: GroupService.groups()
|
||||||
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpoints = data;
|
var endpoints = data.endpoints;
|
||||||
$scope.endpoints = _.sortBy(endpoints, ['Name']);
|
$scope.groups = data.groups;
|
||||||
setActiveEndpoint(endpoints);
|
$scope.endpoints = endpoints;
|
||||||
|
|
||||||
|
var activeEndpointID = EndpointProvider.endpointID();
|
||||||
|
for (var i = 0; i < endpoints.length; i++) {
|
||||||
|
var endpoint = endpoints[i];
|
||||||
|
if (endpoint.Id === activeEndpointID) {
|
||||||
|
$scope.activeEndpoint = endpoint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (StateManager.getState().application.authentication) {
|
if (StateManager.getState().application.authentication) {
|
||||||
var userDetails = Authentication.getUserDetails();
|
var userDetails = Authentication.getUserDetails();
|
||||||
|
|
Loading…
Reference in New Issue