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
|
||||
TeamMembershipService *TeamMembershipService
|
||||
EndpointService *EndpointService
|
||||
EndpointGroupService *EndpointGroupService
|
||||
ResourceControlService *ResourceControlService
|
||||
VersionService *VersionService
|
||||
SettingsService *SettingsService
|
||||
|
@ -38,6 +39,7 @@ const (
|
|||
teamBucketName = "teams"
|
||||
teamMembershipBucketName = "team_membership"
|
||||
endpointBucketName = "endpoints"
|
||||
endpointGroupBucketName = "endpoint_groups"
|
||||
resourceControlBucketName = "resource_control"
|
||||
settingsBucketName = "settings"
|
||||
registryBucketName = "registries"
|
||||
|
@ -53,6 +55,7 @@ func NewStore(storePath string) (*Store, error) {
|
|||
TeamService: &TeamService{},
|
||||
TeamMembershipService: &TeamMembershipService{},
|
||||
EndpointService: &EndpointService{},
|
||||
EndpointGroupService: &EndpointGroupService{},
|
||||
ResourceControlService: &ResourceControlService{},
|
||||
VersionService: &VersionService{},
|
||||
SettingsService: &SettingsService{},
|
||||
|
@ -64,6 +67,7 @@ func NewStore(storePath string) (*Store, error) {
|
|||
store.TeamService.store = store
|
||||
store.TeamMembershipService.store = store
|
||||
store.EndpointService.store = store
|
||||
store.EndpointGroupService.store = store
|
||||
store.ResourceControlService.store = store
|
||||
store.VersionService.store = store
|
||||
store.SettingsService.store = store
|
||||
|
@ -94,7 +98,7 @@ func (store *Store) Open() error {
|
|||
store.db = db
|
||||
|
||||
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
|
||||
resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
|
||||
endpointGroupBucketName, resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
|
||||
registryBucketName, dockerhubBucketName, stackBucketName}
|
||||
|
||||
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.
|
||||
func (store *Store) Close() error {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func MarshalStack(stack *portainer.Stack) ([]byte, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -49,6 +49,11 @@ func initStore(dataStorePath string) *bolt.Store {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.MigrateData()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -275,6 +280,7 @@ func main() {
|
|||
TeamService: store.TeamService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
RegistryService: store.RegistryService,
|
||||
|
|
|
@ -28,7 +28,7 @@ const (
|
|||
// TeamMembership errors.
|
||||
const (
|
||||
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.
|
||||
|
@ -44,6 +44,12 @@ const (
|
|||
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.
|
||||
const (
|
||||
ErrRegistryNotFound = Error("Registry not found")
|
||||
|
|
|
@ -20,6 +20,7 @@ type DockerHandler struct {
|
|||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
@ -64,9 +65,17 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
|
|||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
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
|
||||
|
|
|
@ -28,6 +28,7 @@ type EndpointHandler struct {
|
|||
Logger *log.Logger
|
||||
authorizeEndpointManagement bool
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
@ -75,6 +76,7 @@ type (
|
|||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
PublicURL string `valid:"-"`
|
||||
GroupID int `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
TLSSkipVerify bool `valid:"-"`
|
||||
TLSSkipClientVerify bool `valid:"-"`
|
||||
|
@ -84,6 +86,7 @@ type (
|
|||
name string
|
||||
url string
|
||||
publicURL string
|
||||
groupID int
|
||||
useTLS bool
|
||||
skipTLSServerVerification bool
|
||||
skipTLSClientVerification bool
|
||||
|
@ -107,7 +110,13 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
|
|||
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 {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -154,6 +163,7 @@ func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPa
|
|||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.name,
|
||||
URL: payload.url,
|
||||
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||
PublicURL: payload.publicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: payload.useTLS,
|
||||
|
@ -225,6 +235,7 @@ func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPay
|
|||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.name,
|
||||
URL: payload.url,
|
||||
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||
PublicURL: payload.publicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
|
@ -259,6 +270,17 @@ func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload,
|
|||
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"
|
||||
|
||||
if payload.useTLS {
|
||||
|
@ -439,6 +461,10 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
|||
endpoint.PublicURL = req.PublicURL
|
||||
}
|
||||
|
||||
if req.GroupID != 0 {
|
||||
endpoint.GroupID = portainer.EndpointGroupID(req.GroupID)
|
||||
}
|
||||
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
if req.TLS {
|
||||
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
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
@ -64,9 +65,17 @@ func (handler *StoridgeHandler) proxyRequestsToStoridgeAPI(w http.ResponseWriter
|
|||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
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
|
||||
|
|
|
@ -19,6 +19,7 @@ type Handler struct {
|
|||
TeamHandler *TeamHandler
|
||||
TeamMembershipHandler *TeamMembershipHandler
|
||||
EndpointHandler *EndpointHandler
|
||||
EndpointGroupHandler *EndpointGroupHandler
|
||||
RegistryHandler *RegistryHandler
|
||||
DockerHubHandler *DockerHubHandler
|
||||
ExtensionHandler *ExtensionHandler
|
||||
|
@ -51,6 +52,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
||||
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"):
|
||||
switch {
|
||||
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.
|
||||
// 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.
|
||||
func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, membership := range memberships {
|
||||
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
|
||||
if membership.TeamID == authorizedTeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
func AuthorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
||||
}
|
||||
|
||||
// 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
|
||||
// listed in the authorized teams.
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, membership := range memberships {
|
||||
for _, authorizedTeamID := range registry.AuthorizedTeams {
|
||||
for _, authorizedTeamID := range authorizedTeams {
|
||||
if membership.TeamID == authorizedTeamID {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -79,15 +79,17 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques
|
|||
}
|
||||
|
||||
// FilterEndpoints filters endpoints based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized endpoints.
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestContext) ([]portainer.Endpoint, error) {
|
||||
// Non administrator users only have access to authorized endpoints (can be inherited via endoint groups).
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) ([]portainer.Endpoint, error) {
|
||||
filteredEndpoints := endpoints
|
||||
|
||||
if !context.IsAdmin {
|
||||
filteredEndpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -95,3 +97,30 @@ func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestC
|
|||
|
||||
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
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
SettingsService portainer.SettingsService
|
||||
CryptoService portainer.CryptoService
|
||||
|
@ -72,14 +73,19 @@ func (server *Server) Start() error {
|
|||
templatesHandler.SettingsService = server.SettingsService
|
||||
var dockerHandler = handler.NewDockerHandler(requestBouncer)
|
||||
dockerHandler.EndpointService = server.EndpointService
|
||||
dockerHandler.EndpointGroupService = server.EndpointGroupService
|
||||
dockerHandler.TeamMembershipService = server.TeamMembershipService
|
||||
dockerHandler.ProxyManager = proxyManager
|
||||
var websocketHandler = handler.NewWebSocketHandler()
|
||||
websocketHandler.EndpointService = server.EndpointService
|
||||
var endpointHandler = handler.NewEndpointHandler(requestBouncer, server.EndpointManagement)
|
||||
endpointHandler.EndpointService = server.EndpointService
|
||||
endpointHandler.EndpointGroupService = server.EndpointGroupService
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
var endpointGroupHandler = handler.NewEndpointGroupHandler(requestBouncer)
|
||||
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
||||
endpointGroupHandler.EndpointService = server.EndpointService
|
||||
var registryHandler = handler.NewRegistryHandler(requestBouncer)
|
||||
registryHandler.RegistryService = server.RegistryService
|
||||
var dockerHubHandler = handler.NewDockerHubHandler(requestBouncer)
|
||||
|
@ -102,6 +108,7 @@ func (server *Server) Start() error {
|
|||
extensionHandler.ProxyManager = proxyManager
|
||||
var storidgeHandler = extensions.NewStoridgeHandler(requestBouncer)
|
||||
storidgeHandler.EndpointService = server.EndpointService
|
||||
storidgeHandler.EndpointGroupService = server.EndpointGroupService
|
||||
storidgeHandler.TeamMembershipService = server.TeamMembershipService
|
||||
storidgeHandler.ProxyManager = proxyManager
|
||||
|
||||
|
@ -111,6 +118,7 @@ func (server *Server) Start() error {
|
|||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
EndpointHandler: endpointHandler,
|
||||
EndpointGroupHandler: endpointGroupHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
DockerHubHandler: dockerHubHandler,
|
||||
ResourceHandler: resourceHandler,
|
||||
|
|
|
@ -174,6 +174,7 @@ type (
|
|||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
GroupID EndpointGroupID `json:"GroupId"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
|
@ -188,6 +189,19 @@ type (
|
|||
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 struct {
|
||||
Type EndpointExtensionType `json:"Type"`
|
||||
|
@ -248,6 +262,7 @@ type (
|
|||
// DataStore defines the interface to manage the data.
|
||||
DataStore interface {
|
||||
Open() error
|
||||
Init() error
|
||||
Close() error
|
||||
MigrateData() error
|
||||
}
|
||||
|
@ -301,6 +316,15 @@ type (
|
|||
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 interface {
|
||||
Registry(ID RegistryID) (*Registry, error)
|
||||
|
@ -403,7 +427,7 @@ const (
|
|||
// APIVersion is the version number of the Portainer API.
|
||||
APIVersion = "1.16.5"
|
||||
// DBVersion is the version number of the Portainer database.
|
||||
DBVersion = 8
|
||||
DBVersion = 9
|
||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||
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_DOCKERHUB', 'api/dockerhub')
|
||||
.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints')
|
||||
.constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups')
|
||||
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
|
||||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
|
||||
.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 = {
|
||||
name: 'portainer.registries',
|
||||
url: '/registries',
|
||||
|
@ -253,6 +297,10 @@ angular.module('portainer.app', [])
|
|||
$stateRegistryProvider.register(endpoints);
|
||||
$stateRegistryProvider.register(endpoint);
|
||||
$stateRegistryProvider.register(endpointAccess);
|
||||
$stateRegistryProvider.register(groups);
|
||||
$stateRegistryProvider.register(group);
|
||||
$stateRegistryProvider.register(groupAccess);
|
||||
$stateRegistryProvider.register(groupCreation);
|
||||
$stateRegistryProvider.register(registries);
|
||||
$stateRegistryProvider.register(registry);
|
||||
$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',
|
||||
bindings: {
|
||||
accessControlledEntity: '<',
|
||||
inheritFrom: '<',
|
||||
entityType: '@',
|
||||
updateAccess: '&'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,134 +1,46 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users and teams">
|
||||
<div class="pull-md-right pull-lg-right">
|
||||
Items per page:
|
||||
<select ng-model="$ctrl.state.pagination_count_accesses" ng-change="$ctrl.changePaginationCountAccesses()">
|
||||
<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>
|
||||
</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.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 class="col-sm-12 col-md-6 nopadding">
|
||||
<input type="text" id="filter" ng-model="$ctrl.state.filterUsers" 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.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>
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-users" title="Access management"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="small text-muted">
|
||||
<p>You can select which user or team can access this {{ $ctrl.entityType }} by moving them to the authorized accesses table. Simply click
|
||||
on a user or team entry to move it from one table to the other.</p>
|
||||
<p ng-if="$ctrl.inheritFrom">
|
||||
<b>Note</b>: accesses tagged as <code>inherited</code> are inherited from the group accesses and cannot be remove at the endpoint level.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<!-- available-endpoints -->
|
||||
<div class="col-sm-6">
|
||||
<div class="text-center small text-muted">Users and teams</div>
|
||||
<div class="text-center small text-muted" style="margin-top: 5px;">
|
||||
<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>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<access-table
|
||||
dataset="$ctrl.accesses"
|
||||
entry-click="$ctrl.authorizeAccess"
|
||||
empty-dataset-message="No user or team available"
|
||||
></access-table>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users and teams">
|
||||
<div class="pull-md-right pull-lg-right">
|
||||
Items per page:
|
||||
<select ng-model="$ctrl.state.pagination_count_authorizedAccesses" ng-change="$ctrl.changePaginationCountAuthorizedAccesses()">
|
||||
<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>
|
||||
</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>
|
||||
<!-- !available-endpoints -->
|
||||
<!-- associated-endpoints -->
|
||||
<div class="col-sm-6">
|
||||
<div class="text-center small text-muted">Authorized users and teams</div>
|
||||
<div class="text-center small text-muted" style="margin-top: 5px;">
|
||||
<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>
|
||||
<div style="margin-top: 10px;">
|
||||
<access-table
|
||||
dataset="$ctrl.authorizedAccesses"
|
||||
entry-click="$ctrl.unauthorizeAccess"
|
||||
empty-dataset-message="No authorized user or team"
|
||||
></access-table>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !associated-endpoints -->
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -1,40 +1,13 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('porAccessManagementController', ['AccessService', 'PaginationService', 'Notifications',
|
||||
function (AccessService, PaginationService, Notifications) {
|
||||
.controller('porAccessManagementController', ['AccessService', 'Notifications',
|
||||
function (AccessService, Notifications) {
|
||||
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) {
|
||||
angular.forEach(accesses, function (access) {
|
||||
if (access.Type === 'user') {
|
||||
if (access.Type === 'user' && !access.Inherited) {
|
||||
users.push(access.Id);
|
||||
} else if (access.Type === 'team') {
|
||||
} else if (access.Type === 'team' && !access.Inherited) {
|
||||
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.updateAccess({ userAccesses: [], teamAccesses: [] })
|
||||
.then(function success(data) {
|
||||
ctrl.accesses = ctrl.accesses.concat(ctrl.authorizedAccesses);
|
||||
ctrl.authorizedAccesses = [];
|
||||
moveAccesses(ctrl.authorizedAccesses, ctrl.accesses);
|
||||
Notifications.success('Accesses successfully updated');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -130,8 +112,7 @@ function (AccessService, PaginationService, Notifications) {
|
|||
|
||||
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
|
||||
.then(function success(data) {
|
||||
ctrl.authorizedAccesses = ctrl.authorizedAccesses.concat(ctrl.accesses);
|
||||
ctrl.accesses = [];
|
||||
moveAccesses(ctrl.accesses, ctrl.authorizedAccesses);
|
||||
Notifications.success('Accesses successfully updated');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -141,7 +122,8 @@ function (AccessService, PaginationService, Notifications) {
|
|||
|
||||
function initComponent() {
|
||||
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) {
|
||||
ctrl.accesses = data.accesses;
|
||||
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>
|
||||
</a>
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -57,6 +64,7 @@
|
|||
<span ng-if="!$ctrl.endpointManagement">{{ item.Name }}</span>
|
||||
</td>
|
||||
<td>{{ item.URL | stripprotocol }}</td>
|
||||
<td>{{ item.GroupName }}</td>
|
||||
<td>
|
||||
<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
|
||||
|
|
|
@ -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.Name = data.Username;
|
||||
this.Type = 'user';
|
||||
this.Inherited = false;
|
||||
}
|
||||
|
||||
function TeamAccessViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Name = data.Name;
|
||||
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';
|
||||
var service = {};
|
||||
|
||||
function mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs) {
|
||||
var accesses = [];
|
||||
function mapAccessData(accesses, authorizedIDs, inheritedIDs) {
|
||||
var availableAccesses = [];
|
||||
var authorizedAccesses = [];
|
||||
|
||||
angular.forEach(userAccesses, function(access) {
|
||||
if (_.includes(authorizedUserIDs, access.Id)) {
|
||||
authorizedAccesses.push(access);
|
||||
} else {
|
||||
accesses.push(access);
|
||||
}
|
||||
});
|
||||
for (var i = 0; i < accesses.length; i++) {
|
||||
|
||||
angular.forEach(teamAccesses, function(access) {
|
||||
if (_.includes(authorizedTeamIDs, access.Id)) {
|
||||
var access = accesses[i];
|
||||
if (_.includes(inheritedIDs, access.Id)) {
|
||||
access.Inherited = true;
|
||||
authorizedAccesses.push(access);
|
||||
} else if (_.includes(authorizedIDs, access.Id)) {
|
||||
authorizedAccesses.push(access);
|
||||
} else {
|
||||
accesses.push(access);
|
||||
availableAccesses.push(access);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
accesses: accesses,
|
||||
accesses: availableAccesses,
|
||||
authorizedAccesses: authorizedAccesses
|
||||
};
|
||||
}
|
||||
|
||||
service.accesses = function(authorizedUserIDs, authorizedTeamIDs) {
|
||||
service.accesses = function(authorizedUserIDs, authorizedTeamIDs, inheritedUserIDs, inheritedTeamIDs) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
$q.all({
|
||||
|
@ -44,7 +41,14 @@ angular.module('portainer.app')
|
|||
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);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -12,6 +12,23 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
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) {
|
||||
return Endpoints.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
|
||||
};
|
||||
|
@ -20,6 +37,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
var query = {
|
||||
name: endpointParams.name,
|
||||
PublicURL: endpointParams.PublicURL,
|
||||
GroupId: endpointParams.GroupId,
|
||||
TLS: endpointParams.TLS,
|
||||
TLSSkipVerify: endpointParams.TLSSkipVerify,
|
||||
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
|
||||
|
@ -49,10 +67,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
return Endpoints.remove({id: endpointID}).$promise;
|
||||
};
|
||||
|
||||
service.createLocalEndpoint = function(name, URL, TLS, active) {
|
||||
service.createLocalEndpoint = function() {
|
||||
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) {
|
||||
deferred.resolve(response.data);
|
||||
})
|
||||
|
@ -63,10 +81,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
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();
|
||||
|
||||
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) {
|
||||
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({
|
||||
url: 'api/endpoints',
|
||||
data: {
|
||||
Name: name,
|
||||
URL: URL,
|
||||
PublicURL: PublicURL,
|
||||
GroupID: groupID,
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</rd-header>
|
||||
|
||||
<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-header icon="fa-plug" title="Endpoint"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
@ -25,11 +25,9 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<span class="small text-muted">
|
||||
You can select which user or team can access this endpoint by moving them to the authorized accesses table. Simply click
|
||||
on a user or team entry to move it from one table to the other.
|
||||
</span>
|
||||
<td>Group</td>
|
||||
<td>
|
||||
<a ui-sref="portainer.groups.group({ id: group.Id })">{{ group.Name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -39,5 +37,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<por-access-management ng-if="endpoint" access-controlled-entity="endpoint" update-access="updateAccess(userAccesses, teamAccesses)">
|
||||
</por-access-management>
|
||||
<div class="row" ng-if="endpoint && group">
|
||||
<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')
|
||||
.controller('EndpointAccessController', ['$scope', '$transition$', 'EndpointService', 'Notifications',
|
||||
function ($scope, $transition$, EndpointService, Notifications) {
|
||||
.controller('EndpointAccessController', ['$scope', '$transition$', 'EndpointService', 'GroupService', 'Notifications',
|
||||
function ($scope, $transition$, EndpointService, GroupService, Notifications) {
|
||||
|
||||
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
|
||||
return EndpointService.updateAccess($transition$.params().id, authorizedUsers, authorizedTeams);
|
||||
|
@ -9,7 +9,12 @@ function ($scope, $transition$, EndpointService, Notifications) {
|
|||
function initView() {
|
||||
EndpointService.endpoint($transition$.params().id)
|
||||
.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) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
|
|
|
@ -43,6 +43,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !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 -->
|
||||
<div ng-if="endpointType === 'remote'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('EndpointController', ['$scope', '$state', '$transition$', '$filter', 'EndpointService', 'Notifications',
|
||||
function ($scope, $state, $transition$, $filter, EndpointService, Notifications) {
|
||||
.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'EndpointService', 'GroupService', 'EndpointProvider', 'Notifications',
|
||||
function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupService, EndpointProvider, Notifications) {
|
||||
|
||||
if (!$scope.applicationState.application.endpointManagement) {
|
||||
$state.go('portainer.endpoints');
|
||||
|
@ -27,6 +27,7 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
|||
name: endpoint.Name,
|
||||
URL: endpoint.URL,
|
||||
PublicURL: endpoint.PublicURL,
|
||||
GroupId: endpoint.GroupId,
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||
|
@ -40,7 +41,8 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
|||
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||
$state.go('portainer.endpoints');
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
$state.go('portainer.endpoints', {}, {reload: true});
|
||||
}, function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint');
|
||||
$scope.state.actionInProgress = false;
|
||||
|
@ -52,9 +54,12 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
|||
};
|
||||
|
||||
function initView() {
|
||||
EndpointService.endpoint($transition$.params().id)
|
||||
$q.all({
|
||||
endpoint: EndpointService.endpoint($transition$.params().id),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoint = data;
|
||||
var endpoint = data.endpoint;
|
||||
if (endpoint.URL.indexOf('unix://') === 0) {
|
||||
$scope.endpointType = 'local';
|
||||
} else {
|
||||
|
@ -62,6 +67,7 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
|||
}
|
||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.groups = data.groups;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
|
|
|
@ -59,6 +59,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !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 -->
|
||||
<por-endpoint-security form-data="formValues.SecurityFormData"></por-endpoint-security>
|
||||
<!-- !endpoint-security -->
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications',
|
||||
function ($scope, $state, $filter, EndpointService, Notifications) {
|
||||
.controller('EndpointsController', ['$q', '$scope', '$state', '$filter', 'EndpointService', 'GroupService', 'EndpointHelper', 'Notifications',
|
||||
function ($q, $scope, $state, $filter, EndpointService, GroupService, EndpointHelper, Notifications) {
|
||||
$scope.state = {
|
||||
uploadInProgress: false,
|
||||
actionInProgress: false
|
||||
|
@ -10,6 +10,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
|||
Name: '',
|
||||
URL: '',
|
||||
PublicURL: '',
|
||||
GroupId: 1,
|
||||
SecurityFormData: new EndpointSecurityFormData()
|
||||
};
|
||||
|
||||
|
@ -20,6 +21,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
|||
if (PublicURL === '') {
|
||||
PublicURL = URL.split(':')[0];
|
||||
}
|
||||
var groupId = $scope.formValues.GroupId;
|
||||
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
|
@ -31,7 +33,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
|||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||
|
||||
$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() {
|
||||
Notifications.success('Endpoint created', name);
|
||||
$state.reload();
|
||||
|
@ -65,16 +67,22 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
|||
});
|
||||
};
|
||||
|
||||
function fetchEndpoints() {
|
||||
EndpointService.endpoints()
|
||||
function initView() {
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.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) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
$scope.endpoints = [];
|
||||
Notifications.error('Failure', err, 'Unable to load view');
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createLocalEndpoint(name, URL, false, true)
|
||||
EndpointService.createLocalEndpoint()
|
||||
.then(function success(data) {
|
||||
endpointID = data.Id;
|
||||
EndpointProvider.setEndpointID(endpointID);
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
</div>
|
||||
<div class="sidebar-content">
|
||||
<ul class="sidebar">
|
||||
<li class="sidebar-title"><span>Active endpoint</span></li>
|
||||
<li class="sidebar-title">
|
||||
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)">
|
||||
</select>
|
||||
</li>
|
||||
<li class="sidebar-title"><span>Endpoint actions</span></li>
|
||||
<endpoint-selector ng-if="endpoints && groups"
|
||||
endpoints="endpoints"
|
||||
groups="groups"
|
||||
select-endpoint="switchEndpoint"
|
||||
></endpoint-selector>
|
||||
<li class="sidebar-title"><span>{{ activeEndpoint.Name }}</span></li>
|
||||
<docker-sidebar-content
|
||||
endpoint-api-version="applicationState.endpoint.apiVersion"
|
||||
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
||||
|
@ -47,6 +47,9 @@
|
|||
</li>
|
||||
<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>
|
||||
<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 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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager',
|
||||
function ($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, EndpointService, GroupService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) {
|
||||
|
||||
$scope.switchEndpoint = function(endpoint) {
|
||||
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) {
|
||||
var isLeader = false;
|
||||
angular.forEach(memberships, function(membership) {
|
||||
|
@ -49,13 +39,24 @@ function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointP
|
|||
$scope.uiVersion = StateManager.getState().application.version;
|
||||
$scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
$scope.endpoints = [];
|
||||
|
||||
EndpointService.endpoints()
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoints = data;
|
||||
$scope.endpoints = _.sortBy(endpoints, ['Name']);
|
||||
setActiveEndpoint(endpoints);
|
||||
var endpoints = data.endpoints;
|
||||
$scope.groups = data.groups;
|
||||
$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) {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
|
|
Loading…
Reference in New Issue