diff --git a/api/bolt/data_migration.go b/api/bolt/data_migration.go
new file mode 100644
index 000000000..2cc094e24
--- /dev/null
+++ b/api/bolt/data_migration.go
@@ -0,0 +1,76 @@
+package bolt
+
+import (
+ "github.com/boltdb/bolt"
+ "github.com/portainer/portainer"
+)
+
+type Migrator struct {
+ UserService *UserService
+ EndpointService *EndpointService
+ ResourceControlService *ResourceControlService
+ VersionService *VersionService
+ CurrentDBVersion int
+ store *Store
+}
+
+func NewMigrator(store *Store, version int) *Migrator {
+ return &Migrator{
+ UserService: store.UserService,
+ EndpointService: store.EndpointService,
+ ResourceControlService: store.ResourceControlService,
+ VersionService: store.VersionService,
+ CurrentDBVersion: version,
+ store: store,
+ }
+}
+
+func (m *Migrator) Migrate() error {
+
+ // Portainer < 1.12
+ if m.CurrentDBVersion == 0 {
+ err := m.updateAdminUser()
+ if err != nil {
+ return err
+ }
+ }
+
+ err := m.VersionService.StoreDBVersion(portainer.DBVersion)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *Migrator) updateAdminUser() error {
+ u, err := m.UserService.UserByUsername("admin")
+ if err == nil {
+ admin := &portainer.User{
+ Username: "admin",
+ Password: u.Password,
+ Role: portainer.AdministratorRole,
+ }
+ err = m.UserService.CreateUser(admin)
+ if err != nil {
+ return err
+ }
+ err = m.removeLegacyAdminUser()
+ if err != nil {
+ return err
+ }
+ } else if err != nil && err != portainer.ErrUserNotFound {
+ return err
+ }
+ return nil
+}
+
+func (m *Migrator) removeLegacyAdminUser() error {
+ return m.store.db.Update(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(userBucketName))
+ err := bucket.Delete([]byte("admin"))
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+}
diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go
index d0321db21..79e859a5f 100644
--- a/api/bolt/datastore.go
+++ b/api/bolt/datastore.go
@@ -1,9 +1,12 @@
package bolt
import (
+ "log"
+ "os"
"time"
"github.com/boltdb/bolt"
+ "github.com/portainer/portainer"
)
// Store defines the implementation of portainer.DataStore using
@@ -13,29 +16,49 @@ type Store struct {
Path string
// Services
- UserService *UserService
- EndpointService *EndpointService
+ UserService *UserService
+ EndpointService *EndpointService
+ ResourceControlService *ResourceControlService
+ VersionService *VersionService
- db *bolt.DB
+ db *bolt.DB
+ checkForDataMigration bool
}
const (
- databaseFileName = "portainer.db"
- userBucketName = "users"
- endpointBucketName = "endpoints"
- activeEndpointBucketName = "activeEndpoint"
+ databaseFileName = "portainer.db"
+ versionBucketName = "version"
+ userBucketName = "users"
+ endpointBucketName = "endpoints"
+ containerResourceControlBucketName = "containerResourceControl"
+ serviceResourceControlBucketName = "serviceResourceControl"
+ volumeResourceControlBucketName = "volumeResourceControl"
)
// NewStore initializes a new Store and the associated services
-func NewStore(storePath string) *Store {
+func NewStore(storePath string) (*Store, error) {
store := &Store{
- Path: storePath,
- UserService: &UserService{},
- EndpointService: &EndpointService{},
+ Path: storePath,
+ UserService: &UserService{},
+ EndpointService: &EndpointService{},
+ ResourceControlService: &ResourceControlService{},
+ VersionService: &VersionService{},
}
store.UserService.store = store
store.EndpointService.store = store
- return store
+ store.ResourceControlService.store = store
+ store.VersionService.store = store
+
+ _, err := os.Stat(storePath)
+ if err != nil && os.IsNotExist(err) {
+ store.checkForDataMigration = false
+ } else if err != nil {
+ return nil, err
+ } else {
+ store.checkForDataMigration = true
+ }
+
+ return store, nil
}
// Open opens and initializes the BoltDB database.
@@ -47,7 +70,11 @@ func (store *Store) Open() error {
}
store.db = db
return db.Update(func(tx *bolt.Tx) error {
- _, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
+ _, err := tx.CreateBucketIfNotExists([]byte(versionBucketName))
+ if err != nil {
+ return err
+ }
+ _, err = tx.CreateBucketIfNotExists([]byte(userBucketName))
if err != nil {
return err
}
@@ -55,7 +82,15 @@ func (store *Store) Open() error {
if err != nil {
return err
}
- _, err = tx.CreateBucketIfNotExists([]byte(activeEndpointBucketName))
+ _, err = tx.CreateBucketIfNotExists([]byte(containerResourceControlBucketName))
+ if err != nil {
+ return err
+ }
+ _, err = tx.CreateBucketIfNotExists([]byte(serviceResourceControlBucketName))
+ if err != nil {
+ return err
+ }
+ _, err = tx.CreateBucketIfNotExists([]byte(volumeResourceControlBucketName))
if err != nil {
return err
}
@@ -70,3 +105,32 @@ func (store *Store) Close() error {
}
return nil
}
+
+// MigrateData automatically migrate the data based on the DBVersion.
+func (store *Store) MigrateData() error {
+ if !store.checkForDataMigration {
+ err := store.VersionService.StoreDBVersion(portainer.DBVersion)
+ if err != nil {
+ return err
+ }
+ return nil
+ }
+
+ version, err := store.VersionService.DBVersion()
+ if err == portainer.ErrDBVersionNotFound {
+ version = 0
+ } else if err != nil {
+ return err
+ }
+
+ if version < portainer.DBVersion {
+ log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
+ migrator := NewMigrator(store, version)
+ err = migrator.Migrate()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/api/bolt/endpoint_service.go b/api/bolt/endpoint_service.go
index cb8349076..d7c230e9a 100644
--- a/api/bolt/endpoint_service.go
+++ b/api/bolt/endpoint_service.go
@@ -12,10 +12,6 @@ type EndpointService struct {
store *Store
}
-const (
- activeEndpointID = 0
-)
-
// Endpoint returns an endpoint by ID.
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var data []byte
@@ -138,62 +134,6 @@ func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error {
})
}
-// GetActive returns the active endpoint.
-func (service *EndpointService) GetActive() (*portainer.Endpoint, error) {
- var data []byte
- err := service.store.db.View(func(tx *bolt.Tx) error {
- bucket := tx.Bucket([]byte(activeEndpointBucketName))
- value := bucket.Get(internal.Itob(activeEndpointID))
- if value == nil {
- return portainer.ErrEndpointNotFound
- }
-
- data = make([]byte, len(value))
- copy(data, value)
- return nil
- })
- if err != nil {
- return nil, err
- }
-
- var endpoint portainer.Endpoint
- err = internal.UnmarshalEndpoint(data, &endpoint)
- if err != nil {
- return nil, err
- }
- return &endpoint, nil
-}
-
-// SetActive saves an endpoint as active.
-func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error {
- return service.store.db.Update(func(tx *bolt.Tx) error {
- bucket := tx.Bucket([]byte(activeEndpointBucketName))
-
- data, err := internal.MarshalEndpoint(endpoint)
- if err != nil {
- return err
- }
-
- err = bucket.Put(internal.Itob(activeEndpointID), data)
- if err != nil {
- return err
- }
- return nil
- })
-}
-
-// DeleteActive deletes the active endpoint.
-func (service *EndpointService) DeleteActive() error {
- return service.store.db.Update(func(tx *bolt.Tx) error {
- bucket := tx.Bucket([]byte(activeEndpointBucketName))
- err := bucket.Delete(internal.Itob(activeEndpointID))
- if err != nil {
- return err
- }
- return nil
- })
-}
-
func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
@@ -210,6 +150,5 @@ func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket)
func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
-
return marshalAndStoreEndpoint(endpoint, bucket)
}
diff --git a/api/bolt/internal/internal.go b/api/bolt/internal/internal.go
index e9d6416eb..351592963 100644
--- a/api/bolt/internal/internal.go
+++ b/api/bolt/internal/internal.go
@@ -27,6 +27,16 @@ func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
return json.Unmarshal(data, endpoint)
}
+// MarshalResourceControl encodes a resource control object to binary format.
+func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) {
+ return json.Marshal(rc)
+}
+
+// UnmarshalResourceControl decodes a resource control object from a binary data.
+func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error {
+ return json.Unmarshal(data, rc)
+}
+
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
diff --git a/api/bolt/resourcecontrol_service.go b/api/bolt/resourcecontrol_service.go
new file mode 100644
index 000000000..07b174616
--- /dev/null
+++ b/api/bolt/resourcecontrol_service.go
@@ -0,0 +1,110 @@
+package bolt
+
+import (
+ "github.com/portainer/portainer"
+ "github.com/portainer/portainer/bolt/internal"
+
+ "github.com/boltdb/bolt"
+)
+
+// ResourceControlService represents a service for managing resource controls.
+type ResourceControlService struct {
+ store *Store
+}
+
+func getBucketNameByResourceControlType(rcType portainer.ResourceControlType) string {
+ bucketName := containerResourceControlBucketName
+ if rcType == portainer.ServiceResourceControl {
+ bucketName = serviceResourceControlBucketName
+ } else if rcType == portainer.VolumeResourceControl {
+ bucketName = volumeResourceControlBucketName
+ }
+ return bucketName
+}
+
+// ResourceControl returns a resource control object by resource ID
+func (service *ResourceControlService) ResourceControl(resourceID string, rcType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
+ var data []byte
+ bucketName := getBucketNameByResourceControlType(rcType)
+ err := service.store.db.View(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(bucketName))
+ value := bucket.Get([]byte(resourceID))
+ if value == nil {
+ return nil
+ }
+
+ data = make([]byte, len(value))
+ copy(data, value)
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ if data == nil {
+ return nil, nil
+ }
+
+ var rc portainer.ResourceControl
+ err = internal.UnmarshalResourceControl(data, &rc)
+ if err != nil {
+ return nil, err
+ }
+ return &rc, nil
+}
+
+// ResourceControls returns all resource control objects
+func (service *ResourceControlService) ResourceControls(rcType portainer.ResourceControlType) ([]portainer.ResourceControl, error) {
+ var rcs = make([]portainer.ResourceControl, 0)
+ bucketName := getBucketNameByResourceControlType(rcType)
+ err := service.store.db.View(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(bucketName))
+
+ cursor := bucket.Cursor()
+ for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
+ var rc portainer.ResourceControl
+ err := internal.UnmarshalResourceControl(v, &rc)
+ if err != nil {
+ return err
+ }
+ rcs = append(rcs, rc)
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return rcs, nil
+}
+
+// CreateResourceControl creates a new resource control
+func (service *ResourceControlService) CreateResourceControl(resourceID string, rc *portainer.ResourceControl, rcType portainer.ResourceControlType) error {
+ bucketName := getBucketNameByResourceControlType(rcType)
+ return service.store.db.Update(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(bucketName))
+ data, err := internal.MarshalResourceControl(rc)
+ if err != nil {
+ return err
+ }
+
+ err = bucket.Put([]byte(resourceID), data)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+}
+
+// DeleteResourceControl deletes a resource control object by resource ID
+func (service *ResourceControlService) DeleteResourceControl(resourceID string, rcType portainer.ResourceControlType) error {
+ bucketName := getBucketNameByResourceControlType(rcType)
+ return service.store.db.Update(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(bucketName))
+ err := bucket.Delete([]byte(resourceID))
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+}
diff --git a/api/bolt/user_service.go b/api/bolt/user_service.go
index 0171c3e33..1e1c68f40 100644
--- a/api/bolt/user_service.go
+++ b/api/bolt/user_service.go
@@ -12,12 +12,12 @@ type UserService struct {
store *Store
}
-// User returns a user by username.
-func (service *UserService) User(username string) (*portainer.User, error) {
+// User returns a user by ID
+func (service *UserService) User(ID portainer.UserID) (*portainer.User, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
- value := bucket.Get([]byte(username))
+ value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrUserNotFound
}
@@ -38,8 +38,88 @@ func (service *UserService) User(username string) (*portainer.User, error) {
return &user, nil
}
+// UserByUsername returns a user by username.
+func (service *UserService) UserByUsername(username string) (*portainer.User, error) {
+ var user *portainer.User
+
+ err := service.store.db.View(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(userBucketName))
+ cursor := bucket.Cursor()
+ for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
+ var u portainer.User
+ err := internal.UnmarshalUser(v, &u)
+ if err != nil {
+ return err
+ }
+ if u.Username == username {
+ user = &u
+ }
+ }
+
+ if user == nil {
+ return portainer.ErrUserNotFound
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return user, nil
+}
+
+// Users return an array containing all the users.
+func (service *UserService) Users() ([]portainer.User, error) {
+ var users = make([]portainer.User, 0)
+ err := service.store.db.View(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(userBucketName))
+
+ cursor := bucket.Cursor()
+ for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
+ var user portainer.User
+ err := internal.UnmarshalUser(v, &user)
+ if err != nil {
+ return err
+ }
+ users = append(users, user)
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return users, nil
+}
+
+// UsersByRole return an array containing all the users with the specified role.
+func (service *UserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
+ var users = make([]portainer.User, 0)
+ err := service.store.db.View(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(userBucketName))
+
+ cursor := bucket.Cursor()
+ for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
+ var user portainer.User
+ err := internal.UnmarshalUser(v, &user)
+ if err != nil {
+ return err
+ }
+ if user.Role == role {
+ users = append(users, user)
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return users, nil
+}
+
// UpdateUser saves a user.
-func (service *UserService) UpdateUser(user *portainer.User) error {
+func (service *UserService) UpdateUser(ID portainer.UserID, user *portainer.User) error {
data, err := internal.MarshalUser(user)
if err != nil {
return err
@@ -47,7 +127,41 @@ func (service *UserService) UpdateUser(user *portainer.User) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
- err = bucket.Put([]byte(user.Username), data)
+ err = bucket.Put(internal.Itob(int(ID)), data)
+
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+}
+
+// CreateUser creates a new user.
+func (service *UserService) CreateUser(user *portainer.User) error {
+ return service.store.db.Update(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(userBucketName))
+
+ id, _ := bucket.NextSequence()
+ user.ID = portainer.UserID(id)
+
+ data, err := internal.MarshalUser(user)
+ if err != nil {
+ return err
+ }
+
+ err = bucket.Put(internal.Itob(int(user.ID)), data)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+}
+
+// DeleteUser deletes a user.
+func (service *UserService) DeleteUser(ID portainer.UserID) error {
+ return service.store.db.Update(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(userBucketName))
+ err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
diff --git a/api/bolt/version_service.go b/api/bolt/version_service.go
new file mode 100644
index 000000000..2da511437
--- /dev/null
+++ b/api/bolt/version_service.go
@@ -0,0 +1,58 @@
+package bolt
+
+import (
+ "strconv"
+
+ "github.com/portainer/portainer"
+
+ "github.com/boltdb/bolt"
+)
+
+// EndpointService represents a service for managing users.
+type VersionService struct {
+ store *Store
+}
+
+const (
+ DBVersionKey = "DB_VERSION"
+)
+
+// DBVersion the stored database version.
+func (service *VersionService) DBVersion() (int, error) {
+ var data []byte
+ err := service.store.db.View(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(versionBucketName))
+ value := bucket.Get([]byte(DBVersionKey))
+ if value == nil {
+ return portainer.ErrDBVersionNotFound
+ }
+
+ data = make([]byte, len(value))
+ copy(data, value)
+ return nil
+ })
+ if err != nil {
+ return 0, err
+ }
+
+ dbVersion, err := strconv.Atoi(string(data))
+ if err != nil {
+ return 0, err
+ }
+
+ return dbVersion, nil
+}
+
+// StoreDBVersion store the database version.
+func (service *VersionService) StoreDBVersion(version int) error {
+ return service.store.db.Update(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(versionBucketName))
+
+ data := []byte(strconv.Itoa(version))
+ err := bucket.Put([]byte(DBVersionKey), data)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+}
diff --git a/api/cli/cli.go b/api/cli/cli.go
index ead424779..6148fdc95 100644
--- a/api/cli/cli.go
+++ b/api/cli/cli.go
@@ -29,14 +29,15 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
flags := &portainer.CLIFlags{
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
+ Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
- Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
+ NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
diff --git a/api/cli/defaults.go b/api/cli/defaults.go
index 4545ecf79..160b74808 100644
--- a/api/cli/defaults.go
+++ b/api/cli/defaults.go
@@ -8,6 +8,7 @@ const (
defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false"
+ defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go
index 17c6c3776..cbd0555a8 100644
--- a/api/cli/defaults_windows.go
+++ b/api/cli/defaults_windows.go
@@ -6,6 +6,7 @@ const (
defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false"
+ defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go
index 423c554cc..6ebdf1ee1 100644
--- a/api/cmd/portainer/main.go
+++ b/api/cmd/portainer/main.go
@@ -36,8 +36,17 @@ func initFileService(dataStorePath string) portainer.FileService {
}
func initStore(dataStorePath string) *bolt.Store {
- var store = bolt.NewStore(dataStorePath)
- err := store.Open()
+ store, err := bolt.NewStore(dataStorePath)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = store.Open()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = store.MigrateData()
if err != nil {
log.Fatal(err)
}
@@ -77,6 +86,7 @@ func initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portai
return &portainer.Settings{
HiddenLabels: *flags.Labels,
Logo: *flags.Logo,
+ Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
}
@@ -90,31 +100,6 @@ func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService
return &endpoints[0]
}
-func initActiveEndpoint(endpointService portainer.EndpointService, flags *portainer.CLIFlags) *portainer.Endpoint {
- activeEndpoint, err := endpointService.GetActive()
- if err == portainer.ErrEndpointNotFound {
- if *flags.Endpoint != "" {
- activeEndpoint = &portainer.Endpoint{
- Name: "primary",
- URL: *flags.Endpoint,
- TLS: *flags.TLSVerify,
- TLSCACertPath: *flags.TLSCacert,
- TLSCertPath: *flags.TLSCert,
- TLSKeyPath: *flags.TLSKey,
- }
- err = endpointService.CreateEndpoint(activeEndpoint)
- if err != nil {
- log.Fatal(err)
- }
- } else if *flags.ExternalEndpoints != "" {
- activeEndpoint = retrieveFirstEndpointFromDatabase(endpointService)
- }
- } else if err != nil {
- log.Fatal(err)
- }
- return activeEndpoint
-}
-
func main() {
flags := initCLI()
@@ -131,21 +116,43 @@ func main() {
settings := initSettings(authorizeEndpointMgmt, flags)
- activeEndpoint := initActiveEndpoint(store.EndpointService, flags)
+ if *flags.Endpoint != "" {
+ var endpoints []portainer.Endpoint
+ endpoints, err := store.EndpointService.Endpoints()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if len(endpoints) == 0 {
+ endpoint := &portainer.Endpoint{
+ Name: "primary",
+ URL: *flags.Endpoint,
+ TLS: *flags.TLSVerify,
+ TLSCACertPath: *flags.TLSCacert,
+ TLSCertPath: *flags.TLSCert,
+ TLSKeyPath: *flags.TLSKey,
+ }
+ err = store.EndpointService.CreateEndpoint(endpoint)
+ if err != nil {
+ log.Fatal(err)
+ }
+ } else {
+ log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
+ }
+ }
var server portainer.Server = &http.Server{
- BindAddress: *flags.Addr,
- AssetsPath: *flags.Assets,
- Settings: settings,
- TemplatesURL: *flags.Templates,
- AuthDisabled: *flags.NoAuth,
- EndpointManagement: authorizeEndpointMgmt,
- UserService: store.UserService,
- EndpointService: store.EndpointService,
- CryptoService: cryptoService,
- JWTService: jwtService,
- FileService: fileService,
- ActiveEndpoint: activeEndpoint,
+ BindAddress: *flags.Addr,
+ AssetsPath: *flags.Assets,
+ Settings: settings,
+ TemplatesURL: *flags.Templates,
+ AuthDisabled: *flags.NoAuth,
+ EndpointManagement: authorizeEndpointMgmt,
+ UserService: store.UserService,
+ EndpointService: store.EndpointService,
+ ResourceControlService: store.ResourceControlService,
+ CryptoService: cryptoService,
+ JWTService: jwtService,
+ FileService: fileService,
}
log.Printf("Starting Portainer on %s", *flags.Addr)
diff --git a/api/cron/endpoint_sync.go b/api/cron/endpoint_sync.go
index 9dcd4e290..3401c6893 100644
--- a/api/cron/endpoint_sync.go
+++ b/api/cron/endpoint_sync.go
@@ -66,7 +66,10 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var endpoint *portainer.Endpoint
- if original.URL != updated.URL || original.TLS != updated.TLS {
+ if original.URL != updated.URL || original.TLS != updated.TLS ||
+ (updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) ||
+ (updated.TLS && original.TLSCertPath != updated.TLSCertPath) ||
+ (updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) {
endpoint = original
endpoint.URL = updated.URL
if updated.TLS {
diff --git a/api/errors.go b/api/errors.go
index 48ea6ab75..14dbc3156 100644
--- a/api/errors.go
+++ b/api/errors.go
@@ -2,19 +2,26 @@ package portainer
// General errors.
const (
- ErrUnauthorized = Error("Unauthorized")
+ ErrUnauthorized = Error("Unauthorized")
+ ErrResourceAccessDenied = Error("Access denied to resource")
)
// User errors.
const (
ErrUserNotFound = Error("User not found")
+ ErrUserAlreadyExists = Error("User already exists")
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
)
// Endpoint errors.
const (
- ErrEndpointNotFound = Error("Endpoint not found")
- ErrNoActiveEndpoint = Error("Undefined Docker endpoint")
+ ErrEndpointNotFound = Error("Endpoint not found")
+ ErrEndpointAccessDenied = Error("Access denied to endpoint")
+)
+
+// Version errors.
+const (
+ ErrDBVersionNotFound = Error("DB version not found")
)
// Crypto errors.
@@ -24,8 +31,9 @@ const (
// JWT errors.
const (
- ErrSecretGeneration = Error("Unable to generate secret key")
- ErrInvalidJWTToken = Error("Invalid JWT token")
+ ErrSecretGeneration = Error("Unable to generate secret key")
+ ErrInvalidJWTToken = Error("Invalid JWT token")
+ ErrMissingContextData = Error("Unable to find JWT data in request context")
)
// File errors.
diff --git a/api/http/auth_handler.go b/api/http/auth_handler.go
index b4c1af789..0eb0e7559 100644
--- a/api/http/auth_handler.go
+++ b/api/http/auth_handler.go
@@ -33,12 +33,14 @@ const (
)
// NewAuthHandler returns a new instance of AuthHandler.
-func NewAuthHandler() *AuthHandler {
+func NewAuthHandler(mw *middleWareService) *AuthHandler {
h := &AuthHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
- h.HandleFunc("/auth", h.handlePostAuth)
+ h.Handle("/auth",
+ mw.public(http.HandlerFunc(h.handlePostAuth)))
+
return h
}
@@ -68,7 +70,7 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
var username = req.Username
var password = req.Password
- u, err := handler.UserService.User(username)
+ u, err := handler.UserService.UserByUsername(username)
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
@@ -84,7 +86,9 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
}
tokenData := &portainer.TokenData{
- username,
+ ID: u.ID,
+ Username: u.Username,
+ Role: u.Role,
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
diff --git a/api/http/docker_handler.go b/api/http/docker_handler.go
index 9cffa4b26..5a8668a3a 100644
--- a/api/http/docker_handler.go
+++ b/api/http/docker_handler.go
@@ -1,16 +1,14 @@
package http
import (
+ "strconv"
+
"github.com/portainer/portainer"
- "io"
"log"
- "net"
"net/http"
- "net/http/httputil"
"net/url"
"os"
- "strings"
"github.com/gorilla/mux"
)
@@ -18,142 +16,95 @@ import (
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
- Logger *log.Logger
- middleWareService *middleWareService
- proxy http.Handler
+ Logger *log.Logger
+ EndpointService portainer.EndpointService
+ ProxyFactory ProxyFactory
+ proxies map[portainer.EndpointID]http.Handler
}
// NewDockerHandler returns a new instance of DockerHandler.
-func NewDockerHandler(middleWareService *middleWareService) *DockerHandler {
+func NewDockerHandler(mw *middleWareService, resourceControlService portainer.ResourceControlService) *DockerHandler {
h := &DockerHandler{
- Router: mux.NewRouter(),
- Logger: log.New(os.Stderr, "", log.LstdFlags),
- middleWareService: middleWareService,
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
+ ProxyFactory: ProxyFactory{
+ ResourceControlService: resourceControlService,
+ },
+ proxies: make(map[portainer.EndpointID]http.Handler),
}
- h.PathPrefix("/").Handler(middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.proxyRequestsToDockerAPI(w, r)
- })))
+ h.PathPrefix("/{id}/").Handler(
+ mw.authenticated(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
return h
}
+func checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
+ for _, authorizedUserID := range endpoint.AuthorizedUsers {
+ if authorizedUserID == userID {
+ return true
+ }
+ }
+ return false
+}
+
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
- if handler.proxy != nil {
- handler.proxy.ServeHTTP(w, r)
- } else {
- Error(w, portainer.ErrNoActiveEndpoint, http.StatusNotFound, handler.Logger)
- }
-}
+ vars := mux.Vars(r)
+ id := vars["id"]
-func (handler *DockerHandler) setupProxy(endpoint *portainer.Endpoint) error {
- var proxy http.Handler
- endpointURL, err := url.Parse(endpoint.URL)
+ parsedID, err := strconv.Atoi(id)
if err != nil {
- return err
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
}
- if endpointURL.Scheme == "tcp" {
- if endpoint.TLS {
- proxy, err = newHTTPSProxy(endpointURL, endpoint)
- if err != nil {
- return err
- }
- } else {
- proxy = newHTTPProxy(endpointURL)
- }
- } else {
- // Assume unix:// scheme
- proxy = newSocketProxy(endpointURL.Path)
- }
- handler.proxy = proxy
- return nil
-}
-// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
-// included here for use in NewSingleHostReverseProxyWithHostHeader
-// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
-func singleJoiningSlash(a, b string) string {
- aslash := strings.HasSuffix(a, "/")
- bslash := strings.HasPrefix(b, "/")
- switch {
- case aslash && bslash:
- return a + b[1:]
- case !aslash && !bslash:
- return a + "/" + b
+ endpointID := portainer.EndpointID(parsedID)
+ endpoint, err := handler.EndpointService.Endpoint(endpointID)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
}
- return a + b
-}
-// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
-// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
-// HTTP header, which NewSingleHostReverseProxy deliberately preserves
-func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
- targetQuery := target.RawQuery
- director := func(req *http.Request) {
- req.URL.Scheme = target.Scheme
- req.URL.Host = target.Host
- req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
- req.Host = req.URL.Host
- if targetQuery == "" || req.URL.RawQuery == "" {
- req.URL.RawQuery = targetQuery + req.URL.RawQuery
- } else {
- req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
- }
- if _, ok := req.Header["User-Agent"]; !ok {
- // explicitly disable User-Agent so it's not set to default value
- req.Header.Set("User-Agent", "")
+ tokenData, err := extractTokenDataFromRequestContext(r)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ }
+ if tokenData.Role != portainer.AdministratorRole && !checkEndpointAccessControl(endpoint, tokenData.ID) {
+ Error(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
+ return
+ }
+
+ proxy := handler.proxies[endpointID]
+ if proxy == nil {
+ proxy, err = handler.createAndRegisterEndpointProxy(endpoint)
+ if err != nil {
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
}
}
- return &httputil.ReverseProxy{Director: director}
+ http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
}
-func newHTTPProxy(u *url.URL) http.Handler {
- u.Scheme = "http"
- return NewSingleHostReverseProxyWithHostHeader(u)
-}
+func (handler *DockerHandler) createAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
+ var proxy http.Handler
-func newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
- u.Scheme = "https"
- proxy := NewSingleHostReverseProxyWithHostHeader(u)
- config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
+ endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return nil, err
}
- proxy.Transport = &http.Transport{
- TLSClientConfig: config,
+
+ if endpointURL.Scheme == "tcp" {
+ if endpoint.TLS {
+ proxy, err = handler.ProxyFactory.newHTTPSProxy(endpointURL, endpoint)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ proxy = handler.ProxyFactory.newHTTPProxy(endpointURL)
+ }
+ } else {
+ // Assume unix:// scheme
+ proxy = handler.ProxyFactory.newSocketProxy(endpointURL.Path)
}
+
+ handler.proxies[endpoint.ID] = proxy
return proxy, nil
}
-
-func newSocketProxy(path string) http.Handler {
- return &unixSocketHandler{path}
-}
-
-// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
-type unixSocketHandler struct {
- path string
-}
-
-func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- conn, err := net.Dial("unix", h.path)
- if err != nil {
- Error(w, err, http.StatusInternalServerError, nil)
- return
- }
- c := httputil.NewClientConn(conn, nil)
- defer c.Close()
-
- res, err := c.Do(r)
- if err != nil {
- Error(w, err, http.StatusInternalServerError, nil)
- return
- }
- defer res.Body.Close()
-
- for k, vv := range res.Header {
- for _, v := range vv {
- w.Header().Add(k, v)
- }
- }
- if _, err := io.Copy(w, res.Body); err != nil {
- Error(w, err, http.StatusInternalServerError, nil)
- }
-}
diff --git a/api/http/docker_proxy.go b/api/http/docker_proxy.go
new file mode 100644
index 000000000..f4644dd36
--- /dev/null
+++ b/api/http/docker_proxy.go
@@ -0,0 +1,121 @@
+package http
+
+import (
+ "io"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "strings"
+
+ "github.com/portainer/portainer"
+)
+
+// ProxyFactory is a factory to create reverse proxies to Docker endpoints
+type ProxyFactory struct {
+ ResourceControlService portainer.ResourceControlService
+}
+
+// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
+// included here for use in NewSingleHostReverseProxyWithHostHeader
+// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
+func singleJoiningSlash(a, b string) string {
+ aslash := strings.HasSuffix(a, "/")
+ bslash := strings.HasPrefix(b, "/")
+ switch {
+ case aslash && bslash:
+ return a + b[1:]
+ case !aslash && !bslash:
+ return a + "/" + b
+ }
+ return a + b
+}
+
+// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
+// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
+// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
+// It also adds an extra Transport to the proxy to allow Portainer to rewrite the responses.
+func (factory *ProxyFactory) newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
+ targetQuery := target.RawQuery
+ director := func(req *http.Request) {
+ req.URL.Scheme = target.Scheme
+ req.URL.Host = target.Host
+ req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
+ req.Host = req.URL.Host
+ if targetQuery == "" || req.URL.RawQuery == "" {
+ req.URL.RawQuery = targetQuery + req.URL.RawQuery
+ } else {
+ req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
+ }
+ if _, ok := req.Header["User-Agent"]; !ok {
+ // explicitly disable User-Agent so it's not set to default value
+ req.Header.Set("User-Agent", "")
+ }
+ }
+ transport := &proxyTransport{
+ ResourceControlService: factory.ResourceControlService,
+ transport: &http.Transport{},
+ }
+ return &httputil.ReverseProxy{Director: director, Transport: transport}
+}
+
+func (factory *ProxyFactory) newHTTPProxy(u *url.URL) http.Handler {
+ u.Scheme = "http"
+ return factory.newSingleHostReverseProxyWithHostHeader(u)
+}
+
+func (factory *ProxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
+ u.Scheme = "https"
+ proxy := factory.newSingleHostReverseProxyWithHostHeader(u)
+ config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
+ if err != nil {
+ return nil, err
+ }
+
+ proxy.Transport.(*proxyTransport).transport.TLSClientConfig = config
+ return proxy, nil
+}
+
+func (factory *ProxyFactory) newSocketProxy(path string) http.Handler {
+ return &unixSocketHandler{path, &proxyTransport{
+ ResourceControlService: factory.ResourceControlService,
+ }}
+}
+
+// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
+type unixSocketHandler struct {
+ path string
+ transport *proxyTransport
+}
+
+func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ conn, err := net.Dial("unix", h.path)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, nil)
+ return
+ }
+ c := httputil.NewClientConn(conn, nil)
+ defer c.Close()
+
+ res, err := c.Do(r)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, nil)
+ return
+ }
+ defer res.Body.Close()
+
+ err = h.transport.proxyDockerRequests(r, res)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, nil)
+ return
+ }
+
+ for k, vv := range res.Header {
+ for _, v := range vv {
+ w.Header().Add(k, v)
+ }
+ }
+ if _, err := io.Copy(w, res.Body); err != nil {
+ Error(w, err, http.StatusInternalServerError, nil)
+ }
+}
diff --git a/api/http/endpoint_handler.go b/api/http/endpoint_handler.go
index 7c7d0f930..5fac67276 100644
--- a/api/http/endpoint_handler.go
+++ b/api/http/endpoint_handler.go
@@ -20,8 +20,7 @@ type EndpointHandler struct {
authorizeEndpointManagement bool
EndpointService portainer.EndpointService
FileService portainer.FileService
- server *Server
- middleWareService *middleWareService
+ // server *Server
}
const (
@@ -31,30 +30,24 @@ const (
)
// NewEndpointHandler returns a new instance of EndpointHandler.
-func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
+func NewEndpointHandler(mw *middleWareService) *EndpointHandler {
h := &EndpointHandler{
- Router: mux.NewRouter(),
- Logger: log.New(os.Stderr, "", log.LstdFlags),
- middleWareService: middleWareService,
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
}
- h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handlePostEndpoints(w, r)
- }))).Methods(http.MethodPost)
- h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handleGetEndpoints(w, r)
- }))).Methods(http.MethodGet)
- h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handleGetEndpoint(w, r)
- }))).Methods(http.MethodGet)
- h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handlePutEndpoint(w, r)
- }))).Methods(http.MethodPut)
- h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handleDeleteEndpoint(w, r)
- }))).Methods(http.MethodDelete)
- h.Handle("/endpoints/{id}/active", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handlePostEndpoint(w, r)
- }))).Methods(http.MethodPost)
+ h.Handle("/endpoints",
+ mw.administrator(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
+ h.Handle("/endpoints",
+ mw.authenticated(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
+ h.Handle("/endpoints/{id}",
+ mw.administrator(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
+ h.Handle("/endpoints/{id}",
+ mw.administrator(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
+ h.Handle("/endpoints/{id}/access",
+ mw.administrator(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
+ h.Handle("/endpoints/{id}",
+ mw.administrator(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
+
return h
}
@@ -65,12 +58,35 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
- encodeJSON(w, endpoints, handler.Logger)
+
+ tokenData, err := extractTokenDataFromRequestContext(r)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ }
+ if tokenData == nil {
+ Error(w, portainer.ErrInvalidJWTToken, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ var allowedEndpoints []portainer.Endpoint
+ if tokenData.Role != portainer.AdministratorRole {
+ allowedEndpoints = make([]portainer.Endpoint, 0)
+ for _, endpoint := range endpoints {
+ for _, authorizedUserID := range endpoint.AuthorizedUsers {
+ if authorizedUserID == tokenData.ID {
+ allowedEndpoints = append(allowedEndpoints, endpoint)
+ break
+ }
+ }
+ }
+ } else {
+ allowedEndpoints = endpoints
+ }
+
+ encodeJSON(w, allowedEndpoints, handler.Logger)
}
// handlePostEndpoints handles POST requests on /endpoints
-// if the active URL parameter is specified, will also define the new endpoint as the active endpoint.
-// /endpoints(?active=true|false)
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
@@ -90,9 +106,10 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
}
endpoint := &portainer.Endpoint{
- Name: req.Name,
- URL: req.URL,
- TLS: req.TLS,
+ Name: req.Name,
+ URL: req.URL,
+ TLS: req.TLS,
+ AuthorizedUsers: []portainer.UserID{},
}
err = handler.EndpointService.CreateEndpoint(endpoint)
@@ -115,22 +132,6 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
}
}
- activeEndpointParameter := r.FormValue("active")
- if activeEndpointParameter != "" {
- active, err := strconv.ParseBool(activeEndpointParameter)
- if err != nil {
- Error(w, err, http.StatusBadRequest, handler.Logger)
- return
- }
- if active == true {
- err = handler.server.updateActiveEndpoint(endpoint)
- if err != nil {
- Error(w, err, http.StatusInternalServerError, handler.Logger)
- return
- }
- }
- }
-
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
}
@@ -145,7 +146,6 @@ type postEndpointsResponse struct {
}
// handleGetEndpoint handles GET requests on /endpoints/:id
-// GET /endpoints/0 returns active endpoint
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
@@ -156,48 +156,6 @@ func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http
return
}
- var endpoint *portainer.Endpoint
- if id == "0" {
- endpoint, err = handler.EndpointService.GetActive()
- if err == portainer.ErrEndpointNotFound {
- Error(w, err, http.StatusNotFound, handler.Logger)
- return
- } else if err != nil {
- Error(w, err, http.StatusInternalServerError, handler.Logger)
- return
- }
- if handler.server.ActiveEndpoint == nil {
- err = handler.server.updateActiveEndpoint(endpoint)
- if err != nil {
- Error(w, err, http.StatusInternalServerError, handler.Logger)
- return
- }
- }
- } else {
- endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
- if err == portainer.ErrEndpointNotFound {
- Error(w, err, http.StatusNotFound, handler.Logger)
- return
- } else if err != nil {
- Error(w, err, http.StatusInternalServerError, handler.Logger)
- return
- }
- }
-
- encodeJSON(w, endpoint, handler.Logger)
-}
-
-// handlePostEndpoint handles POST requests on /endpoints/:id/active
-func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- id := vars["id"]
-
- endpointID, err := strconv.Atoi(id)
- if err != nil {
- Error(w, err, http.StatusBadRequest, handler.Logger)
- return
- }
-
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
@@ -207,12 +165,58 @@ func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *htt
return
}
- err = handler.server.updateActiveEndpoint(endpoint)
+ encodeJSON(w, endpoint, handler.Logger)
+}
+
+// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
+func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ endpointID, err := strconv.Atoi(id)
+ if err != nil {
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ var req putEndpointAccessRequest
+ if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
+ Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ _, err = govalidator.ValidateStruct(req)
+ if err != nil {
+ Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
+ if err == portainer.ErrEndpointNotFound {
+ Error(w, err, http.StatusNotFound, handler.Logger)
+ return
+ } else if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+
+ authorizedUserIDs := []portainer.UserID{}
+ for _, value := range req.AuthorizedUsers {
+ authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
+ }
+ endpoint.AuthorizedUsers = authorizedUserIDs
+
+ err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
}
}
+type putEndpointAccessRequest struct {
+ AuthorizedUsers []int `valid:"required"`
+}
+
// handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
@@ -241,14 +245,25 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
return
}
- endpoint := &portainer.Endpoint{
- ID: portainer.EndpointID(endpointID),
- Name: req.Name,
- URL: req.URL,
- TLS: req.TLS,
+ endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
+ if err == portainer.ErrEndpointNotFound {
+ Error(w, err, http.StatusNotFound, handler.Logger)
+ return
+ } else if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+
+ if req.Name != "" {
+ endpoint.Name = req.Name
+ }
+
+ if req.URL != "" {
+ endpoint.URL = req.URL
}
if req.TLS {
+ endpoint.TLS = true
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
endpoint.TLSCACertPath = caCertPath
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
@@ -256,6 +271,10 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
endpoint.TLSKeyPath = keyPath
} else {
+ endpoint.TLS = false
+ endpoint.TLSCACertPath = ""
+ endpoint.TLSCertPath = ""
+ endpoint.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
@@ -271,13 +290,12 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
}
type putEndpointsRequest struct {
- Name string `valid:"required"`
- URL string `valid:"required"`
- TLS bool
+ Name string `valid:"-"`
+ URL string `valid:"-"`
+ TLS bool `valid:"-"`
}
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
-// DELETE /endpoints/0 deletes the active endpoint
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
@@ -293,13 +311,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
return
}
- var endpoint *portainer.Endpoint
- if id == "0" {
- endpoint, err = handler.EndpointService.GetActive()
- endpointID = int(endpoint.ID)
- } else {
- endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
- }
+ endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
@@ -314,13 +326,6 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
- if id == "0" {
- err = handler.EndpointService.DeleteActive()
- if err != nil {
- Error(w, err, http.StatusInternalServerError, handler.Logger)
- return
- }
- }
if endpoint.TLS {
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
diff --git a/api/http/handler.go b/api/http/handler.go
index efce098b5..7a27925c7 100644
--- a/api/http/handler.go
+++ b/api/http/handler.go
@@ -27,6 +27,10 @@ const (
ErrInvalidJSON = portainer.Error("Invalid JSON")
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
+ // ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
+ ErrInvalidQueryFormat = portainer.Error("Invalid query format")
+ // ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse
+ ErrEmptyResponseBody = portainer.Error("Empty response body")
)
// ServeHTTP delegates a request to the appropriate subhandler.
diff --git a/api/http/middleware.go b/api/http/middleware.go
index 891e7c610..4221a61f4 100644
--- a/api/http/middleware.go
+++ b/api/http/middleware.go
@@ -1,33 +1,61 @@
package http
import (
+ "context"
+
"github.com/portainer/portainer"
"net/http"
"strings"
)
-// Service represents a service to manage HTTP middlewares
-type middleWareService struct {
- jwtService portainer.JWTService
- authDisabled bool
-}
-
-func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
- for _, mw := range middleware {
- h = mw(h)
+type (
+ // middleWareService represents a service to manage HTTP middlewares
+ middleWareService struct {
+ jwtService portainer.JWTService
+ authDisabled bool
}
+ contextKey int
+)
+
+const (
+ contextAuthenticationKey contextKey = iota
+)
+
+func extractTokenDataFromRequestContext(request *http.Request) (*portainer.TokenData, error) {
+ contextData := request.Context().Value(contextAuthenticationKey)
+ if contextData == nil {
+ return nil, portainer.ErrMissingContextData
+ }
+
+ tokenData := contextData.(*portainer.TokenData)
+ return tokenData, nil
+}
+
+// public defines a chain of middleware for public endpoints (no authentication required)
+func (service *middleWareService) public(h http.Handler) http.Handler {
+ h = mwSecureHeaders(h)
return h
}
-func (service *middleWareService) addMiddleWares(h http.Handler) http.Handler {
- h = service.middleWareSecureHeaders(h)
- h = service.middleWareAuthenticate(h)
+// authenticated defines a chain of middleware for private endpoints (authentication required)
+func (service *middleWareService) authenticated(h http.Handler) http.Handler {
+ h = service.mwCheckAuthentication(h)
+ h = mwSecureHeaders(h)
return h
}
-// middleWareAuthenticate provides secure headers middleware for handlers
-func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handler {
+// administrator defines a chain of middleware for private administrator restricted endpoints
+// (authentication and role admin required)
+func (service *middleWareService) administrator(h http.Handler) http.Handler {
+ h = mwCheckAdministratorRole(h)
+ h = service.mwCheckAuthentication(h)
+ h = mwSecureHeaders(h)
+ return h
+}
+
+// mwSecureHeaders provides secure headers middleware for handlers
+func mwSecureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-Frame-Options", "DENY")
@@ -35,9 +63,28 @@ func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handle
})
}
-// middleWareAuthenticate provides Authentication middleware for handlers
-func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler {
+// mwCheckAdministratorRole check the role of the user associated to the request
+func mwCheckAdministratorRole(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tokenData, err := extractTokenDataFromRequestContext(r)
+ if err != nil {
+ Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
+ return
+ }
+
+ if tokenData.Role != portainer.AdministratorRole {
+ Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
+
+// mwCheckAuthentication provides Authentication middleware for handlers
+func (service *middleWareService) mwCheckAuthentication(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var tokenData *portainer.TokenData
if !service.authDisabled {
var token string
@@ -53,14 +100,20 @@ func (service *middleWareService) middleWareAuthenticate(next http.Handler) http
return
}
- err := service.jwtService.VerifyToken(token)
+ var err error
+ tokenData, err = service.jwtService.ParseAndVerifyToken(token)
if err != nil {
Error(w, err, http.StatusUnauthorized, nil)
return
}
+ } else {
+ tokenData = &portainer.TokenData{
+ Role: portainer.AdministratorRole,
+ }
}
- next.ServeHTTP(w, r)
+ ctx := context.WithValue(r.Context(), contextAuthenticationKey, tokenData)
+ next.ServeHTTP(w, r.WithContext(ctx))
return
})
}
diff --git a/api/http/proxy_transport.go b/api/http/proxy_transport.go
new file mode 100644
index 000000000..34130979a
--- /dev/null
+++ b/api/http/proxy_transport.go
@@ -0,0 +1,664 @@
+package http
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "path"
+ "strconv"
+ "strings"
+
+ "github.com/portainer/portainer"
+)
+
+type (
+ proxyTransport struct {
+ transport *http.Transport
+ ResourceControlService portainer.ResourceControlService
+ }
+ resourceControlMetadata struct {
+ OwnerID portainer.UserID `json:"OwnerId"`
+ }
+)
+
+func (p *proxyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ response, err := p.transport.RoundTrip(req)
+ if err != nil {
+ return response, err
+ }
+
+ err = p.proxyDockerRequests(req, response)
+ return response, err
+}
+
+func (p *proxyTransport) proxyDockerRequests(request *http.Request, response *http.Response) error {
+ path := request.URL.Path
+
+ if strings.HasPrefix(path, "/containers") {
+ return p.handleContainerRequests(request, response)
+ } else if strings.HasPrefix(path, "/services") {
+ return p.handleServiceRequests(request, response)
+ } else if strings.HasPrefix(path, "/volumes") {
+ return p.handleVolumeRequests(request, response)
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) handleContainerRequests(request *http.Request, response *http.Response) error {
+ requestPath := request.URL.Path
+
+ tokenData, err := extractTokenDataFromRequestContext(request)
+ if err != nil {
+ return err
+ }
+
+ if requestPath == "/containers/prune" && tokenData.Role != portainer.AdministratorRole {
+ return writeAccessDeniedResponse(response)
+ }
+ if requestPath == "/containers/json" {
+ if tokenData.Role == portainer.AdministratorRole {
+ return p.decorateContainerResponse(response)
+ }
+ return p.proxyContainerResponseWithResourceControl(response, tokenData.ID)
+ }
+ // /containers/{id}/action
+ if match, _ := path.Match("/containers/*/*", requestPath); match {
+ if tokenData.Role != portainer.AdministratorRole {
+ resourceID := path.Base(path.Dir(requestPath))
+ return p.proxyContainerResponseWithAccessControl(response, tokenData.ID, resourceID)
+ }
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) handleServiceRequests(request *http.Request, response *http.Response) error {
+ requestPath := request.URL.Path
+
+ tokenData, err := extractTokenDataFromRequestContext(request)
+ if err != nil {
+ return err
+ }
+
+ if requestPath == "/services" {
+ if tokenData.Role == portainer.AdministratorRole {
+ return p.decorateServiceResponse(response)
+ }
+ return p.proxyServiceResponseWithResourceControl(response, tokenData.ID)
+ }
+ // /services/{id}
+ if match, _ := path.Match("/services/*", requestPath); match {
+ if tokenData.Role != portainer.AdministratorRole {
+ resourceID := path.Base(requestPath)
+ return p.proxyServiceResponseWithAccessControl(response, tokenData.ID, resourceID)
+ }
+ }
+ // /services/{id}/action
+ if match, _ := path.Match("/services/*/*", requestPath); match {
+ if tokenData.Role != portainer.AdministratorRole {
+ resourceID := path.Base(path.Dir(requestPath))
+ return p.proxyServiceResponseWithAccessControl(response, tokenData.ID, resourceID)
+ }
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) handleVolumeRequests(request *http.Request, response *http.Response) error {
+ requestPath := request.URL.Path
+
+ tokenData, err := extractTokenDataFromRequestContext(request)
+ if err != nil {
+ return err
+ }
+
+ if requestPath == "/volumes" {
+ if tokenData.Role == portainer.AdministratorRole {
+ return p.decorateVolumeResponse(response)
+ }
+ return p.proxyVolumeResponseWithResourceControl(response, tokenData.ID)
+ }
+ if requestPath == "/volumes/prune" && tokenData.Role != portainer.AdministratorRole {
+ return writeAccessDeniedResponse(response)
+ }
+ // /volumes/{name}
+ if match, _ := path.Match("/volumes/*", requestPath); match {
+ if tokenData.Role != portainer.AdministratorRole {
+ resourceID := path.Base(requestPath)
+ return p.proxyVolumeResponseWithAccessControl(response, tokenData.ID, resourceID)
+ }
+ }
+ return nil
+}
+
+func (p *proxyTransport) proxyContainerResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
+ rcs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
+ if err != nil {
+ return err
+ }
+
+ userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
+ if err != nil {
+ return err
+ }
+
+ if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
+ return writeAccessDeniedResponse(response)
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) proxyServiceResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
+ rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
+ if err != nil {
+ return err
+ }
+
+ userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
+ if err != nil {
+ return err
+ }
+
+ if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
+ return writeAccessDeniedResponse(response)
+ }
+ return nil
+}
+
+func (p *proxyTransport) proxyVolumeResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
+ rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
+ if err != nil {
+ return err
+ }
+
+ userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
+ if err != nil {
+ return err
+ }
+
+ if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
+ return writeAccessDeniedResponse(response)
+ }
+ return nil
+}
+
+func (p *proxyTransport) decorateContainerResponse(response *http.Response) error {
+ responseData, err := getResponseData(response)
+ if err != nil {
+ return err
+ }
+
+ containers, err := p.decorateContainers(responseData)
+ if err != nil {
+ return err
+ }
+
+ err = rewriteContainerResponse(response, containers)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) proxyContainerResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
+ responseData, err := getResponseData(response)
+ if err != nil {
+ return err
+ }
+
+ containers, err := p.filterContainers(userID, responseData)
+ if err != nil {
+ return err
+ }
+
+ err = rewriteContainerResponse(response, containers)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) decorateServiceResponse(response *http.Response) error {
+ responseData, err := getResponseData(response)
+ if err != nil {
+ return err
+ }
+
+ services, err := p.decorateServices(responseData)
+ if err != nil {
+ return err
+ }
+
+ err = rewriteServiceResponse(response, services)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) proxyServiceResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
+ responseData, err := getResponseData(response)
+ if err != nil {
+ return err
+ }
+
+ volumes, err := p.filterServices(userID, responseData)
+ if err != nil {
+ return err
+ }
+
+ err = rewriteServiceResponse(response, volumes)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) decorateVolumeResponse(response *http.Response) error {
+ responseData, err := getResponseData(response)
+ if err != nil {
+ return err
+ }
+
+ volumes, err := p.decorateVolumes(responseData)
+ if err != nil {
+ return err
+ }
+
+ err = rewriteVolumeResponse(response, volumes)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) proxyVolumeResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
+ responseData, err := getResponseData(response)
+ if err != nil {
+ return err
+ }
+
+ volumes, err := p.filterVolumes(userID, responseData)
+ if err != nil {
+ return err
+ }
+
+ err = rewriteVolumeResponse(response, volumes)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (p *proxyTransport) decorateContainers(responseData interface{}) ([]interface{}, error) {
+ responseDataArray := responseData.([]interface{})
+
+ containerRCs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
+ if err != nil {
+ return nil, err
+ }
+
+ serviceRCs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
+ if err != nil {
+ return nil, err
+ }
+
+ decoratedResources := make([]interface{}, 0)
+
+ for _, container := range responseDataArray {
+ jsonObject := container.(map[string]interface{})
+ containerID := jsonObject["Id"].(string)
+ containerRC := getRCByResourceID(containerID, containerRCs)
+ if containerRC != nil {
+ decoratedObject := decorateWithResourceControlMetadata(jsonObject, containerRC.OwnerID)
+ decoratedResources = append(decoratedResources, decoratedObject)
+ continue
+ }
+
+ containerLabels := jsonObject["Labels"]
+ if containerLabels != nil {
+ jsonLabels := containerLabels.(map[string]interface{})
+ serviceID := jsonLabels["com.docker.swarm.service.id"]
+ if serviceID != nil {
+ serviceRC := getRCByResourceID(serviceID.(string), serviceRCs)
+ if serviceRC != nil {
+ decoratedObject := decorateWithResourceControlMetadata(jsonObject, serviceRC.OwnerID)
+ decoratedResources = append(decoratedResources, decoratedObject)
+ continue
+ }
+ }
+ }
+ decoratedResources = append(decoratedResources, container)
+ }
+
+ return decoratedResources, nil
+}
+
+func (p *proxyTransport) filterContainers(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
+ responseDataArray := responseData.([]interface{})
+
+ containerRCs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
+ if err != nil {
+ return nil, err
+ }
+
+ serviceRCs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
+ if err != nil {
+ return nil, err
+ }
+
+ userOwnedContainerIDs, err := getResourceIDsOwnedByUser(userID, containerRCs)
+ if err != nil {
+ return nil, err
+ }
+
+ userOwnedServiceIDs, err := getResourceIDsOwnedByUser(userID, serviceRCs)
+ if err != nil {
+ return nil, err
+ }
+
+ publicContainers := getPublicContainers(responseDataArray, containerRCs, serviceRCs)
+
+ filteredResources := make([]interface{}, 0)
+
+ for _, container := range responseDataArray {
+ jsonObject := container.(map[string]interface{})
+ containerID := jsonObject["Id"].(string)
+ if isStringInArray(containerID, userOwnedContainerIDs) {
+ decoratedObject := decorateWithResourceControlMetadata(jsonObject, userID)
+ filteredResources = append(filteredResources, decoratedObject)
+ continue
+ }
+
+ containerLabels := jsonObject["Labels"]
+ if containerLabels != nil {
+ jsonLabels := containerLabels.(map[string]interface{})
+ serviceID := jsonLabels["com.docker.swarm.service.id"]
+ if serviceID != nil && isStringInArray(serviceID.(string), userOwnedServiceIDs) {
+ decoratedObject := decorateWithResourceControlMetadata(jsonObject, userID)
+ filteredResources = append(filteredResources, decoratedObject)
+ }
+ }
+ }
+
+ filteredResources = append(filteredResources, publicContainers...)
+ return filteredResources, nil
+}
+
+func decorateWithResourceControlMetadata(object map[string]interface{}, userID portainer.UserID) map[string]interface{} {
+ metadata := make(map[string]interface{})
+ metadata["ResourceControl"] = resourceControlMetadata{
+ OwnerID: userID,
+ }
+ object["Portainer"] = metadata
+ return object
+}
+
+func (p *proxyTransport) decorateServices(responseData interface{}) ([]interface{}, error) {
+ responseDataArray := responseData.([]interface{})
+
+ rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
+ if err != nil {
+ return nil, err
+ }
+
+ decoratedResources := make([]interface{}, 0)
+
+ for _, service := range responseDataArray {
+ jsonResource := service.(map[string]interface{})
+ resourceID := jsonResource["ID"].(string)
+ serviceRC := getRCByResourceID(resourceID, rcs)
+ if serviceRC != nil {
+ decoratedObject := decorateWithResourceControlMetadata(jsonResource, serviceRC.OwnerID)
+ decoratedResources = append(decoratedResources, decoratedObject)
+ continue
+ }
+ decoratedResources = append(decoratedResources, service)
+ }
+
+ return decoratedResources, nil
+}
+
+func (p *proxyTransport) filterServices(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
+ responseDataArray := responseData.([]interface{})
+
+ rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
+ if err != nil {
+ return nil, err
+ }
+
+ userOwnedServiceIDs, err := getResourceIDsOwnedByUser(userID, rcs)
+ if err != nil {
+ return nil, err
+ }
+
+ publicServices := getPublicResources(responseDataArray, rcs, "ID")
+
+ filteredResources := make([]interface{}, 0)
+
+ for _, res := range responseDataArray {
+ jsonResource := res.(map[string]interface{})
+ resourceID := jsonResource["ID"].(string)
+ if isStringInArray(resourceID, userOwnedServiceIDs) {
+ decoratedObject := decorateWithResourceControlMetadata(jsonResource, userID)
+ filteredResources = append(filteredResources, decoratedObject)
+ }
+ }
+
+ filteredResources = append(filteredResources, publicServices...)
+ return filteredResources, nil
+}
+
+func (p *proxyTransport) decorateVolumes(responseData interface{}) ([]interface{}, error) {
+ var responseDataArray []interface{}
+ jsonObject := responseData.(map[string]interface{})
+ if jsonObject["Volumes"] != nil {
+ responseDataArray = jsonObject["Volumes"].([]interface{})
+ }
+
+ rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
+ if err != nil {
+ return nil, err
+ }
+
+ decoratedResources := make([]interface{}, 0)
+
+ for _, volume := range responseDataArray {
+ jsonResource := volume.(map[string]interface{})
+ resourceID := jsonResource["Name"].(string)
+ volumeRC := getRCByResourceID(resourceID, rcs)
+ if volumeRC != nil {
+ decoratedObject := decorateWithResourceControlMetadata(jsonResource, volumeRC.OwnerID)
+ decoratedResources = append(decoratedResources, decoratedObject)
+ continue
+ }
+ decoratedResources = append(decoratedResources, volume)
+ }
+
+ return decoratedResources, nil
+}
+
+func (p *proxyTransport) filterVolumes(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
+ var responseDataArray []interface{}
+ jsonObject := responseData.(map[string]interface{})
+ if jsonObject["Volumes"] != nil {
+ responseDataArray = jsonObject["Volumes"].([]interface{})
+ }
+
+ rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
+ if err != nil {
+ return nil, err
+ }
+
+ userOwnedVolumeIDs, err := getResourceIDsOwnedByUser(userID, rcs)
+ if err != nil {
+ return nil, err
+ }
+
+ publicVolumes := getPublicResources(responseDataArray, rcs, "Name")
+
+ filteredResources := make([]interface{}, 0)
+
+ for _, res := range responseDataArray {
+ jsonResource := res.(map[string]interface{})
+ resourceID := jsonResource["Name"].(string)
+ if isStringInArray(resourceID, userOwnedVolumeIDs) {
+ decoratedObject := decorateWithResourceControlMetadata(jsonResource, userID)
+ filteredResources = append(filteredResources, decoratedObject)
+ }
+ }
+
+ filteredResources = append(filteredResources, publicVolumes...)
+ return filteredResources, nil
+}
+
+func getResourceIDsOwnedByUser(userID portainer.UserID, rcs []portainer.ResourceControl) ([]string, error) {
+ ownedResources := make([]string, 0)
+ for _, rc := range rcs {
+ if rc.OwnerID == userID {
+ ownedResources = append(ownedResources, rc.ResourceID)
+ }
+ }
+ return ownedResources, nil
+}
+
+func getOwnedServiceContainers(responseData []interface{}, serviceRCs []portainer.ResourceControl) []interface{} {
+ ownedContainers := make([]interface{}, 0)
+ for _, res := range responseData {
+ jsonResource := res.(map[string]map[string]interface{})
+ swarmServiceID := jsonResource["Labels"]["com.docker.swarm.service.id"]
+ if swarmServiceID != nil {
+ resourceID := swarmServiceID.(string)
+ if isResourceIDInRCs(resourceID, serviceRCs) {
+ ownedContainers = append(ownedContainers, res)
+ }
+ }
+ }
+ return ownedContainers
+}
+
+func getPublicContainers(responseData []interface{}, containerRCs []portainer.ResourceControl, serviceRCs []portainer.ResourceControl) []interface{} {
+ publicContainers := make([]interface{}, 0)
+ for _, container := range responseData {
+ jsonObject := container.(map[string]interface{})
+ containerID := jsonObject["Id"].(string)
+ if !isResourceIDInRCs(containerID, containerRCs) {
+ containerLabels := jsonObject["Labels"]
+ if containerLabels != nil {
+ jsonLabels := containerLabels.(map[string]interface{})
+ serviceID := jsonLabels["com.docker.swarm.service.id"]
+ if serviceID == nil {
+ publicContainers = append(publicContainers, container)
+ } else if serviceID != nil && !isResourceIDInRCs(serviceID.(string), serviceRCs) {
+ publicContainers = append(publicContainers, container)
+ }
+ } else {
+ publicContainers = append(publicContainers, container)
+ }
+ }
+ }
+
+ return publicContainers
+}
+
+func getPublicResources(responseData []interface{}, rcs []portainer.ResourceControl, resourceIDKey string) []interface{} {
+ publicResources := make([]interface{}, 0)
+ for _, res := range responseData {
+ jsonResource := res.(map[string]interface{})
+ resourceID := jsonResource[resourceIDKey].(string)
+ if !isResourceIDInRCs(resourceID, rcs) {
+ publicResources = append(publicResources, res)
+ }
+ }
+ return publicResources
+}
+
+func isStringInArray(target string, array []string) bool {
+ for _, element := range array {
+ if element == target {
+ return true
+ }
+ }
+ return false
+}
+
+func isResourceIDInRCs(resourceID string, rcs []portainer.ResourceControl) bool {
+ for _, rc := range rcs {
+ if resourceID == rc.ResourceID {
+ return true
+ }
+ }
+ return false
+}
+
+func getRCByResourceID(resourceID string, rcs []portainer.ResourceControl) *portainer.ResourceControl {
+ for _, rc := range rcs {
+ if resourceID == rc.ResourceID {
+ return &rc
+ }
+ }
+ return nil
+}
+
+func getResponseData(response *http.Response) (interface{}, error) {
+ var data interface{}
+ if response.Body != nil {
+ body, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ err = response.Body.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal(body, &data)
+ if err != nil {
+ return nil, err
+ }
+
+ return data, nil
+ }
+ return nil, ErrEmptyResponseBody
+}
+
+func writeAccessDeniedResponse(response *http.Response) error {
+ return rewriteResponse(response, portainer.ErrResourceAccessDenied, 403)
+}
+
+func rewriteContainerResponse(response *http.Response, responseData interface{}) error {
+ return rewriteResponse(response, responseData, 200)
+}
+
+func rewriteServiceResponse(response *http.Response, responseData interface{}) error {
+ return rewriteResponse(response, responseData, 200)
+}
+
+func rewriteVolumeResponse(response *http.Response, responseData interface{}) error {
+ data := map[string]interface{}{}
+ data["Volumes"] = responseData
+ return rewriteResponse(response, data, 200)
+}
+
+func rewriteResponse(response *http.Response, newContent interface{}, statusCode int) error {
+ jsonData, err := json.Marshal(newContent)
+ if err != nil {
+ return err
+ }
+ body := ioutil.NopCloser(bytes.NewReader(jsonData))
+ response.StatusCode = statusCode
+ response.Body = body
+ response.ContentLength = int64(len(jsonData))
+ response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
+ return nil
+}
diff --git a/api/http/server.go b/api/http/server.go
index dc09db888..046e58ee9 100644
--- a/api/http/server.go
+++ b/api/http/server.go
@@ -8,35 +8,19 @@ import (
// Server implements the portainer.Server interface
type Server struct {
- BindAddress string
- AssetsPath string
- AuthDisabled bool
- EndpointManagement bool
- UserService portainer.UserService
- EndpointService portainer.EndpointService
- CryptoService portainer.CryptoService
- JWTService portainer.JWTService
- FileService portainer.FileService
- Settings *portainer.Settings
- TemplatesURL string
- ActiveEndpoint *portainer.Endpoint
- Handler *Handler
-}
-
-func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
- if endpoint != nil {
- server.ActiveEndpoint = endpoint
- server.Handler.WebSocketHandler.endpoint = endpoint
- err := server.Handler.DockerHandler.setupProxy(endpoint)
- if err != nil {
- return err
- }
- err = server.EndpointService.SetActive(endpoint)
- if err != nil {
- return err
- }
- }
- return nil
+ BindAddress string
+ AssetsPath string
+ AuthDisabled bool
+ EndpointManagement bool
+ UserService portainer.UserService
+ EndpointService portainer.EndpointService
+ ResourceControlService portainer.ResourceControlService
+ CryptoService portainer.CryptoService
+ JWTService portainer.JWTService
+ FileService portainer.FileService
+ Settings *portainer.Settings
+ TemplatesURL string
+ Handler *Handler
}
// Start starts the HTTP server
@@ -46,7 +30,7 @@ func (server *Server) Start() error {
authDisabled: server.AuthDisabled,
}
- var authHandler = NewAuthHandler()
+ var authHandler = NewAuthHandler(middleWareService)
authHandler.UserService = server.UserService
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
@@ -54,18 +38,19 @@ func (server *Server) Start() error {
var userHandler = NewUserHandler(middleWareService)
userHandler.UserService = server.UserService
userHandler.CryptoService = server.CryptoService
+ userHandler.ResourceControlService = server.ResourceControlService
var settingsHandler = NewSettingsHandler(middleWareService)
settingsHandler.settings = server.Settings
var templatesHandler = NewTemplatesHandler(middleWareService)
templatesHandler.templatesURL = server.TemplatesURL
- var dockerHandler = NewDockerHandler(middleWareService)
+ var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
+ dockerHandler.EndpointService = server.EndpointService
var websocketHandler = NewWebSocketHandler()
- // EndpointHandler requires a reference to the server to be able to update the active endpoint.
+ websocketHandler.EndpointService = server.EndpointService
var endpointHandler = NewEndpointHandler(middleWareService)
endpointHandler.authorizeEndpointManagement = server.EndpointManagement
endpointHandler.EndpointService = server.EndpointService
endpointHandler.FileService = server.FileService
- endpointHandler.server = server
var uploadHandler = NewUploadHandler(middleWareService)
uploadHandler.FileService = server.FileService
var fileHandler = newFileHandler(server.AssetsPath)
@@ -81,10 +66,6 @@ func (server *Server) Start() error {
FileHandler: fileHandler,
UploadHandler: uploadHandler,
}
- err := server.updateActiveEndpoint(server.ActiveEndpoint)
- if err != nil {
- return err
- }
return http.ListenAndServe(server.BindAddress, server.Handler)
}
diff --git a/api/http/settings_handler.go b/api/http/settings_handler.go
index fab77aaa7..db426c071 100644
--- a/api/http/settings_handler.go
+++ b/api/http/settings_handler.go
@@ -13,19 +13,19 @@ import (
// SettingsHandler represents an HTTP API handler for managing settings.
type SettingsHandler struct {
*mux.Router
- Logger *log.Logger
- middleWareService *middleWareService
- settings *portainer.Settings
+ Logger *log.Logger
+ settings *portainer.Settings
}
// NewSettingsHandler returns a new instance of SettingsHandler.
-func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
+func NewSettingsHandler(mw *middleWareService) *SettingsHandler {
h := &SettingsHandler{
- Router: mux.NewRouter(),
- Logger: log.New(os.Stderr, "", log.LstdFlags),
- middleWareService: middleWareService,
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
}
- h.HandleFunc("/settings", h.handleGetSettings)
+ h.Handle("/settings",
+ mw.public(http.HandlerFunc(h.handleGetSettings)))
+
return h
}
diff --git a/api/http/templates_handler.go b/api/http/templates_handler.go
index a72a6a4c1..510605021 100644
--- a/api/http/templates_handler.go
+++ b/api/http/templates_handler.go
@@ -12,21 +12,18 @@ import (
// TemplatesHandler represents an HTTP API handler for managing templates.
type TemplatesHandler struct {
*mux.Router
- Logger *log.Logger
- middleWareService *middleWareService
- templatesURL string
+ Logger *log.Logger
+ templatesURL string
}
// NewTemplatesHandler returns a new instance of TemplatesHandler.
-func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler {
+func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
h := &TemplatesHandler{
- Router: mux.NewRouter(),
- Logger: log.New(os.Stderr, "", log.LstdFlags),
- middleWareService: middleWareService,
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
}
- h.Handle("/templates", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handleGetTemplates(w, r)
- })))
+ h.Handle("/templates",
+ mw.authenticated(http.HandlerFunc(h.handleGetTemplates)))
return h
}
diff --git a/api/http/upload_handler.go b/api/http/upload_handler.go
index a0d53c36b..a89bf03a4 100644
--- a/api/http/upload_handler.go
+++ b/api/http/upload_handler.go
@@ -14,21 +14,18 @@ import (
// UploadHandler represents an HTTP API handler for managing file uploads.
type UploadHandler struct {
*mux.Router
- Logger *log.Logger
- FileService portainer.FileService
- middleWareService *middleWareService
+ Logger *log.Logger
+ FileService portainer.FileService
}
// NewUploadHandler returns a new instance of UploadHandler.
-func NewUploadHandler(middleWareService *middleWareService) *UploadHandler {
+func NewUploadHandler(mw *middleWareService) *UploadHandler {
h := &UploadHandler{
- Router: mux.NewRouter(),
- Logger: log.New(os.Stderr, "", log.LstdFlags),
- middleWareService: middleWareService,
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
}
- h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handlePostUploadTLS(w, r)
- })))
+ h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}",
+ mw.authenticated(http.HandlerFunc(h.handlePostUploadTLS)))
return h
}
diff --git a/api/http/user_handler.go b/api/http/user_handler.go
index 38243b0a6..b1ba8f684 100644
--- a/api/http/user_handler.go
+++ b/api/http/user_handler.go
@@ -1,6 +1,8 @@
package http
import (
+ "strconv"
+
"github.com/portainer/portainer"
"encoding/json"
@@ -15,43 +17,44 @@ import (
// UserHandler represents an HTTP API handler for managing users.
type UserHandler struct {
*mux.Router
- Logger *log.Logger
- UserService portainer.UserService
- CryptoService portainer.CryptoService
- middleWareService *middleWareService
+ Logger *log.Logger
+ UserService portainer.UserService
+ ResourceControlService portainer.ResourceControlService
+ CryptoService portainer.CryptoService
}
// NewUserHandler returns a new instance of UserHandler.
-func NewUserHandler(middleWareService *middleWareService) *UserHandler {
+func NewUserHandler(mw *middleWareService) *UserHandler {
h := &UserHandler{
- Router: mux.NewRouter(),
- Logger: log.New(os.Stderr, "", log.LstdFlags),
- middleWareService: middleWareService,
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
}
- h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handlePostUsers(w, r)
- })))
- h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handleGetUser(w, r)
- }))).Methods(http.MethodGet)
- h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handlePutUser(w, r)
- }))).Methods(http.MethodPut)
- h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.handlePostUserPasswd(w, r)
- })))
- h.HandleFunc("/users/admin/check", h.handleGetAdminCheck)
- h.HandleFunc("/users/admin/init", h.handlePostAdminInit)
+ h.Handle("/users",
+ mw.administrator(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
+ h.Handle("/users",
+ mw.administrator(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
+ h.Handle("/users/{id}",
+ mw.administrator(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
+ h.Handle("/users/{id}",
+ mw.authenticated(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
+ h.Handle("/users/{id}",
+ mw.administrator(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
+ h.Handle("/users/{id}/passwd",
+ mw.authenticated(http.HandlerFunc(h.handlePostUserPasswd)))
+ h.Handle("/users/{userId}/resources/{resourceType}",
+ mw.authenticated(http.HandlerFunc(h.handlePostUserResource))).Methods(http.MethodPost)
+ h.Handle("/users/{userId}/resources/{resourceType}/{resourceId}",
+ mw.authenticated(http.HandlerFunc(h.handleDeleteUserResource))).Methods(http.MethodDelete)
+ h.Handle("/users/admin/check",
+ mw.public(http.HandlerFunc(h.handleGetAdminCheck)))
+ h.Handle("/users/admin/init",
+ mw.public(http.HandlerFunc(h.handlePostAdminInit)))
+
return h
}
// handlePostUsers handles POST requests on /users
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- handleNotAllowed(w, []string{http.MethodPost})
- return
- }
-
var req postUsersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
@@ -64,8 +67,26 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
return
}
- user := &portainer.User{
+ var role portainer.UserRole
+ if req.Role == 1 {
+ role = portainer.AdministratorRole
+ } else {
+ role = portainer.StandardUserRole
+ }
+
+ user, err := handler.UserService.UserByUsername(req.Username)
+ if err != nil && err != portainer.ErrUserNotFound {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+ if user != nil {
+ Error(w, portainer.ErrUserAlreadyExists, http.StatusConflict, handler.Logger)
+ return
+ }
+
+ user = &portainer.User{
Username: req.Username,
+ Role: role,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
@@ -73,7 +94,7 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
return
}
- err = handler.UserService.UpdateUser(user)
+ err = handler.UserService.CreateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -83,9 +104,24 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
type postUsersRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
+ Role int `valid:"required"`
}
-// handlePostUserPasswd handles POST requests on /users/:username/passwd
+// handleGetUsers handles GET requests on /users
+func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
+ users, err := handler.UserService.Users()
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+
+ for i := range users {
+ users[i].Password = ""
+ }
+ encodeJSON(w, users, handler.Logger)
+}
+
+// handlePostUserPasswd handles POST requests on /users/:id/passwd
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
@@ -93,15 +129,21 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
}
vars := mux.Vars(r)
- username := vars["username"]
+ id := vars["id"]
+
+ userID, err := strconv.Atoi(id)
+ if err != nil {
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
+ }
var req postUserPasswdRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
- _, err := govalidator.ValidateStruct(req)
+ _, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
@@ -109,7 +151,7 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
var password = req.Password
- u, err := handler.UserService.User(username)
+ u, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
@@ -135,12 +177,18 @@ type postUserPasswdResponse struct {
Valid bool `json:"valid"`
}
-// handleGetUser handles GET requests on /users/:username
+// handleGetUser handles GET requests on /users/:id
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
- username := vars["username"]
+ id := vars["id"]
- user, err := handler.UserService.User(username)
+ userID, err := strconv.Atoi(id)
+ if err != nil {
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ user, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
@@ -153,30 +201,74 @@ func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request
encodeJSON(w, &user, handler.Logger)
}
-// handlePutUser handles PUT requests on /users/:username
+// handlePutUser handles PUT requests on /users/:id
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ userID, err := strconv.Atoi(id)
+ if err != nil {
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ tokenData, err := extractTokenDataFromRequestContext(r)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ }
+
+ if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
+ Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
+ return
+ }
+
var req putUserRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
- _, err := govalidator.ValidateStruct(req)
+ _, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
- user := &portainer.User{
- Username: req.Username,
- }
- user.Password, err = handler.CryptoService.Hash(req.Password)
- if err != nil {
- Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
+ if req.Password == "" && req.Role == 0 {
+ Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
- err = handler.UserService.UpdateUser(user)
+ user, err := handler.UserService.User(portainer.UserID(userID))
+ if err == portainer.ErrUserNotFound {
+ Error(w, err, http.StatusNotFound, handler.Logger)
+ return
+ } else if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+
+ if req.Password != "" {
+ user.Password, err = handler.CryptoService.Hash(req.Password)
+ if err != nil {
+ Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
+ return
+ }
+ }
+
+ if req.Role != 0 {
+ if tokenData.Role != portainer.AdministratorRole {
+ Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
+ return
+ }
+ if req.Role == 1 {
+ user.Role = portainer.AdministratorRole
+ } else {
+ user.Role = portainer.StandardUserRole
+ }
+ }
+
+ err = handler.UserService.UpdateUser(user.ID, user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -184,8 +276,8 @@ func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request
}
type putUserRequest struct {
- Username string `valid:"alphanum,required"`
- Password string `valid:"required"`
+ Password string `valid:"-"`
+ Role int `valid:"-"`
}
// handlePostAdminInit handles GET requests on /users/admin/check
@@ -195,17 +287,15 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R
return
}
- user, err := handler.UserService.User("admin")
- if err == portainer.ErrUserNotFound {
- Error(w, err, http.StatusNotFound, handler.Logger)
- return
- } else if err != nil {
+ users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
+ if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
-
- user.Password = ""
- encodeJSON(w, &user, handler.Logger)
+ if len(users) == 0 {
+ Error(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
+ return
+ }
}
// handlePostAdminInit handles POST requests on /users/admin/init
@@ -227,10 +317,11 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
return
}
- user, err := handler.UserService.User("admin")
+ user, err := handler.UserService.UserByUsername("admin")
if err == portainer.ErrUserNotFound {
user := &portainer.User{
Username: "admin",
+ Role: portainer.AdministratorRole,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
@@ -238,7 +329,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
return
}
- err = handler.UserService.UpdateUser(user)
+ err = handler.UserService.CreateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -256,3 +347,134 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
type postAdminInitRequest struct {
Password string `valid:"required"`
}
+
+// handleDeleteUser handles DELETE requests on /users/:id
+func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ userID, err := strconv.Atoi(id)
+ if err != nil {
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ _, err = handler.UserService.User(portainer.UserID(userID))
+
+ if err == portainer.ErrUserNotFound {
+ Error(w, err, http.StatusNotFound, handler.Logger)
+ return
+ } else if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+
+ err = handler.UserService.DeleteUser(portainer.UserID(userID))
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+}
+
+// handlePostUserResource handles POST requests on /users/:userId/resources/:resourceType
+func (handler *UserHandler) handlePostUserResource(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ userID := vars["userId"]
+ resourceType := vars["resourceType"]
+
+ uid, err := strconv.Atoi(userID)
+ if err != nil {
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ var rcType portainer.ResourceControlType
+ if resourceType == "container" {
+ rcType = portainer.ContainerResourceControl
+ } else if resourceType == "service" {
+ rcType = portainer.ServiceResourceControl
+ } else if resourceType == "volume" {
+ rcType = portainer.VolumeResourceControl
+ } else {
+ Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ tokenData, err := extractTokenDataFromRequestContext(r)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ }
+ if tokenData.ID != portainer.UserID(uid) {
+ Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
+ return
+ }
+
+ var req postUserResourceRequest
+ if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
+ Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ _, err = govalidator.ValidateStruct(req)
+ if err != nil {
+ Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ resource := portainer.ResourceControl{
+ OwnerID: portainer.UserID(uid),
+ ResourceID: req.ResourceID,
+ AccessLevel: portainer.RestrictedResourceAccessLevel,
+ }
+
+ err = handler.ResourceControlService.CreateResourceControl(req.ResourceID, &resource, rcType)
+ if err != nil {
+ Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
+ return
+ }
+}
+
+type postUserResourceRequest struct {
+ ResourceID string `valid:"required"`
+}
+
+// handleDeleteUserResource handles DELETE requests on /users/:userId/resources/:resourceType/:resourceId
+func (handler *UserHandler) handleDeleteUserResource(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ userID := vars["userId"]
+ resourceID := vars["resourceId"]
+ resourceType := vars["resourceType"]
+
+ uid, err := strconv.Atoi(userID)
+ if err != nil {
+ Error(w, err, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ var rcType portainer.ResourceControlType
+ if resourceType == "container" {
+ rcType = portainer.ContainerResourceControl
+ } else if resourceType == "service" {
+ rcType = portainer.ServiceResourceControl
+ } else if resourceType == "volume" {
+ rcType = portainer.VolumeResourceControl
+ } else {
+ Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ tokenData, err := extractTokenDataFromRequestContext(r)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ }
+ if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(uid) {
+ Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
+ return
+ }
+
+ err = handler.ResourceControlService.DeleteResourceControl(resourceID, rcType)
+ if err != nil {
+ Error(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+}
diff --git a/api/http/websocket_handler.go b/api/http/websocket_handler.go
index c217404a0..126fcbed0 100644
--- a/api/http/websocket_handler.go
+++ b/api/http/websocket_handler.go
@@ -1,8 +1,6 @@
package http
import (
- "github.com/portainer/portainer"
-
"bytes"
"crypto/tls"
"encoding/json"
@@ -14,18 +12,19 @@ import (
"net/http/httputil"
"net/url"
"os"
+ "strconv"
"time"
"github.com/gorilla/mux"
+ "github.com/portainer/portainer"
"golang.org/x/net/websocket"
)
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
type WebSocketHandler struct {
*mux.Router
- Logger *log.Logger
- middleWareService *middleWareService
- endpoint *portainer.Endpoint
+ Logger *log.Logger
+ EndpointService portainer.EndpointService
}
// NewWebSocketHandler returns a new instance of WebSocketHandler.
@@ -41,34 +40,47 @@ func NewWebSocketHandler() *WebSocketHandler {
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
qry := ws.Request().URL.Query()
execID := qry.Get("id")
+ edpID := qry.Get("endpointId")
- // Should not be managed here
- endpoint, err := url.Parse(handler.endpoint.URL)
+ parsedID, err := strconv.Atoi(edpID)
if err != nil {
- log.Fatalf("Unable to parse endpoint URL: %s", err)
+ log.Printf("Unable to parse endpoint ID: %s", err)
+ return
+ }
+
+ endpointID := portainer.EndpointID(parsedID)
+ endpoint, err := handler.EndpointService.Endpoint(endpointID)
+ if err != nil {
+ log.Printf("Unable to retrieve endpoint: %s", err)
+ return
+ }
+
+ endpointURL, err := url.Parse(endpoint.URL)
+ if err != nil {
+ log.Printf("Unable to parse endpoint URL: %s", err)
return
}
var host string
- if endpoint.Scheme == "tcp" {
- host = endpoint.Host
- } else if endpoint.Scheme == "unix" {
- host = endpoint.Path
+ if endpointURL.Scheme == "tcp" {
+ host = endpointURL.Host
+ } else if endpointURL.Scheme == "unix" {
+ host = endpointURL.Path
}
// Should not be managed here
var tlsConfig *tls.Config
- if handler.endpoint.TLS {
- tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath,
- handler.endpoint.TLSCertPath,
- handler.endpoint.TLSKeyPath)
+ if endpoint.TLS {
+ tlsConfig, err = createTLSConfiguration(endpoint.TLSCACertPath,
+ endpoint.TLSCertPath,
+ endpoint.TLSKeyPath)
if err != nil {
log.Fatalf("Unable to create TLS configuration: %s", err)
return
}
}
- if err := hijack(host, endpoint.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
+ if err := hijack(host, endpointURL.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
log.Fatalf("error during hijack: %s", err)
return
}
diff --git a/api/jwt/jwt.go b/api/jwt/jwt.go
index 0971ee5f7..9d14be4bc 100644
--- a/api/jwt/jwt.go
+++ b/api/jwt/jwt.go
@@ -4,9 +4,10 @@ import (
"github.com/portainer/portainer"
"fmt"
+ "time"
+
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/securecookie"
- "time"
)
// Service represents a service for managing JWT tokens.
@@ -15,7 +16,9 @@ type Service struct {
}
type claims struct {
+ UserID int `json:"id"`
Username string `json:"username"`
+ Role int `json:"role"`
jwt.StandardClaims
}
@@ -35,7 +38,9 @@ func NewService() (*Service, error) {
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
expireToken := time.Now().Add(time.Hour * 8).Unix()
cl := claims{
+ int(data.ID),
data.Username,
+ int(data.Role),
jwt.StandardClaims{
ExpiresAt: expireToken,
},
@@ -50,17 +55,25 @@ func (service *Service) GenerateToken(data *portainer.TokenData) (string, error)
return signedToken, nil
}
-// VerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
-func (service *Service) VerifyToken(token string) error {
- parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
+// ParseAndVerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
+func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, error) {
+ parsedToken, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
return nil, msg
}
return service.secret, nil
})
- if err != nil || parsedToken == nil || !parsedToken.Valid {
- return portainer.ErrInvalidJWTToken
+ if err == nil && parsedToken != nil {
+ if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
+ tokenData := &portainer.TokenData{
+ ID: portainer.UserID(cl.UserID),
+ Username: cl.Username,
+ Role: portainer.UserRole(cl.Role),
+ }
+ return tokenData, nil
+ }
}
- return nil
+
+ return nil, portainer.ErrInvalidJWTToken
}
diff --git a/api/portainer.go b/api/portainer.go
index ee84c4404..14032e931 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -23,6 +23,7 @@ type (
Logo *string
Templates *string
NoAuth *bool
+ NoAnalytics *bool
TLSVerify *bool
TLSCacert *string
TLSCert *string
@@ -34,18 +35,30 @@ type (
HiddenLabels []Pair `json:"hiddenLabels"`
Logo string `json:"logo"`
Authentication bool `json:"authentication"`
+ Analytics bool `json:"analytics"`
EndpointManagement bool `json:"endpointManagement"`
}
// User represent a user account.
User struct {
- Username string `json:"Username"`
- Password string `json:"Password,omitempty"`
+ ID UserID `json:"Id"`
+ Username string `json:"Username"`
+ Password string `json:"Password,omitempty"`
+ Role UserRole `json:"Role"`
}
+ // UserID represents a user identifier
+ UserID int
+
+ // UserRole represents the role of a user. It can be either an administrator
+ // or a regular user.
+ UserRole int
+
// TokenData represents the data embedded in a JWT token.
TokenData struct {
+ ID UserID
Username string
+ Role UserRole
}
// EndpointID represents an endpoint identifier.
@@ -54,15 +67,31 @@ type (
// Endpoint represents a Docker endpoint with all the info required
// to connect to it.
Endpoint struct {
- ID EndpointID `json:"Id"`
- Name string `json:"Name"`
- URL string `json:"URL"`
- TLS bool `json:"TLS"`
- TLSCACertPath string `json:"TLSCACert,omitempty"`
- TLSCertPath string `json:"TLSCert,omitempty"`
- TLSKeyPath string `json:"TLSKey,omitempty"`
+ ID EndpointID `json:"Id"`
+ Name string `json:"Name"`
+ URL string `json:"URL"`
+ TLS bool `json:"TLS"`
+ TLSCACertPath string `json:"TLSCACert,omitempty"`
+ TLSCertPath string `json:"TLSCert,omitempty"`
+ TLSKeyPath string `json:"TLSKey,omitempty"`
+ AuthorizedUsers []UserID `json:"AuthorizedUsers"`
}
+ // ResourceControl represent a reference to a Docker resource with specific controls
+ ResourceControl struct {
+ OwnerID UserID `json:"OwnerId"`
+ ResourceID string `json:"ResourceId"`
+ AccessLevel ResourceAccessLevel `json:"AccessLevel"`
+ }
+
+ // ResourceControlType represents a type of resource control.
+ // Can be one of: container, service or volume.
+ ResourceControlType int
+
+ // ResourceAccessLevel represents the level of control associated to a resource for a specific owner.
+ // Can be one of: full, restricted, limited.
+ ResourceAccessLevel int
+
// TLSFileType represents a type of TLS file required to connect to a Docker endpoint.
// It can be either a TLS CA file, a TLS certificate file or a TLS key file.
TLSFileType int
@@ -77,32 +106,49 @@ type (
DataStore interface {
Open() error
Close() error
+ MigrateData() error
}
- // Server defines the interface to serve the data.
+ // Server defines the interface to serve the API.
Server interface {
Start() error
}
- // UserService represents a service for managing users.
+ // UserService represents a service for managing user data.
UserService interface {
- User(username string) (*User, error)
- UpdateUser(user *User) error
+ User(ID UserID) (*User, error)
+ UserByUsername(username string) (*User, error)
+ Users() ([]User, error)
+ UsersByRole(role UserRole) ([]User, error)
+ CreateUser(user *User) error
+ UpdateUser(ID UserID, user *User) error
+ DeleteUser(ID UserID) error
}
- // EndpointService represents a service for managing endpoints.
+ // EndpointService represents a service for managing endpoint data.
EndpointService interface {
Endpoint(ID EndpointID) (*Endpoint, error)
Endpoints() ([]Endpoint, error)
CreateEndpoint(endpoint *Endpoint) error
UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error
DeleteEndpoint(ID EndpointID) error
- GetActive() (*Endpoint, error)
- SetActive(endpoint *Endpoint) error
- DeleteActive() error
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
}
+ // VersionService represents a service for managing version data.
+ VersionService interface {
+ DBVersion() (int, error)
+ StoreDBVersion(version int) error
+ }
+
+ // ResourceControlService represents a service for managing resource control data.
+ ResourceControlService interface {
+ ResourceControl(resourceID string, rcType ResourceControlType) (*ResourceControl, error)
+ ResourceControls(rcType ResourceControlType) ([]ResourceControl, error)
+ CreateResourceControl(resourceID string, rc *ResourceControl, rcType ResourceControlType) error
+ DeleteResourceControl(resourceID string, rcType ResourceControlType) error
+ }
+
// CryptoService represents a service for encrypting/hashing data.
CryptoService interface {
Hash(data string) (string, error)
@@ -112,7 +158,7 @@ type (
// JWTService represents a service for managing JWT tokens.
JWTService interface {
GenerateToken(data *TokenData) (string, error)
- VerifyToken(token string) error
+ ParseAndVerifyToken(token string) (*TokenData, error)
}
// FileService represents a service for managing files.
@@ -129,8 +175,10 @@ type (
)
const (
- // APIVersion is the version number of portainer API.
- APIVersion = "1.11.4"
+ // APIVersion is the version number of Portainer API.
+ APIVersion = "1.12.0"
+ // DBVersion is the version number of Portainer database.
+ DBVersion = 1
)
const (
@@ -141,3 +189,27 @@ const (
// TLSFileKey represents a TLS key file.
TLSFileKey
)
+
+const (
+ _ UserRole = iota
+ // AdministratorRole represents an administrator user role
+ AdministratorRole
+ // StandardUserRole represents a regular user role
+ StandardUserRole
+)
+
+const (
+ _ ResourceControlType = iota
+ // ContainerResourceControl represents a resource control for a container
+ ContainerResourceControl
+ // ServiceResourceControl represents a resource control for a service
+ ServiceResourceControl
+ // VolumeResourceControl represents a resource control for a volume
+ VolumeResourceControl
+)
+
+const (
+ _ ResourceAccessLevel = iota
+ // RestrictedResourceAccessLevel represents a restricted access level on a resource (private ownership)
+ RestrictedResourceAccessLevel
+)
diff --git a/app/app.js b/app/app.js
index e819fd0aa..7805c060b 100644
--- a/app/app.js
+++ b/app/app.js
@@ -1,71 +1,78 @@
-angular.module('portainer.filters', []);
-angular.module('portainer.rest', ['ngResource']);
-angular.module('portainer.services', []);
-angular.module('portainer.helpers', []);
-angular.module('portainer', [
- 'ui.bootstrap',
- 'ui.router',
- 'ui.select',
- 'ngCookies',
- 'ngSanitize',
- 'ngFileUpload',
- 'angularUtils.directives.dirPagination',
- 'LocalStorageModule',
- 'angular-jwt',
- 'portainer.templates',
- 'portainer.filters',
- 'portainer.rest',
- 'portainer.helpers',
- 'portainer.services',
- 'auth',
- 'dashboard',
- 'container',
- 'containerConsole',
- 'containerLogs',
- 'containers',
- 'createContainer',
- 'createNetwork',
- 'createService',
- 'createVolume',
- 'docker',
- 'endpoint',
- 'endpointInit',
- 'endpoints',
- 'events',
- 'image',
- 'images',
- 'main',
- 'network',
- 'networks',
- 'node',
- 'service',
- 'services',
- 'settings',
- 'sidebar',
- 'stats',
- 'swarm',
- 'task',
- 'templates',
- 'volumes'])
- .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider) {
- 'use strict';
-
- localStorageServiceProvider
- .setStorageType('sessionStorage')
- .setPrefix('portainer');
-
- jwtOptionsProvider.config({
- tokenGetter: ['LocalStorage', function(LocalStorage) {
- return LocalStorage.getJWT();
- }],
- unauthenticatedRedirector: ['$state', function($state) {
- $state.go('auth', {error: 'Your session has expired'});
- }]
- });
- $httpProvider.interceptors.push('jwtInterceptor');
-
- $urlRouterProvider.otherwise('/auth');
-
+angular.module('portainer.filters', []);
+angular.module('portainer.rest', ['ngResource']);
+angular.module('portainer.services', []);
+angular.module('portainer.helpers', []);
+angular.module('portainer', [
+ 'ui.bootstrap',
+ 'ui.router',
+ 'ui.select',
+ 'ngCookies',
+ 'ngSanitize',
+ 'ngFileUpload',
+ 'angularUtils.directives.dirPagination',
+ 'LocalStorageModule',
+ 'angular-jwt',
+ 'angular-google-analytics',
+ 'portainer.templates',
+ 'portainer.filters',
+ 'portainer.rest',
+ 'portainer.helpers',
+ 'portainer.services',
+ 'auth',
+ 'dashboard',
+ 'container',
+ 'containerConsole',
+ 'containerLogs',
+ 'containers',
+ 'createContainer',
+ 'createNetwork',
+ 'createService',
+ 'createVolume',
+ 'docker',
+ 'endpoint',
+ 'endpointAccess',
+ 'endpointInit',
+ 'endpoints',
+ 'events',
+ 'image',
+ 'images',
+ 'main',
+ 'network',
+ 'networks',
+ 'node',
+ 'service',
+ 'services',
+ 'settings',
+ 'sidebar',
+ 'stats',
+ 'swarm',
+ 'task',
+ 'templates',
+ 'user',
+ 'users',
+ 'volumes'])
+ .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider) {
+ 'use strict';
+
+ localStorageServiceProvider
+ .setStorageType('sessionStorage')
+ .setPrefix('portainer');
+
+ jwtOptionsProvider.config({
+ tokenGetter: ['LocalStorage', function(LocalStorage) {
+ return LocalStorage.getJWT();
+ }],
+ unauthenticatedRedirector: ['$state', function($state) {
+ $state.go('auth', {error: 'Your session has expired'});
+ }]
+ });
+ $httpProvider.interceptors.push('jwtInterceptor');
+
+ AnalyticsProvider.setAccount('@@CONFIG_GA_ID');
+ AnalyticsProvider.startOffline(true);
+
+ $urlRouterProvider.otherwise('/auth');
+
$stateProvider
.state('root', {
abstract: true,
@@ -79,435 +86,484 @@ angular.module('portainer', [
.state('auth', {
parent: 'root',
url: '/auth',
- params: {
- logout: false,
- error: ''
- },
- views: {
- "content@": {
- templateUrl: 'app/components/auth/auth.html',
- controller: 'AuthenticationController'
- }
- },
- data: {
- requiresLogin: false
- }
- })
- .state('containers', {
- parent: 'root',
- url: '/containers/',
- views: {
- "content@": {
- templateUrl: 'app/components/containers/containers.html',
- controller: 'ContainersController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('container', {
- url: "^/containers/:id",
- views: {
- "content@": {
- templateUrl: 'app/components/container/container.html',
- controller: 'ContainerController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('stats', {
- url: "^/containers/:id/stats",
- views: {
- "content@": {
- templateUrl: 'app/components/stats/stats.html',
- controller: 'StatsController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('logs', {
- url: "^/containers/:id/logs",
- views: {
- "content@": {
- templateUrl: 'app/components/containerLogs/containerlogs.html',
- controller: 'ContainerLogsController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('console', {
- url: "^/containers/:id/console",
- views: {
- "content@": {
- templateUrl: 'app/components/containerConsole/containerConsole.html',
- controller: 'ContainerConsoleController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('dashboard', {
- parent: 'root',
- url: '/dashboard',
- views: {
- "content@": {
- templateUrl: 'app/components/dashboard/dashboard.html',
- controller: 'DashboardController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('actions', {
- abstract: true,
- url: "/actions",
- views: {
- "content@": {
- template: '
'
- },
- "sidebar@": {
- template: '
'
- }
- }
- })
- .state('actions.create', {
- abstract: true,
- url: "/create",
- views: {
- "content@": {
- template: '
'
- },
- "sidebar@": {
- template: '
'
- }
- }
- })
- .state('actions.create.container', {
- url: "/container",
- views: {
- "content@": {
- templateUrl: 'app/components/createContainer/createcontainer.html',
- controller: 'CreateContainerController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('actions.create.network', {
- url: "/network",
- views: {
- "content@": {
- templateUrl: 'app/components/createNetwork/createnetwork.html',
- controller: 'CreateNetworkController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('actions.create.service', {
- url: "/service",
- views: {
- "content@": {
- templateUrl: 'app/components/createService/createservice.html',
- controller: 'CreateServiceController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('actions.create.volume', {
- url: "/volume",
- views: {
- "content@": {
- templateUrl: 'app/components/createVolume/createvolume.html',
- controller: 'CreateVolumeController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('docker', {
- url: '/docker/',
- views: {
- "content@": {
- templateUrl: 'app/components/docker/docker.html',
- controller: 'DockerController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('endpoints', {
- url: '/endpoints/',
- views: {
- "content@": {
- templateUrl: 'app/components/endpoints/endpoints.html',
- controller: 'EndpointsController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('endpoint', {
- url: '^/endpoints/:id',
- views: {
- "content@": {
- templateUrl: 'app/components/endpoint/endpoint.html',
- controller: 'EndpointController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('endpointInit', {
- url: '/init/endpoint',
- views: {
- "content@": {
- templateUrl: 'app/components/endpointInit/endpointInit.html',
- controller: 'EndpointInitController'
- }
- }
- })
- .state('events', {
- url: '/events/',
- views: {
- "content@": {
- templateUrl: 'app/components/events/events.html',
- controller: 'EventsController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('images', {
- url: '/images/',
- views: {
- "content@": {
- templateUrl: 'app/components/images/images.html',
- controller: 'ImagesController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('image', {
- url: '^/images/:id/',
- views: {
- "content@": {
- templateUrl: 'app/components/image/image.html',
- controller: 'ImageController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('networks', {
- url: '/networks/',
- views: {
- "content@": {
- templateUrl: 'app/components/networks/networks.html',
- controller: 'NetworksController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('network', {
- url: '^/networks/:id/',
- views: {
- "content@": {
- templateUrl: 'app/components/network/network.html',
- controller: 'NetworkController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('node', {
- url: '^/nodes/:id/',
- views: {
- "content@": {
- templateUrl: 'app/components/node/node.html',
- controller: 'NodeController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('services', {
- url: '/services/',
- views: {
- "content@": {
- templateUrl: 'app/components/services/services.html',
- controller: 'ServicesController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('service', {
- url: '^/service/:id/',
- views: {
- "content@": {
- templateUrl: 'app/components/service/service.html',
- controller: 'ServiceController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('settings', {
- url: '/settings/',
- views: {
- "content@": {
- templateUrl: 'app/components/settings/settings.html',
- controller: 'SettingsController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('task', {
- url: '^/task/:id',
- views: {
- "content@": {
- templateUrl: 'app/components/task/task.html',
- controller: 'TaskController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('templates', {
- url: '/templates/',
- views: {
- "content@": {
- templateUrl: 'app/components/templates/templates.html',
- controller: 'TemplatesController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('volumes', {
- url: '/volumes/',
- views: {
- "content@": {
- templateUrl: 'app/components/volumes/volumes.html',
- controller: 'VolumesController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- })
- .state('swarm', {
- url: '/swarm/',
- views: {
- "content@": {
- templateUrl: 'app/components/swarm/swarm.html',
- controller: 'SwarmController'
- },
- "sidebar@": {
- templateUrl: 'app/components/sidebar/sidebar.html',
- controller: 'SidebarController'
- }
- }
- });
-
- // The Docker API likes to return plaintext errors, this catches them and disp
- $httpProvider.interceptors.push(function() {
- return {
- 'response': function(response) {
- if (typeof(response.data) === 'string' &&
- (response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
- $.gritter.add({
- title: 'Error',
- text: $('').text(response.data).html(),
- time: 10000
- });
- }
- return response;
- }
- };
- });
- }])
- .run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'Messages', function ($rootScope, $state, Authentication, authManager, StateManager, Messages) {
- StateManager.initialize().then(function success(state) {
- if (state.application.authentication) {
- authManager.checkAuthOnRefresh();
- authManager.redirectWhenUnauthenticated();
- Authentication.init();
- $rootScope.$on('tokenHasExpired', function($state) {
- $state.go('auth', {error: 'Your session has expired'});
- });
- }
- }, function error(err) {
- Messages.error("Failure", err, 'Unable to retrieve application settings');
- });
-
- $rootScope.$state = $state;
- }])
- // This is your docker url that the api will use to make requests
- // You need to set this to the api endpoint without the port i.e. http://192.168.1.9
- .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
- .constant('DOCKER_ENDPOINT', 'api/docker')
- .constant('CONFIG_ENDPOINT', 'api/settings')
+ params: {
+ logout: false,
+ error: ''
+ },
+ views: {
+ "content@": {
+ templateUrl: 'app/components/auth/auth.html',
+ controller: 'AuthenticationController'
+ }
+ },
+ data: {
+ requiresLogin: false
+ }
+ })
+ .state('containers', {
+ parent: 'root',
+ url: '/containers/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/containers/containers.html',
+ controller: 'ContainersController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('container', {
+ url: "^/containers/:id",
+ views: {
+ "content@": {
+ templateUrl: 'app/components/container/container.html',
+ controller: 'ContainerController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('stats', {
+ url: "^/containers/:id/stats",
+ views: {
+ "content@": {
+ templateUrl: 'app/components/stats/stats.html',
+ controller: 'StatsController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('logs', {
+ url: "^/containers/:id/logs",
+ views: {
+ "content@": {
+ templateUrl: 'app/components/containerLogs/containerlogs.html',
+ controller: 'ContainerLogsController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('console', {
+ url: "^/containers/:id/console",
+ views: {
+ "content@": {
+ templateUrl: 'app/components/containerConsole/containerConsole.html',
+ controller: 'ContainerConsoleController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('dashboard', {
+ parent: 'root',
+ url: '/dashboard',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/dashboard/dashboard.html',
+ controller: 'DashboardController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('actions', {
+ abstract: true,
+ url: "/actions",
+ views: {
+ "content@": {
+ template: '
'
+ },
+ "sidebar@": {
+ template: '
'
+ }
+ }
+ })
+ .state('actions.create', {
+ abstract: true,
+ url: "/create",
+ views: {
+ "content@": {
+ template: '
'
+ },
+ "sidebar@": {
+ template: '
'
+ }
+ }
+ })
+ .state('actions.create.container', {
+ url: "/container",
+ views: {
+ "content@": {
+ templateUrl: 'app/components/createContainer/createcontainer.html',
+ controller: 'CreateContainerController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('actions.create.network', {
+ url: "/network",
+ views: {
+ "content@": {
+ templateUrl: 'app/components/createNetwork/createnetwork.html',
+ controller: 'CreateNetworkController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('actions.create.service', {
+ url: "/service",
+ views: {
+ "content@": {
+ templateUrl: 'app/components/createService/createservice.html',
+ controller: 'CreateServiceController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('actions.create.volume', {
+ url: "/volume",
+ views: {
+ "content@": {
+ templateUrl: 'app/components/createVolume/createvolume.html',
+ controller: 'CreateVolumeController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('docker', {
+ url: '/docker/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/docker/docker.html',
+ controller: 'DockerController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('endpoints', {
+ url: '/endpoints/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/endpoints/endpoints.html',
+ controller: 'EndpointsController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('endpoint', {
+ url: '^/endpoints/:id',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/endpoint/endpoint.html',
+ controller: 'EndpointController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('endpoint.access', {
+ url: '^/endpoints/:id/access',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/endpointAccess/endpointAccess.html',
+ controller: 'EndpointAccessController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('endpointInit', {
+ url: '/init/endpoint',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/endpointInit/endpointInit.html',
+ controller: 'EndpointInitController'
+ }
+ }
+ })
+ .state('events', {
+ url: '/events/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/events/events.html',
+ controller: 'EventsController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('images', {
+ url: '/images/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/images/images.html',
+ controller: 'ImagesController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('image', {
+ url: '^/images/:id/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/image/image.html',
+ controller: 'ImageController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('networks', {
+ url: '/networks/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/networks/networks.html',
+ controller: 'NetworksController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('network', {
+ url: '^/networks/:id/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/network/network.html',
+ controller: 'NetworkController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('node', {
+ url: '^/nodes/:id/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/node/node.html',
+ controller: 'NodeController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('services', {
+ url: '/services/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/services/services.html',
+ controller: 'ServicesController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('service', {
+ url: '^/service/:id/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/service/service.html',
+ controller: 'ServiceController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('settings', {
+ url: '/settings/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/settings/settings.html',
+ controller: 'SettingsController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('task', {
+ url: '^/task/:id',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/task/task.html',
+ controller: 'TaskController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('templates', {
+ url: '/templates/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/templates/templates.html',
+ controller: 'TemplatesController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('volumes', {
+ url: '/volumes/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/volumes/volumes.html',
+ controller: 'VolumesController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('users', {
+ url: '/users/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/users/users.html',
+ controller: 'UsersController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('user', {
+ url: '^/users/:id',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/user/user.html',
+ controller: 'UserController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ .state('swarm', {
+ url: '/swarm/',
+ views: {
+ "content@": {
+ templateUrl: 'app/components/swarm/swarm.html',
+ controller: 'SwarmController'
+ },
+ "sidebar@": {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ });
+
+ // The Docker API likes to return plaintext errors, this catches them and disp
+ $httpProvider.interceptors.push(function() {
+ return {
+ 'response': function(response) {
+ if (typeof(response.data) === 'string' &&
+ (_.startsWith(response.data, 'Conflict.') || _.startsWith(response.data, 'conflict:'))) {
+ $.gritter.add({
+ title: 'Error',
+ text: $('
').text(response.data).html(),
+ time: 10000
+ });
+ }
+ return response;
+ }
+ };
+ });
+ }])
+ .run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Messages', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Messages, Analytics) {
+ EndpointProvider.initialize();
+ StateManager.initialize().then(function success(state) {
+ if (state.application.authentication) {
+ authManager.checkAuthOnRefresh();
+ authManager.redirectWhenUnauthenticated();
+ Authentication.init();
+ $rootScope.$on('tokenHasExpired', function($state) {
+ $state.go('auth', {error: 'Your session has expired'});
+ });
+ }
+ if (state.application.analytics) {
+ Analytics.offline(false);
+ Analytics.registerScriptTags();
+ Analytics.registerTrackers();
+ $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
+ Analytics.trackPage(toState.url);
+ Analytics.pageView();
+ });
+ }
+ }, function error(err) {
+ Messages.error("Failure", err, 'Unable to retrieve application settings');
+ });
+
+ $rootScope.$state = $state;
+ }])
+ // This is your docker url that the api will use to make requests
+ // You need to set this to the api endpoint without the port i.e. http://192.168.1.9
+ .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
+ .constant('DOCKER_ENDPOINT', 'api/docker')
+ .constant('CONFIG_ENDPOINT', 'api/settings')
.constant('AUTH_ENDPOINT', 'api/auth')
.constant('USERS_ENDPOINT', 'api/users')
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
.constant('TEMPLATES_ENDPOINT', 'api/templates')
.constant('PAGINATION_MAX_ITEMS', 10)
- .constant('UI_VERSION', 'v1.11.4');
+ .constant('UI_VERSION', 'v1.12.0');
diff --git a/app/components/auth/authController.js b/app/components/auth/authController.js
index b46e8537a..7d5026039 100644
--- a/app/components/auth/authController.js
+++ b/app/components/auth/authController.js
@@ -1,6 +1,6 @@
angular.module('auth', [])
-.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'Messages',
-function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, Messages) {
+.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Messages',
+function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, EndpointProvider, Messages) {
$scope.authData = {
username: 'admin',
@@ -14,18 +14,34 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
};
if (!$scope.applicationState.application.authentication) {
- EndpointService.getActive().then(function success(data) {
- StateManager.updateEndpointState(true)
- .then(function success() {
- $state.go('dashboard');
- }, function error(err) {
- Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
- });
- }, function error(err) {
- if (err.status === 404) {
+ EndpointService.endpoints()
+ .then(function success(data) {
+ if (data.length > 0) {
+ endpointID = EndpointProvider.endpointID();
+ if (!endpointID) {
+ endpointID = data[0].Id;
+ EndpointProvider.setEndpointID(endpointID);
+ }
+ StateManager.updateEndpointState(true)
+ .then(function success() {
+ $state.go('dashboard');
+ }, function error(err) {
+ Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
+ });
+ }
+ else {
$state.go('endpointInit');
+ }
+ }, function error(err) {
+ Messages.error("Failure", err, 'Unable to retrieve endpoints');
+ });
+ } else {
+ Users.checkAdminUser({}, function () {},
+ function (e) {
+ if (e.status === 404) {
+ $scope.initPassword = true;
} else {
- Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
+ Messages.error("Failure", e, 'Unable to verify administrator account existence');
}
});
}
@@ -47,15 +63,6 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
$scope.logo = c.logo;
});
- Users.checkAdminUser({}, function (d) {},
- function (e) {
- if (e.status === 404) {
- $scope.initPassword = true;
- } else {
- Messages.error("Failure", e, 'Unable to verify administrator account existence');
- }
- });
-
$scope.createAdminUser = function() {
var password = $sanitize($scope.initPasswordData.password);
Users.initAdminUser({password: password}, function (d) {
@@ -75,23 +82,33 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
$scope.authenticationError = false;
var username = $sanitize($scope.authData.username);
var password = $sanitize($scope.authData.password);
- Authentication.login(username, password).then(function success() {
- EndpointService.getActive().then(function success(data) {
+ Authentication.login(username, password)
+ .then(function success(data) {
+ return EndpointService.endpoints();
+ })
+ .then(function success(data) {
+ var userDetails = Authentication.getUserDetails();
+ if (data.length > 0) {
+ endpointID = EndpointProvider.endpointID();
+ if (!endpointID) {
+ endpointID = data[0].Id;
+ EndpointProvider.setEndpointID(endpointID);
+ }
StateManager.updateEndpointState(true)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
});
- }, function error(err) {
- if (err.status === 404) {
- $state.go('endpointInit');
- } else {
- Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
- }
- });
- }, function error() {
- $scope.authData.error = 'Invalid credentials';
+ }
+ else if (data.length === 0 && userDetails.role === 1) {
+ $state.go('endpointInit');
+ } else if (data.length === 0 && userDetails.role === 2) {
+ $scope.authData.error = 'User not allowed. Please contact your administrator.';
+ }
+ })
+ .catch(function error(err) {
+ $scope.authData.error = 'Authentication error';
});
};
}]);
diff --git a/app/components/containerConsole/containerConsoleController.js b/app/components/containerConsole/containerConsoleController.js
index d81bff349..4ef8cc3d6 100644
--- a/app/components/containerConsole/containerConsoleController.js
+++ b/app/components/containerConsole/containerConsoleController.js
@@ -1,6 +1,6 @@
angular.module('containerConsole', [])
-.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'Messages',
-function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Messages) {
+.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Messages',
+function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, EndpointProvider, Messages) {
$scope.state = {};
$scope.state.loaded = false;
$scope.state.connected = false;
@@ -55,7 +55,7 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
} else {
var execId = d.Id;
resizeTTY(execId, termHeight, termWidth);
- var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId;
+ var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
if (url.indexOf('https') > -1) {
url = url.replace('https://', 'wss://');
} else {
diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html
index 1da516551..39a754168 100644
--- a/app/components/containers/containers.html
+++ b/app/components/containers/containers.html
@@ -90,6 +90,13 @@
+
+
+ Ownership
+
+
+
+
@@ -98,7 +105,7 @@
{{ container.Status }}
{{ container|swarmcontainername}}
{{ container|containername}}
- {{ container.Image }}
+ {{ container.Image | hideshasum }}
{{ container.IP ? container.IP : '-' }}
{{ container.hostIP }}
@@ -107,12 +114,43 @@
-
+
+
+
+
+ Public service
+
+
+ Public
+
+
+
+
+
+ Private service
+
+
+ Private
+ Switch to public
+
+
+
+
+
+ Private service (owner: {{ container.Owner }})
+
+
+ Private (owner: {{ container.Owner }})
+ Switch to public
+
+
+
- Loading...
+ Loading...
- No containers available.
+ No containers available.
diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js
index 59f8caa6b..eea0140e0 100644
--- a/app/components/containers/containersController.js
+++ b/app/components/containers/containersController.js
@@ -1,6 +1,6 @@
angular.module('containers', [])
-.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination',
-function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination) {
+ .controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination', 'EntityListService', 'ModalService', 'Authentication', 'ResourceControlService', 'UserService',
+ function ($q, $scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination, EntityListService, ModalService, Authentication, ResourceControlService, UserService) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
$scope.state.displayAll = Settings.displayAll;
@@ -17,8 +17,51 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
Pagination.setPaginationCount('containers', $scope.state.pagination_count);
};
+ function removeContainerResourceControl(container) {
+ volumeResourceControlQueries = [];
+ angular.forEach(container.Mounts, function (volume) {
+ volumeResourceControlQueries.push(ResourceControlService.removeVolumeResourceControl(container.Metadata.ResourceControl.OwnerId, volume.Name));
+ });
+
+ $q.all(volumeResourceControlQueries)
+ .then(function success() {
+ return ResourceControlService.removeContainerResourceControl(container.Metadata.ResourceControl.OwnerId, container.Id);
+ })
+ .then(function success() {
+ delete container.Metadata.ResourceControl;
+ Messages.send('Ownership changed to public', container.Id);
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to change container ownership");
+ });
+ }
+
+ $scope.switchOwnership = function(container) {
+ ModalService.confirmContainerOwnershipChange(function (confirmed) {
+ if(!confirmed) { return; }
+ removeContainerResourceControl(container);
+ });
+ };
+
+ function mapUsersToContainers(users) {
+ angular.forEach($scope.containers, function (container) {
+ if (container.Metadata) {
+ var containerRC = container.Metadata.ResourceControl;
+ if (containerRC && containerRC.OwnerId != $scope.user.ID) {
+ angular.forEach(users, function (user) {
+ if (containerRC.OwnerId === user.Id) {
+ container.Owner = user.Username;
+ }
+ });
+ }
+ }
+ });
+ }
+
var update = function (data) {
$('#loadContainersSpinner').show();
+ var userDetails = Authentication.getUserDetails();
+ $scope.user = userDetails;
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
@@ -29,6 +72,10 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
var model = new ContainerViewModel(container);
model.Status = $filter('containerstatus')(model.Status);
+ EntityListService.rememberPreviousSelection($scope.containers, model, function onSelect(model){
+ $scope.selectItem(model);
+ });
+
if (model.IP) {
$scope.state.displayIP = true;
}
@@ -37,7 +84,20 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
}
return model;
});
- $('#loadContainersSpinner').hide();
+ if (userDetails.role === 1) {
+ UserService.users()
+ .then(function success(data) {
+ mapUsersToContainers(data);
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to retrieve users");
+ })
+ .finally(function final() {
+ $('#loadContainersSpinner').hide();
+ });
+ } else {
+ $('#loadContainersSpinner').hide();
+ }
}, function (e) {
$('#loadContainersSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve containers");
@@ -73,7 +133,17 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
Messages.send("Error", d.message);
}
else {
- Messages.send("Container " + msg, c.Id);
+ if (c.Metadata && c.Metadata.ResourceControl) {
+ ResourceControlService.removeContainerResourceControl(c.Metadata.ResourceControl.OwnerId, c.Id)
+ .then(function success() {
+ Messages.send("Container " + msg, c.Id);
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to remove container ownership");
+ });
+ } else {
+ Messages.send("Container " + msg, c.Id);
+ }
}
complete();
}, function (e) {
diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js
index 606e84d04..7ca69caa0 100644
--- a/app/components/createContainer/createContainerController.js
+++ b/app/components/createContainer/createContainerController.js
@@ -1,14 +1,18 @@
+// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
+// See app/components/templates/templatesController.js as a reference.
angular.module('createContainer', [])
-.controller('CreateContainerController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Messages',
-function ($scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Messages) {
+.controller('CreateContainerController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Messages',
+function ($scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Messages) {
$scope.formValues = {
+ Ownership: $scope.applicationState.application.authentication ? 'private' : '',
alwaysPull: true,
Console: 'none',
Volumes: [],
Registry: '',
NetworkContainer: '',
- Labels: []
+ Labels: [],
+ ExtraHosts: []
};
$scope.imageConfig = {};
@@ -16,15 +20,18 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
$scope.config = {
Image: '',
Env: [],
+ Cmd: '',
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no'
},
PortBindings: [],
+ PublishAllPorts: false,
Binds: [],
NetworkMode: 'bridge',
- Privileged: false
+ Privileged: false,
+ ExtraHosts: []
},
Labels: {}
};
@@ -61,6 +68,15 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
$scope.formValues.Labels.splice(index, 1);
};
+ $scope.addExtraHost = function() {
+ $scope.formValues.ExtraHosts.push({ value: '' });
+ };
+
+ $scope.removeExtraHost = function(index) {
+ $scope.formValues.ExtraHosts.splice(index, 1);
+ };
+
+
Config.$promise.then(function (c) {
var containersToHideLabels = c.hiddenLabels;
@@ -103,26 +119,40 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
});
});
- // TODO: centralize, already present in templatesController
+ function startContainer(containerID) {
+ Container.start({id: containerID}, {}, function (cd) {
+ if (cd.message) {
+ $('#createContainerSpinner').hide();
+ Messages.error('Error', {}, cd.message);
+ } else {
+ $('#createContainerSpinner').hide();
+ Messages.send('Container Started', containerID);
+ $state.go('containers', {}, {reload: true});
+ }
+ }, function (e) {
+ $('#createContainerSpinner').hide();
+ Messages.error("Failure", e, 'Unable to start container');
+ });
+ }
+
function createContainer(config) {
Container.create(config, function (d) {
if (d.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, d.message);
} else {
- Container.start({id: d.Id}, {}, function (cd) {
- if (cd.message) {
+ if ($scope.formValues.Ownership === 'private') {
+ ResourceControlService.setContainerResourceControl(Authentication.getUserDetails().ID, d.Id)
+ .then(function success() {
+ startContainer(d.Id);
+ })
+ .catch(function error(err) {
$('#createContainerSpinner').hide();
- Messages.error('Error', {}, cd.message);
- } else {
- $('#createContainerSpinner').hide();
- Messages.send('Container Started', d.Id);
- $state.go('containers', {}, {reload: true});
- }
- }, function (e) {
- $('#createContainerSpinner').hide();
- Messages.error("Failure", e, 'Unable to start container');
- });
+ Messages.error("Failure", err, 'Unable to apply resource control on container');
+ });
+ } else {
+ startContainer(d.Id);
+ }
}
}, function (e) {
$('#createContainerSpinner').hide();
@@ -130,7 +160,6 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
});
}
- // TODO: centralize, already present in templatesController
function pullImageAndCreateContainer(config) {
Image.create($scope.imageConfig, function (data) {
createContainer(config);
@@ -229,6 +258,12 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
networkMode += ':' + containerName;
}
config.HostConfig.NetworkMode = networkMode;
+
+ $scope.formValues.ExtraHosts.forEach(function (v) {
+ if (v.value) {
+ config.HostConfig.ExtraHosts.push(v.value);
+ }
+ });
}
function prepareLabels(config) {
@@ -243,6 +278,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
function prepareConfiguration() {
var config = angular.copy($scope.config);
+ config.Cmd = ContainerHelper.commandStringToArray(config.Cmd);
prepareNetworkConfig(config);
prepareImageConfig(config);
preparePortBindings(config);
diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html
index 9a27c741a..8302d29a5 100644
--- a/app/components/createContainer/createcontainer.html
+++ b/app/components/createContainer/createcontainer.html
@@ -64,6 +64,11 @@
+
+
+
diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js
index 8497463ab..ed3d3dce5 100644
--- a/app/components/createService/createServiceController.js
+++ b/app/components/createService/createServiceController.js
@@ -1,8 +1,11 @@
+// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
+// See app/components/templates/templatesController.js as a reference.
angular.module('createService', [])
-.controller('CreateServiceController', ['$scope', '$state', 'Service', 'Volume', 'Network', 'ImageHelper', 'Messages',
-function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
+.controller('CreateServiceController', ['$scope', '$state', 'Service', 'Volume', 'Network', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Messages',
+function ($scope, $state, Service, Volume, Network, ImageHelper, Authentication, ResourceControlService, Messages) {
$scope.formValues = {
+ Ownership: $scope.applicationState.application.authentication ? 'private' : '',
Name: '',
Image: '',
Registry: '',
@@ -205,9 +208,22 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
function createNewService(config) {
Service.create(config, function (d) {
- $('#createServiceSpinner').hide();
- Messages.send('Service created', d.ID);
- $state.go('services', {}, {reload: true});
+ if ($scope.formValues.Ownership === 'private') {
+ ResourceControlService.setServiceResourceControl(Authentication.getUserDetails().ID, d.ID)
+ .then(function success() {
+ $('#createServiceSpinner').hide();
+ Messages.send('Service created', d.ID);
+ $state.go('services', {}, {reload: true});
+ })
+ .catch(function error(err) {
+ $('#createContainerSpinner').hide();
+ Messages.error("Failure", err, 'Unable to apply resource control on service');
+ });
+ } else {
+ $('#createServiceSpinner').hide();
+ Messages.send('Service created', d.ID);
+ $state.go('services', {}, {reload: true});
+ }
}, function (e) {
$('#createServiceSpinner').hide();
Messages.error("Failure", e, 'Unable to create service');
diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html
index c38c606e4..87853badb 100644
--- a/app/components/createService/createservice.html
+++ b/app/components/createService/createservice.html
@@ -87,6 +87,26 @@
+
+
+
diff --git a/app/components/createVolume/createVolumeController.js b/app/components/createVolume/createVolumeController.js
index def0b5dcc..61874d1a7 100644
--- a/app/components/createVolume/createVolumeController.js
+++ b/app/components/createVolume/createVolumeController.js
@@ -1,8 +1,9 @@
angular.module('createVolume', [])
-.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages',
-function ($scope, $state, Volume, Messages) {
+.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'ResourceControlService', 'Authentication', 'Messages',
+function ($scope, $state, Volume, ResourceControlService, Authentication, Messages) {
$scope.formValues = {
+ Ownership: $scope.applicationState.application.authentication ? 'private' : '',
DriverOptions: []
};
@@ -25,9 +26,22 @@ function ($scope, $state, Volume, Messages) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', {}, d.message);
} else {
- Messages.send("Volume created", d.Name);
- $('#createVolumeSpinner').hide();
- $state.go('volumes', {}, {reload: true});
+ if ($scope.formValues.Ownership === 'private') {
+ ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, d.Name)
+ .then(function success() {
+ Messages.send("Volume created", d.Name);
+ $('#createVolumeSpinner').hide();
+ $state.go('volumes', {}, {reload: true});
+ })
+ .catch(function error(err) {
+ $('#createVolumeSpinner').hide();
+ Messages.error("Failure", err, 'Unable to apply resource control on volume');
+ });
+ } else {
+ Messages.send("Volume created", d.Name);
+ $('#createVolumeSpinner').hide();
+ $state.go('volumes', {}, {reload: true});
+ }
}
}, function (e) {
$('#createVolumeSpinner').hide();
diff --git a/app/components/createVolume/createvolume.html b/app/components/createVolume/createvolume.html
index 5213f4dd6..e55d30a4b 100644
--- a/app/components/createVolume/createvolume.html
+++ b/app/components/createVolume/createvolume.html
@@ -55,6 +55,26 @@
+
+
+
diff --git a/app/components/endpoint/endpointController.js b/app/components/endpoint/endpointController.js
index e6e4eb526..b221a4f14 100644
--- a/app/components/endpoint/endpointController.js
+++ b/app/components/endpoint/endpointController.js
@@ -18,14 +18,18 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
$scope.updateEndpoint = function() {
var ID = $scope.endpoint.Id;
- var name = $scope.endpoint.Name;
- var URL = $scope.endpoint.URL;
- var TLS = $scope.endpoint.TLS;
- var TLSCACert = $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null;
- var TLSCert = $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null;
- var TLSKey = $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null;
- var type = $scope.endpointType;
- EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey, type).then(function success(data) {
+ var endpointParams = {
+ name: $scope.endpoint.Name,
+ URL: $scope.endpoint.URL,
+ TLS: $scope.endpoint.TLS,
+ TLSCACert: $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null,
+ TLSCert: $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null,
+ TLSKey: $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null,
+ type: $scope.endpointType
+ };
+
+ EndpointService.updateEndpoint(ID, endpointParams)
+ .then(function success(data) {
Messages.send("Endpoint updated", $scope.endpoint.Name);
$state.go('endpoints');
}, function error(err) {
diff --git a/app/components/endpointAccess/endpointAccess.html b/app/components/endpointAccess/endpointAccess.html
new file mode 100644
index 000000000..042280431
--- /dev/null
+++ b/app/components/endpointAccess/endpointAccess.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+ Endpoints > {{ endpoint.Name }} > Access management
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+ {{ endpoint.Name }}
+
+
+
+ URL
+
+ {{ endpoint.URL | stripprotocol }}
+
+
+
+
+
+ You can select which user can access this endpoint by moving them to the authorized users table. Simply click
+ on a user entry to move it from one table to the other.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Items per page:
+
+ All
+ 10
+ 25
+ 50
+ 100
+
+
+
+
+
+ Authorize all users
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+ Role
+
+
+
+
+
+
+
+
+ {{ user.Username }}
+
+ {{ user.RoleName }}
+
+
+
+
+ Loading...
+
+
+ No users.
+
+
+
+
+
+
+
+
+
+
+
+
+ Items per page:
+
+ All
+ 10
+ 25
+ 50
+ 100
+
+
+
+
+
+ Deny all users
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+ Role
+
+
+
+
+
+
+
+
+ {{ user.Username }}
+
+ {{ user.RoleName }}
+
+
+
+
+ Loading...
+
+
+ No authorized users.
+
+
+
+
+
+
+
+
+
diff --git a/app/components/endpointAccess/endpointAccessController.js b/app/components/endpointAccess/endpointAccessController.js
new file mode 100644
index 000000000..44ebb9c2c
--- /dev/null
+++ b/app/components/endpointAccess/endpointAccessController.js
@@ -0,0 +1,148 @@
+angular.module('endpointAccess', [])
+.controller('EndpointAccessController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'UserService', 'Pagination', 'Messages',
+function ($q, $scope, $state, $stateParams, $filter, EndpointService, UserService, Pagination, Messages) {
+
+ $scope.state = {
+ pagination_count_users: Pagination.getPaginationCount('endpoint_access_users'),
+ pagination_count_authorizedUsers: Pagination.getPaginationCount('endpoint_access_authorizedUsers')
+ };
+
+ $scope.sortTypeUsers = 'Username';
+ $scope.sortReverseUsers = true;
+
+ $scope.orderUsers = function(sortType) {
+ $scope.sortReverseUsers = ($scope.sortTypeUsers === sortType) ? !$scope.sortReverseUsers : false;
+ $scope.sortTypeUsers = sortType;
+ };
+
+ $scope.changePaginationCountUsers = function() {
+ Pagination.setPaginationCount('endpoint_access_users', $scope.state.pagination_count_users);
+ };
+
+ $scope.sortTypeAuthorizedUsers = 'Username';
+ $scope.sortReverseAuthorizedUsers = true;
+
+ $scope.orderAuthorizedUsers = function(sortType) {
+ $scope.sortReverseAuthorizedUsers = ($scope.sortTypeAuthorizedUsers === sortType) ? !$scope.sortReverseAuthorizedUsers : false;
+ $scope.sortTypeAuthorizedUsers = sortType;
+ };
+
+ $scope.changePaginationCountAuthorizedUsers = function() {
+ Pagination.setPaginationCount('endpoint_access_authorizedUsers', $scope.state.pagination_count_authorizedUsers);
+ };
+
+ $scope.authorizeAllUsers = function() {
+ var authorizedUserIDs = [];
+ angular.forEach($scope.authorizedUsers, function (user) {
+ authorizedUserIDs.push(user.Id);
+ });
+ angular.forEach($scope.users, function (user) {
+ authorizedUserIDs.push(user.Id);
+ });
+ EndpointService.updateAuthorizedUsers($stateParams.id, authorizedUserIDs)
+ .then(function success(data) {
+ $scope.authorizedUsers = $scope.authorizedUsers.concat($scope.users);
+ $scope.users = [];
+ Messages.send('Access granted for all users');
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to update endpoint permissions");
+ });
+ };
+
+ $scope.unauthorizeAllUsers = function() {
+ EndpointService.updateAuthorizedUsers($stateParams.id, [])
+ .then(function success(data) {
+ $scope.users = $scope.users.concat($scope.authorizedUsers);
+ $scope.authorizedUsers = [];
+ Messages.send('Access removed for all users');
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to update endpoint permissions");
+ });
+ };
+
+ $scope.authorizeUser = function(user) {
+ var authorizedUserIDs = [];
+ angular.forEach($scope.authorizedUsers, function (u) {
+ authorizedUserIDs.push(u.Id);
+ });
+ authorizedUserIDs.push(user.Id);
+ EndpointService.updateAuthorizedUsers($stateParams.id, authorizedUserIDs)
+ .then(function success(data) {
+ removeUserFromArray(user.Id, $scope.users);
+ $scope.authorizedUsers.push(user);
+ Messages.send('Access granted for user', user.Username);
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to update endpoint permissions");
+ });
+ };
+
+ $scope.unauthorizeUser = function(user) {
+ var authorizedUserIDs = $scope.authorizedUsers.filter(function (u) {
+ if (u.Id !== user.Id) {
+ return u;
+ }
+ }).map(function (u) {
+ return u.Id;
+ });
+ EndpointService.updateAuthorizedUsers($stateParams.id, authorizedUserIDs)
+ .then(function success(data) {
+ removeUserFromArray(user.Id, $scope.authorizedUsers);
+ $scope.users.push(user);
+ Messages.send('Access removed for user', user.Username);
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to update endpoint permissions");
+ });
+ };
+
+ function getEndpointAndUsers(endpointID) {
+ $('#loadingViewSpinner').show();
+ $q.all({
+ endpoint: EndpointService.endpoint($stateParams.id),
+ users: UserService.users(),
+ })
+ .then(function success(data) {
+ $scope.endpoint = data.endpoint;
+ $scope.users = data.users.filter(function (user) {
+ if (user.Role !== 1) {
+ return user;
+ }
+ }).map(function (user) {
+ return new UserViewModel(user);
+ });
+ $scope.authorizedUsers = [];
+ angular.forEach($scope.endpoint.AuthorizedUsers, function(userID) {
+ for (var i = 0, l = $scope.users.length; i < l; i++) {
+ if ($scope.users[i].Id === userID) {
+ $scope.authorizedUsers.push($scope.users[i]);
+ $scope.users.splice(i, 1);
+ return;
+ }
+ }
+ });
+ })
+ .catch(function error(err) {
+ $scope.templates = [];
+ $scope.users = [];
+ $scope.authorizedUsers = [];
+ Messages.error("Failure", err, "Unable to retrieve endpoint details");
+ })
+ .finally(function final(){
+ $('#loadingViewSpinner').hide();
+ });
+ }
+
+ function removeUserFromArray(id, users) {
+ for (var i = 0, l = users.length; i < l; i++) {
+ if (users[i].Id === id) {
+ users.splice(i, 1);
+ return;
+ }
+ }
+ }
+
+ getEndpointAndUsers($stateParams.id);
+}]);
diff --git a/app/components/endpointInit/endpointInit.html b/app/components/endpointInit/endpointInit.html
index 5993ec833..19c72a009 100644
--- a/app/components/endpointInit/endpointInit.html
+++ b/app/components/endpointInit/endpointInit.html
@@ -32,8 +32,8 @@
diff --git a/app/components/endpointInit/endpointInitController.js b/app/components/endpointInit/endpointInitController.js
index b6aa99cdd..911f6c994 100644
--- a/app/components/endpointInit/endpointInitController.js
+++ b/app/components/endpointInit/endpointInitController.js
@@ -1,6 +1,6 @@
angular.module('endpointInit', [])
-.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Messages',
-function ($scope, $state, EndpointService, StateManager, Messages) {
+.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Messages',
+function ($scope, $state, EndpointService, StateManager, EndpointProvider, Messages) {
$scope.state = {
error: '',
uploadInProgress: false
@@ -29,20 +29,28 @@ function ($scope, $state, EndpointService, StateManager, Messages) {
var name = "local";
var URL = "unix:///var/run/docker.sock";
var TLS = false;
- EndpointService.createLocalEndpoint(name, URL, TLS, true).then(function success(data) {
- StateManager.updateEndpointState(false)
- .then(function success() {
+
+ EndpointService.createLocalEndpoint(name, URL, TLS, true)
+ .then(
+ function success(data) {
+ var endpointID = data.Id;
+ EndpointProvider.setEndpointID(endpointID);
+ StateManager.updateEndpointState(false).then(
+ function success() {
$state.go('dashboard');
- }, function error(err) {
- EndpointService.deleteEndpoint(0)
+ },
+ function error(err) {
+ EndpointService.deleteEndpoint(endpointID)
.then(function success() {
- $('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to connect to the Docker endpoint';
});
});
- }, function error(err) {
- $('#initEndpointSpinner').hide();
+ },
+ function error() {
$scope.state.error = 'Unable to create endpoint';
+ })
+ .finally(function final() {
+ $('#initEndpointSpinner').hide();
});
};
@@ -57,11 +65,13 @@ function ($scope, $state, EndpointService, StateManager, Messages) {
var TLSKeyFile = $scope.formValues.TLSKey;
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true)
.then(function success(data) {
+ var endpointID = data.Id;
+ EndpointProvider.setEndpointID(endpointID);
StateManager.updateEndpointState(false)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
- EndpointService.deleteEndpoint(0)
+ EndpointService.deleteEndpoint(endpointID)
.then(function success() {
$('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to connect to the Docker endpoint';
diff --git a/app/components/endpoints/endpoints.html b/app/components/endpoints/endpoints.html
index 41501eaa5..cbce6c15a 100644
--- a/app/components/endpoints/endpoints.html
+++ b/app/components/endpoints/endpoints.html
@@ -14,7 +14,7 @@
- Portainer has been started using the --external-endpoints
flag. Endpoint management via the UI is disabled.
+ Portainer has been started using the --external-endpoints
flag. Endpoint management via the UI is disabled. You can still manage endpoint access.
@@ -137,7 +137,9 @@
-
+
+
+
Name
@@ -152,14 +154,7 @@
-
-
- TLS
-
-
-
-
-
+
@@ -167,13 +162,17 @@
{{ endpoint.Name }}
{{ endpoint.URL | stripprotocol }}
-
-
-
- Edit
+
+
+
+ Edit
+
+
+ You cannot edit the active endpoint
+
-
- You cannot edit the active endpoint
+
+ Manage access
diff --git a/app/components/endpoints/endpointsController.js b/app/components/endpoints/endpointsController.js
index 879ecbc1e..ba50144d4 100644
--- a/app/components/endpoints/endpointsController.js
+++ b/app/components/endpoints/endpointsController.js
@@ -1,6 +1,6 @@
angular.module('endpoints', [])
-.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Messages', 'Pagination',
-function ($scope, $state, EndpointService, Messages, Pagination) {
+.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Messages', 'Pagination',
+function ($scope, $state, EndpointService, EndpointProvider, Messages, Pagination) {
$scope.state = {
error: '',
uploadInProgress: false,
@@ -28,6 +28,15 @@ function ($scope, $state, EndpointService, Messages, Pagination) {
Pagination.setPaginationCount('endpoints', $scope.state.pagination_count);
};
+ $scope.selectItems = function (allSelected) {
+ angular.forEach($scope.state.filteredEndpoints, function (endpoint) {
+ if (endpoint.Checked !== allSelected) {
+ endpoint.Checked = allSelected;
+ $scope.selectItem(endpoint);
+ }
+ });
+ };
+
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
@@ -84,19 +93,17 @@ function ($scope, $state, EndpointService, Messages, Pagination) {
function fetchEndpoints() {
$('#loadEndpointsSpinner').show();
- EndpointService.endpoints().then(function success(data) {
+ EndpointService.endpoints()
+ .then(function success(data) {
$scope.endpoints = data;
- EndpointService.getActive().then(function success(data) {
- $scope.activeEndpoint = data;
- $('#loadEndpointsSpinner').hide();
- }, function error(err) {
- $('#loadEndpointsSpinner').hide();
- Messages.error("Failure", err, "Unable to retrieve active endpoint");
- });
- }, function error(err) {
- $('#loadEndpointsSpinner').hide();
+ $scope.activeEndpointID = EndpointProvider.endpointID();
+ })
+ .catch(function error(err) {
Messages.error("Failure", err, "Unable to retrieve endpoints");
$scope.endpoints = [];
+ })
+ .finally(function final() {
+ $('#loadEndpointsSpinner').hide();
});
}
diff --git a/app/components/images/images.html b/app/components/images/images.html
index 972b0602b..5d494bb77 100644
--- a/app/components/images/images.html
+++ b/app/components/images/images.html
@@ -63,7 +63,16 @@
-
Remove
+
+ Remove
+
+
+ Toggle Dropdown
+
+
+
diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js
index 7000cddaa..44752330e 100644
--- a/app/components/images/imagesController.js
+++ b/app/components/images/imagesController.js
@@ -1,6 +1,6 @@
angular.module('images', [])
-.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination',
-function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) {
+.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination', 'ModalService',
+function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, ModalService) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('images');
$scope.sortType = 'RepoTags';
@@ -59,7 +59,15 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) {
});
};
- $scope.removeAction = function () {
+ $scope.confirmRemovalAction = function (force) {
+ ModalService.confirmImageForceRemoval(function (confirmed) {
+ if(!confirmed) { return; }
+ $scope.removeAction(force);
+ });
+ };
+
+ $scope.removeAction = function (force) {
+ force = !!force;
$('#loadImagesSpinner').show();
var counter = 0;
var complete = function () {
@@ -71,7 +79,7 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) {
angular.forEach($scope.images, function (i) {
if (i.Checked) {
counter = counter + 1;
- Image.remove({id: i.Id}, function (d) {
+ Image.remove({id: i.Id, force: force}, function (d) {
if (d[0].message) {
$('#loadImagesSpinner').hide();
Messages.error("Unable to remove image", {}, d[0].message);
diff --git a/app/components/services/services.html b/app/components/services/services.html
index 0e958cecc..6eb427ac6 100644
--- a/app/components/services/services.html
+++ b/app/components/services/services.html
@@ -58,16 +58,25 @@
+
+
+ Ownership
+
+
+
+
{{ service.Name }}
- {{ service.Image }}
+ {{ service.Image | hideshasum }}
{{ service.Mode }}
+ {{ service.Running }}
+ /
+ {{ service.Replicas }}
- {{ service.Replicas }}
Scale
@@ -76,12 +85,33 @@
+
+
+
+
+ Private
+
+
+ Private (owner: {{ service.Owner }})
+
+ Switch to public
+
+
+
+ Private
+ Switch to public
+
+
+
+ Public
+
+
- Loading...
+ Loading...
- No services available.
+ No services available.
diff --git a/app/components/services/servicesController.js b/app/components/services/servicesController.js
index 1fb5c1dbb..13f63fff6 100644
--- a/app/components/services/servicesController.js
+++ b/app/components/services/servicesController.js
@@ -1,12 +1,40 @@
angular.module('services', [])
-.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination',
-function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination) {
+.controller('ServicesController', ['$q', '$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination', 'Task', 'Node', 'Authentication', 'UserService', 'ModalService', 'ResourceControlService',
+function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination, Task, Node, Authentication, UserService, ModalService, ResourceControlService) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('services');
$scope.sortType = 'Name';
$scope.sortReverse = false;
+ function removeServiceResourceControl(service) {
+ volumeResourceControlQueries = [];
+ angular.forEach(service.Mounts, function (mount) {
+ if (mount.Type === 'volume') {
+ volumeResourceControlQueries.push(ResourceControlService.removeVolumeResourceControl(service.Metadata.ResourceControl.OwnerId, mount.Source));
+ }
+ });
+
+ $q.all(volumeResourceControlQueries)
+ .then(function success() {
+ return ResourceControlService.removeServiceResourceControl(service.Metadata.ResourceControl.OwnerId, service.Id);
+ })
+ .then(function success() {
+ delete service.Metadata.ResourceControl;
+ Messages.send('Ownership changed to public', service.Id);
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to change service ownership");
+ });
+ }
+
+ $scope.switchOwnership = function(volume) {
+ ModalService.confirmServiceOwnershipChange(function (confirmed) {
+ if(!confirmed) { return; }
+ removeServiceResourceControl(volume);
+ });
+ };
+
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('services', $scope.state.pagination_count);
};
@@ -57,9 +85,21 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagina
$('#loadServicesSpinner').hide();
Messages.error("Unable to remove service", {}, d[0].message);
} else {
- Messages.send("Service deleted", service.Id);
- var index = $scope.services.indexOf(service);
- $scope.services.splice(index, 1);
+ if (service.Metadata && service.Metadata.ResourceControl) {
+ ResourceControlService.removeServiceResourceControl(service.Metadata.ResourceControl.OwnerId, service.Id)
+ .then(function success() {
+ Messages.send("Service deleted", service.Id);
+ var index = $scope.services.indexOf(service);
+ $scope.services.splice(index, 1);
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to remove service ownership");
+ });
+ } else {
+ Messages.send("Service deleted", service.Id);
+ var index = $scope.services.indexOf(service);
+ $scope.services.splice(index, 1);
+ }
}
complete();
}, function (e) {
@@ -70,17 +110,58 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagina
});
};
+ function mapUsersToServices(users) {
+ angular.forEach($scope.services, function (service) {
+ if (service.Metadata) {
+ var serviceRC = service.Metadata.ResourceControl;
+ if (serviceRC && serviceRC.OwnerId != $scope.user.ID) {
+ angular.forEach(users, function (user) {
+ if (serviceRC.OwnerId === user.Id) {
+ service.Owner = user.Username;
+ }
+ });
+ }
+ }
+ });
+ }
+
function fetchServices() {
$('#loadServicesSpinner').show();
- Service.query({}, function (d) {
- $scope.services = d.map(function (service) {
- return new ServiceViewModel(service);
+
+ var userDetails = Authentication.getUserDetails();
+ $scope.user = userDetails;
+
+ $q.all({
+ services: Service.query({}).$promise,
+ tasks: Task.query({filters: {'desired-state': ['running']}}).$promise,
+ nodes: Node.query({}).$promise,
+ })
+ .then(function success(data) {
+ $scope.services = data.services.map(function (service) {
+ var serviceTasks = data.tasks.filter(function (task) {
+ return task.ServiceID === service.ID;
+ });
+ var taskNodes = data.nodes.filter(function (node) {
+ return node.Spec.Availability === 'active' && node.Status.State === 'ready';
+ });
+ return new ServiceViewModel(service, serviceTasks, taskNodes);
});
- $('#loadServicesSpinner').hide();
- }, function(e) {
- $('#loadServicesSpinner').hide();
- Messages.error("Failure", e, "Unable to retrieve services");
+ if (userDetails.role === 1) {
+ UserService.users()
+ .then(function success(data) {
+ mapUsersToServices(data);
+ })
+ .finally(function final() {
+ $('#loadServicesSpinner').hide();
+ });
+ }
+ })
+ .catch(function error(err) {
$scope.services = [];
+ Messages.error("Failure", err, "Unable to retrieve services");
+ })
+ .finally(function final() {
+ $('#loadServicesSpinner').hide();
});
}
diff --git a/app/components/settings/settingsController.js b/app/components/settings/settingsController.js
index d79dcc2e0..eb20ec9bb 100644
--- a/app/components/settings/settingsController.js
+++ b/app/components/settings/settingsController.js
@@ -1,6 +1,6 @@
angular.module('settings', [])
-.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Users', 'Messages',
-function ($scope, $state, $sanitize, Users, Messages) {
+.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Messages',
+function ($scope, $state, $sanitize, Authentication, UserService, Messages) {
$scope.formValues = {
currentPassword: '',
newPassword: '',
@@ -9,22 +9,21 @@ function ($scope, $state, $sanitize, Users, Messages) {
$scope.updatePassword = function() {
$scope.invalidPassword = false;
- $scope.error = false;
+ var userID = Authentication.getUserDetails().ID;
var currentPassword = $sanitize($scope.formValues.currentPassword);
- Users.checkPassword({ username: $scope.username, password: currentPassword }, function (d) {
- if (d.valid) {
- var newPassword = $sanitize($scope.formValues.newPassword);
- Users.update({ username: $scope.username, password: newPassword }, function (d) {
- Messages.send("Success", "Password successfully updated");
- $state.reload();
- }, function (e) {
- Messages.error("Failure", e, "Unable to update password");
- });
- } else {
+ var newPassword = $sanitize($scope.formValues.newPassword);
+
+ UserService.updateUserPassword(userID, currentPassword, newPassword)
+ .then(function success() {
+ Messages.send("Success", "Password successfully updated");
+ $state.reload();
+ })
+ .catch(function error(err) {
+ if (err.invalidPassword) {
$scope.invalidPassword = true;
+ } else {
+ Messages.error("Failure", err, err.msg);
}
- }, function (e) {
- Messages.error("Failure", e, "Unable to check password validity");
});
};
}]);
diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html
index 1d29b8f8d..5515fa69e 100644
--- a/app/components/sidebar/sidebar.html
+++ b/app/components/sidebar/sidebar.html
@@ -50,7 +50,10 @@
-
+
diff --git a/app/components/sidebar/sidebarController.js b/app/components/sidebar/sidebarController.js
index f58d8399a..9b040af7f 100644
--- a/app/components/sidebar/sidebarController.js
+++ b/app/components/sidebar/sidebarController.js
@@ -1,39 +1,41 @@
angular.module('sidebar', [])
-.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'Messages',
-function ($scope, $state, Settings, Config, EndpointService, StateManager, Messages) {
+.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'EndpointProvider', 'Messages', 'Authentication',
+function ($scope, $state, Settings, Config, EndpointService, StateManager, EndpointProvider, Messages, Authentication) {
Config.$promise.then(function (c) {
$scope.logo = c.logo;
});
$scope.uiVersion = Settings.uiVersion;
+ $scope.userRole = Authentication.getUserDetails().role;
$scope.switchEndpoint = function(endpoint) {
- EndpointService.setActive(endpoint.Id).then(function success(data) {
+ var activeEndpointID = EndpointProvider.endpointID();
+ EndpointProvider.setEndpointID(endpoint.Id);
+ StateManager.updateEndpointState(true)
+ .then(function success() {
+ $state.go('dashboard');
+ })
+ .catch(function error(err) {
+ Messages.error("Failure", err, "Unable to connect to the Docker endpoint");
+ EndpointProvider.setEndpointID(activeEndpointID);
StateManager.updateEndpointState(true)
- .then(function success() {
- $state.reload();
- }, function error(err) {
- Messages.error("Failure", err, "Unable to connect to the Docker endpoint");
- });
- }, function error(err) {
- Messages.error("Failure", err, "Unable to switch to new endpoint");
+ .then(function success() {});
});
};
function fetchEndpoints() {
- EndpointService.endpoints().then(function success(data) {
+ EndpointService.endpoints()
+ .then(function success(data) {
$scope.endpoints = data;
- EndpointService.getActive().then(function success(data) {
- angular.forEach($scope.endpoints, function (endpoint) {
- if (endpoint.Id === data.Id) {
- $scope.activeEndpoint = endpoint;
- }
- });
- }, function error(err) {
- Messages.error("Failure", err, "Unable to retrieve active endpoint");
+ var activeEndpointID = EndpointProvider.endpointID();
+ angular.forEach($scope.endpoints, function (endpoint) {
+ if (endpoint.Id === activeEndpointID) {
+ $scope.activeEndpoint = endpoint;
+ }
});
- }, function error(err) {
+ })
+ .catch(function error(err) {
$scope.endpoints = [];
});
}
diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js
index 475af604c..4329d0a05 100644
--- a/app/components/stats/statsController.js
+++ b/app/components/stats/statsController.js
@@ -22,6 +22,8 @@ function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stat
$scope.containerTop = data;
});
};
+ var destroyed = false;
+ var timeout;
$document.ready(function(){
var cpuLabels = [];
var cpuData = [];
@@ -117,11 +119,6 @@ function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stat
});
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
- function setUpdateStatsTimeout() {
- if(!destroyed) {
- timeout = $timeout(updateStats, 5000);
- }
- }
function updateStats() {
Container.stats({id: $stateParams.id}, function (d) {
@@ -145,8 +142,7 @@ function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stat
});
}
- var destroyed = false;
- var timeout;
+
$scope.$on('$destroy', function () {
destroyed = true;
$timeout.cancel(timeout);
@@ -204,6 +200,12 @@ function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stat
}
return cpuPercent;
}
+
+ function setUpdateStatsTimeout() {
+ if(!destroyed) {
+ timeout = $timeout(updateStats, 5000);
+ }
+ }
});
Container.get({id: $stateParams.id}, function (d) {
diff --git a/app/components/swarm/swarm.html b/app/components/swarm/swarm.html
index 661dbd710..d3aac8e75 100644
--- a/app/components/swarm/swarm.html
+++ b/app/components/swarm/swarm.html
@@ -163,57 +163,65 @@
-
+
Name
-
-
+
+
-
+
Role
-
-
+
+
-
+
CPU
-
-
+
+
-
+
Memory
-
-
+
+
-
+
Engine
-
-
+
+
+
+
+
+
+ IP Address
+
+
-
+
Status
-
-
+
+
- {{ node.Description.Hostname }}
- {{ node.Spec.Role }}
- {{ node.Description.Resources.NanoCPUs / 1000000000 }}
- {{ node.Description.Resources.MemoryBytes|humansize }}
- {{ node.Description.Engine.EngineVersion }}
- {{ node.Status.State }}
+ {{ node.Hostname }}
+ {{ node.Role }}
+ {{ node.CPUs / 1000000000 }}
+ {{ node.Memory|humansize }}
+ {{ node.EngineVersion }}
+ {{ node.Addr }}
+ {{ node.Status }}
diff --git a/app/components/swarm/swarmController.js b/app/components/swarm/swarmController.js
index 713db07ef..5d844c92c 100644
--- a/app/components/swarm/swarmController.js
+++ b/app/components/swarm/swarmController.js
@@ -28,7 +28,9 @@ function ($scope, Info, Version, Node, Pagination) {
$scope.info = d;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
Node.query({}, function(d) {
- $scope.nodes = d;
+ $scope.nodes = d.map(function (node) {
+ return new NodeViewModel(node);
+ });
var CPU = 0, memory = 0;
angular.forEach(d, function(node) {
CPU += node.Description.Resources.NanoCPUs;
@@ -73,7 +75,7 @@ function ($scope, Info, Version, Node, Pagination) {
var node = {};
node.name = info[offset][0];
node.ip = info[offset][1];
- node.id = info[offset + 1][1];
+ node.Id = info[offset + 1][1];
node.status = info[offset + 2][1];
node.containers = info[offset + 3][1];
node.cpu = info[offset + 4][1].split('/')[1];
diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html
index 57ea3286f..513b55580 100644
--- a/app/components/templates/templates.html
+++ b/app/components/templates/templates.html
@@ -10,7 +10,7 @@