feat(uac): add multi user management and UAC (#647)

pull/648/head
Anthony Lapenna 2017-03-12 17:24:15 +01:00 committed by GitHub
parent f28f223624
commit 80d50378c5
91 changed files with 3973 additions and 866 deletions

View File

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

View File

@ -1,9 +1,12 @@
package bolt package bolt
import ( import (
"log"
"os"
"time" "time"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/portainer/portainer"
) )
// Store defines the implementation of portainer.DataStore using // Store defines the implementation of portainer.DataStore using
@ -15,27 +18,47 @@ type Store struct {
// Services // Services
UserService *UserService UserService *UserService
EndpointService *EndpointService EndpointService *EndpointService
ResourceControlService *ResourceControlService
VersionService *VersionService
db *bolt.DB db *bolt.DB
checkForDataMigration bool
} }
const ( const (
databaseFileName = "portainer.db" databaseFileName = "portainer.db"
versionBucketName = "version"
userBucketName = "users" userBucketName = "users"
endpointBucketName = "endpoints" endpointBucketName = "endpoints"
activeEndpointBucketName = "activeEndpoint" containerResourceControlBucketName = "containerResourceControl"
serviceResourceControlBucketName = "serviceResourceControl"
volumeResourceControlBucketName = "volumeResourceControl"
) )
// NewStore initializes a new Store and the associated services // NewStore initializes a new Store and the associated services
func NewStore(storePath string) *Store { func NewStore(storePath string) (*Store, error) {
store := &Store{ store := &Store{
Path: storePath, Path: storePath,
UserService: &UserService{}, UserService: &UserService{},
EndpointService: &EndpointService{}, EndpointService: &EndpointService{},
ResourceControlService: &ResourceControlService{},
VersionService: &VersionService{},
} }
store.UserService.store = store store.UserService.store = store
store.EndpointService.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. // Open opens and initializes the BoltDB database.
@ -47,7 +70,11 @@ func (store *Store) Open() error {
} }
store.db = db store.db = db
return db.Update(func(tx *bolt.Tx) error { 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 { if err != nil {
return err return err
} }
@ -55,7 +82,15 @@ func (store *Store) Open() error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -70,3 +105,32 @@ func (store *Store) Close() error {
} }
return nil 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
}

View File

@ -12,10 +12,6 @@ type EndpointService struct {
store *Store store *Store
} }
const (
activeEndpointID = 0
)
// Endpoint returns an endpoint by ID. // Endpoint returns an endpoint by ID.
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) { func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var data []byte 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 { func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
data, err := internal.MarshalEndpoint(endpoint) data, err := internal.MarshalEndpoint(endpoint)
if err != nil { if err != nil {
@ -210,6 +150,5 @@ func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket)
func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error { func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id) endpoint.ID = portainer.EndpointID(id)
return marshalAndStoreEndpoint(endpoint, bucket) return marshalAndStoreEndpoint(endpoint, bucket)
} }

View File

@ -27,6 +27,16 @@ func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
return json.Unmarshal(data, endpoint) 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. // Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices // This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys. // so that they can be used as BoltDB keys.

View File

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

View File

@ -12,12 +12,12 @@ type UserService struct {
store *Store store *Store
} }
// User returns a user by username. // User returns a user by ID
func (service *UserService) User(username string) (*portainer.User, error) { func (service *UserService) User(ID portainer.UserID) (*portainer.User, error) {
var data []byte var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error { err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName)) bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get([]byte(username)) value := bucket.Get(internal.Itob(int(ID)))
if value == nil { if value == nil {
return portainer.ErrUserNotFound return portainer.ErrUserNotFound
} }
@ -38,8 +38,88 @@ func (service *UserService) User(username string) (*portainer.User, error) {
return &user, nil 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. // 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) data, err := internal.MarshalUser(user)
if err != nil { if err != nil {
return err return err
@ -47,7 +127,41 @@ func (service *UserService) UpdateUser(user *portainer.User) error {
return service.store.db.Update(func(tx *bolt.Tx) error { return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName)) 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 { if err != nil {
return err return err
} }

View File

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

View File

@ -36,8 +36,17 @@ func initFileService(dataStorePath string) portainer.FileService {
} }
func initStore(dataStorePath string) *bolt.Store { func initStore(dataStorePath string) *bolt.Store {
var store = bolt.NewStore(dataStorePath) store, err := bolt.NewStore(dataStorePath)
err := store.Open() if err != nil {
log.Fatal(err)
}
err = store.Open()
if err != nil {
log.Fatal(err)
}
err = store.MigrateData()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -91,31 +100,6 @@ func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService
return &endpoints[0] 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() { func main() {
flags := initCLI() flags := initCLI()
@ -132,7 +116,29 @@ func main() {
settings := initSettings(authorizeEndpointMgmt, flags) 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{ var server portainer.Server = &http.Server{
BindAddress: *flags.Addr, BindAddress: *flags.Addr,
@ -143,10 +149,10 @@ func main() {
EndpointManagement: authorizeEndpointMgmt, EndpointManagement: authorizeEndpointMgmt,
UserService: store.UserService, UserService: store.UserService,
EndpointService: store.EndpointService, EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
CryptoService: cryptoService, CryptoService: cryptoService,
JWTService: jwtService, JWTService: jwtService,
FileService: fileService, FileService: fileService,
ActiveEndpoint: activeEndpoint,
} }
log.Printf("Starting Portainer on %s", *flags.Addr) log.Printf("Starting Portainer on %s", *flags.Addr)

View File

@ -66,7 +66,10 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint { func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var 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 = original
endpoint.URL = updated.URL endpoint.URL = updated.URL
if updated.TLS { if updated.TLS {

View File

@ -3,18 +3,25 @@ package portainer
// General errors. // General errors.
const ( const (
ErrUnauthorized = Error("Unauthorized") ErrUnauthorized = Error("Unauthorized")
ErrResourceAccessDenied = Error("Access denied to resource")
) )
// User errors. // User errors.
const ( const (
ErrUserNotFound = Error("User not found") ErrUserNotFound = Error("User not found")
ErrUserAlreadyExists = Error("User already exists")
ErrAdminAlreadyInitialized = Error("Admin user already initialized") ErrAdminAlreadyInitialized = Error("Admin user already initialized")
) )
// Endpoint errors. // Endpoint errors.
const ( const (
ErrEndpointNotFound = Error("Endpoint not found") ErrEndpointNotFound = Error("Endpoint not found")
ErrNoActiveEndpoint = Error("Undefined Docker endpoint") ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Version errors.
const (
ErrDBVersionNotFound = Error("DB version not found")
) )
// Crypto errors. // Crypto errors.
@ -26,6 +33,7 @@ const (
const ( const (
ErrSecretGeneration = Error("Unable to generate secret key") ErrSecretGeneration = Error("Unable to generate secret key")
ErrInvalidJWTToken = Error("Invalid JWT token") ErrInvalidJWTToken = Error("Invalid JWT token")
ErrMissingContextData = Error("Unable to find JWT data in request context")
) )
// File errors. // File errors.

View File

@ -33,12 +33,14 @@ const (
) )
// NewAuthHandler returns a new instance of AuthHandler. // NewAuthHandler returns a new instance of AuthHandler.
func NewAuthHandler() *AuthHandler { func NewAuthHandler(mw *middleWareService) *AuthHandler {
h := &AuthHandler{ h := &AuthHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
} }
h.HandleFunc("/auth", h.handlePostAuth) h.Handle("/auth",
mw.public(http.HandlerFunc(h.handlePostAuth)))
return h return h
} }
@ -68,7 +70,7 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
var username = req.Username var username = req.Username
var password = req.Password var password = req.Password
u, err := handler.UserService.User(username) u, err := handler.UserService.UserByUsername(username)
if err == portainer.ErrUserNotFound { if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger) Error(w, err, http.StatusNotFound, handler.Logger)
return return
@ -84,7 +86,9 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
} }
tokenData := &portainer.TokenData{ tokenData := &portainer.TokenData{
username, ID: u.ID,
Username: u.Username,
Role: u.Role,
} }
token, err := handler.JWTService.GenerateToken(tokenData) token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil { if err != nil {

View File

@ -1,16 +1,14 @@
package http package http
import ( import (
"strconv"
"github.com/portainer/portainer" "github.com/portainer/portainer"
"io"
"log" "log"
"net"
"net/http" "net/http"
"net/http/httputil"
"net/url" "net/url"
"os" "os"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -19,141 +17,94 @@ import (
type DockerHandler struct { type DockerHandler struct {
*mux.Router *mux.Router
Logger *log.Logger Logger *log.Logger
middleWareService *middleWareService EndpointService portainer.EndpointService
proxy http.Handler ProxyFactory ProxyFactory
proxies map[portainer.EndpointID]http.Handler
} }
// NewDockerHandler returns a new instance of DockerHandler. // NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(middleWareService *middleWareService) *DockerHandler { func NewDockerHandler(mw *middleWareService, resourceControlService portainer.ResourceControlService) *DockerHandler {
h := &DockerHandler{ h := &DockerHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService, 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.PathPrefix("/{id}/").Handler(
h.proxyRequestsToDockerAPI(w, r) mw.authenticated(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
})))
return h 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) { func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
if handler.proxy != nil { vars := mux.Vars(r)
handler.proxy.ServeHTTP(w, r) id := vars["id"]
} else {
Error(w, portainer.ErrNoActiveEndpoint, http.StatusNotFound, handler.Logger) parsedID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
} }
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
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
}
}
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
} }
func (handler *DockerHandler) setupProxy(endpoint *portainer.Endpoint) error { func (handler *DockerHandler) createAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
var proxy http.Handler var proxy http.Handler
endpointURL, err := url.Parse(endpoint.URL) endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return err
}
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
}
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", "")
}
}
return &httputil.ReverseProxy{Director: director}
}
func newHTTPProxy(u *url.URL) http.Handler {
u.Scheme = "http"
return NewSingleHostReverseProxyWithHostHeader(u)
}
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)
if err != nil { if err != nil {
return nil, err 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 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)
}
}

121
api/http/docker_proxy.go Normal file
View File

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

View File

@ -20,8 +20,7 @@ type EndpointHandler struct {
authorizeEndpointManagement bool authorizeEndpointManagement bool
EndpointService portainer.EndpointService EndpointService portainer.EndpointService
FileService portainer.FileService FileService portainer.FileService
server *Server // server *Server
middleWareService *middleWareService
} }
const ( const (
@ -31,30 +30,24 @@ const (
) )
// NewEndpointHandler returns a new instance of EndpointHandler. // NewEndpointHandler returns a new instance of EndpointHandler.
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler { func NewEndpointHandler(mw *middleWareService) *EndpointHandler {
h := &EndpointHandler{ h := &EndpointHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
} }
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.Handle("/endpoints",
h.handlePostEndpoints(w, r) mw.administrator(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
}))).Methods(http.MethodPost) h.Handle("/endpoints",
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mw.authenticated(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
h.handleGetEndpoints(w, r) h.Handle("/endpoints/{id}",
}))).Methods(http.MethodGet) mw.administrator(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.Handle("/endpoints/{id}",
h.handleGetEndpoint(w, r) mw.administrator(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
}))).Methods(http.MethodGet) h.Handle("/endpoints/{id}/access",
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mw.administrator(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
h.handlePutEndpoint(w, r) h.Handle("/endpoints/{id}",
}))).Methods(http.MethodPut) mw.administrator(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
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)
return h return h
} }
@ -65,12 +58,35 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
Error(w, err, http.StatusInternalServerError, handler.Logger) Error(w, err, http.StatusInternalServerError, handler.Logger)
return 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 // 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) { func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement { if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger) Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
@ -93,6 +109,7 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
Name: req.Name, Name: req.Name,
URL: req.URL, URL: req.URL,
TLS: req.TLS, TLS: req.TLS,
AuthorizedUsers: []portainer.UserID{},
} }
err = handler.EndpointService.CreateEndpoint(endpoint) 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) encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
} }
@ -145,7 +146,6 @@ type postEndpointsResponse struct {
} }
// handleGetEndpoint handles GET requests on /endpoints/:id // handleGetEndpoint handles GET requests on /endpoints/:id
// GET /endpoints/0 returns active endpoint
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) { func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
@ -156,48 +156,6 @@ func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http
return 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)) endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound { if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger) Error(w, err, http.StatusNotFound, handler.Logger)
@ -207,12 +165,58 @@ func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *htt
return 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 { if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger) Error(w, err, http.StatusInternalServerError, handler.Logger)
return
} }
} }
type putEndpointAccessRequest struct {
AuthorizedUsers []int `valid:"required"`
}
// handlePutEndpoint handles PUT requests on /endpoints/:id // handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) { func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement { if !handler.authorizeEndpointManagement {
@ -241,14 +245,25 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
return return
} }
endpoint := &portainer.Endpoint{ endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
ID: portainer.EndpointID(endpointID), if err == portainer.ErrEndpointNotFound {
Name: req.Name, Error(w, err, http.StatusNotFound, handler.Logger)
URL: req.URL, return
TLS: req.TLS, } 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 { if req.TLS {
endpoint.TLS = true
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA) caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
endpoint.TLSCACertPath = caCertPath endpoint.TLSCACertPath = caCertPath
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert) 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) keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
endpoint.TLSKeyPath = keyPath endpoint.TLSKeyPath = keyPath
} else { } else {
endpoint.TLS = false
endpoint.TLSCACertPath = ""
endpoint.TLSCertPath = ""
endpoint.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(endpoint.ID) err = handler.FileService.DeleteTLSFiles(endpoint.ID)
if err != nil { if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger) Error(w, err, http.StatusInternalServerError, handler.Logger)
@ -271,13 +290,12 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
} }
type putEndpointsRequest struct { type putEndpointsRequest struct {
Name string `valid:"required"` Name string `valid:"-"`
URL string `valid:"required"` URL string `valid:"-"`
TLS bool TLS bool `valid:"-"`
} }
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id // handleDeleteEndpoint handles DELETE requests on /endpoints/:id
// DELETE /endpoints/0 deletes the active endpoint
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) { func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement { if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger) Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
@ -293,13 +311,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
return return
} }
var endpoint *portainer.Endpoint endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if id == "0" {
endpoint, err = handler.EndpointService.GetActive()
endpointID = int(endpoint.ID)
} else {
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
}
if err == portainer.ErrEndpointNotFound { if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger) 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) Error(w, err, http.StatusInternalServerError, handler.Logger)
return return
} }
if id == "0" {
err = handler.EndpointService.DeleteActive()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
if endpoint.TLS { if endpoint.TLS {
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID)) err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))

View File

@ -27,6 +27,10 @@ const (
ErrInvalidJSON = portainer.Error("Invalid JSON") ErrInvalidJSON = portainer.Error("Invalid JSON")
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid // 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") 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. // ServeHTTP delegates a request to the appropriate subhandler.

View File

@ -1,33 +1,61 @@
package http package http
import ( import (
"context"
"github.com/portainer/portainer" "github.com/portainer/portainer"
"net/http" "net/http"
"strings" "strings"
) )
// Service represents a service to manage HTTP middlewares type (
type middleWareService struct { // middleWareService represents a service to manage HTTP middlewares
middleWareService struct {
jwtService portainer.JWTService jwtService portainer.JWTService
authDisabled bool authDisabled bool
}
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
for _, mw := range middleware {
h = mw(h)
} }
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 return h
} }
func (service *middleWareService) addMiddleWares(h http.Handler) http.Handler { // authenticated defines a chain of middleware for private endpoints (authentication required)
h = service.middleWareSecureHeaders(h) func (service *middleWareService) authenticated(h http.Handler) http.Handler {
h = service.middleWareAuthenticate(h) h = service.mwCheckAuthentication(h)
h = mwSecureHeaders(h)
return h return h
} }
// middleWareAuthenticate provides secure headers middleware for handlers // administrator defines a chain of middleware for private administrator restricted endpoints
func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handler { // (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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Content-Type-Options", "nosniff") w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-Frame-Options", "DENY") 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 // mwCheckAdministratorRole check the role of the user associated to the request
func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler { func mwCheckAdministratorRole(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 { if !service.authDisabled {
var token string var token string
@ -53,14 +100,20 @@ func (service *middleWareService) middleWareAuthenticate(next http.Handler) http
return return
} }
err := service.jwtService.VerifyToken(token) var err error
tokenData, err = service.jwtService.ParseAndVerifyToken(token)
if err != nil { if err != nil {
Error(w, err, http.StatusUnauthorized, nil) Error(w, err, http.StatusUnauthorized, nil)
return 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 return
}) })
} }

664
api/http/proxy_transport.go Normal file
View File

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

View File

@ -14,31 +14,15 @@ type Server struct {
EndpointManagement bool EndpointManagement bool
UserService portainer.UserService UserService portainer.UserService
EndpointService portainer.EndpointService EndpointService portainer.EndpointService
ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService CryptoService portainer.CryptoService
JWTService portainer.JWTService JWTService portainer.JWTService
FileService portainer.FileService FileService portainer.FileService
Settings *portainer.Settings Settings *portainer.Settings
TemplatesURL string TemplatesURL string
ActiveEndpoint *portainer.Endpoint
Handler *Handler 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
}
// Start starts the HTTP server // Start starts the HTTP server
func (server *Server) Start() error { func (server *Server) Start() error {
middleWareService := &middleWareService{ middleWareService := &middleWareService{
@ -46,7 +30,7 @@ func (server *Server) Start() error {
authDisabled: server.AuthDisabled, authDisabled: server.AuthDisabled,
} }
var authHandler = NewAuthHandler() var authHandler = NewAuthHandler(middleWareService)
authHandler.UserService = server.UserService authHandler.UserService = server.UserService
authHandler.CryptoService = server.CryptoService authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService authHandler.JWTService = server.JWTService
@ -54,18 +38,19 @@ func (server *Server) Start() error {
var userHandler = NewUserHandler(middleWareService) var userHandler = NewUserHandler(middleWareService)
userHandler.UserService = server.UserService userHandler.UserService = server.UserService
userHandler.CryptoService = server.CryptoService userHandler.CryptoService = server.CryptoService
userHandler.ResourceControlService = server.ResourceControlService
var settingsHandler = NewSettingsHandler(middleWareService) var settingsHandler = NewSettingsHandler(middleWareService)
settingsHandler.settings = server.Settings settingsHandler.settings = server.Settings
var templatesHandler = NewTemplatesHandler(middleWareService) var templatesHandler = NewTemplatesHandler(middleWareService)
templatesHandler.templatesURL = server.TemplatesURL templatesHandler.templatesURL = server.TemplatesURL
var dockerHandler = NewDockerHandler(middleWareService) var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
dockerHandler.EndpointService = server.EndpointService
var websocketHandler = NewWebSocketHandler() 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) var endpointHandler = NewEndpointHandler(middleWareService)
endpointHandler.authorizeEndpointManagement = server.EndpointManagement endpointHandler.authorizeEndpointManagement = server.EndpointManagement
endpointHandler.EndpointService = server.EndpointService endpointHandler.EndpointService = server.EndpointService
endpointHandler.FileService = server.FileService endpointHandler.FileService = server.FileService
endpointHandler.server = server
var uploadHandler = NewUploadHandler(middleWareService) var uploadHandler = NewUploadHandler(middleWareService)
uploadHandler.FileService = server.FileService uploadHandler.FileService = server.FileService
var fileHandler = newFileHandler(server.AssetsPath) var fileHandler = newFileHandler(server.AssetsPath)
@ -81,10 +66,6 @@ func (server *Server) Start() error {
FileHandler: fileHandler, FileHandler: fileHandler,
UploadHandler: uploadHandler, UploadHandler: uploadHandler,
} }
err := server.updateActiveEndpoint(server.ActiveEndpoint)
if err != nil {
return err
}
return http.ListenAndServe(server.BindAddress, server.Handler) return http.ListenAndServe(server.BindAddress, server.Handler)
} }

View File

@ -14,18 +14,18 @@ import (
type SettingsHandler struct { type SettingsHandler struct {
*mux.Router *mux.Router
Logger *log.Logger Logger *log.Logger
middleWareService *middleWareService
settings *portainer.Settings settings *portainer.Settings
} }
// NewSettingsHandler returns a new instance of SettingsHandler. // NewSettingsHandler returns a new instance of SettingsHandler.
func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler { func NewSettingsHandler(mw *middleWareService) *SettingsHandler {
h := &SettingsHandler{ h := &SettingsHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
} }
h.HandleFunc("/settings", h.handleGetSettings) h.Handle("/settings",
mw.public(http.HandlerFunc(h.handleGetSettings)))
return h return h
} }

View File

@ -13,20 +13,17 @@ import (
type TemplatesHandler struct { type TemplatesHandler struct {
*mux.Router *mux.Router
Logger *log.Logger Logger *log.Logger
middleWareService *middleWareService
templatesURL string templatesURL string
} }
// NewTemplatesHandler returns a new instance of TemplatesHandler. // NewTemplatesHandler returns a new instance of TemplatesHandler.
func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler { func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
h := &TemplatesHandler{ h := &TemplatesHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
} }
h.Handle("/templates", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.Handle("/templates",
h.handleGetTemplates(w, r) mw.authenticated(http.HandlerFunc(h.handleGetTemplates)))
})))
return h return h
} }

View File

@ -16,19 +16,16 @@ type UploadHandler struct {
*mux.Router *mux.Router
Logger *log.Logger Logger *log.Logger
FileService portainer.FileService FileService portainer.FileService
middleWareService *middleWareService
} }
// NewUploadHandler returns a new instance of UploadHandler. // NewUploadHandler returns a new instance of UploadHandler.
func NewUploadHandler(middleWareService *middleWareService) *UploadHandler { func NewUploadHandler(mw *middleWareService) *UploadHandler {
h := &UploadHandler{ h := &UploadHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
} }
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}",
h.handlePostUploadTLS(w, r) mw.authenticated(http.HandlerFunc(h.handlePostUploadTLS)))
})))
return h return h
} }

View File

@ -1,6 +1,8 @@
package http package http
import ( import (
"strconv"
"github.com/portainer/portainer" "github.com/portainer/portainer"
"encoding/json" "encoding/json"
@ -17,41 +19,42 @@ type UserHandler struct {
*mux.Router *mux.Router
Logger *log.Logger Logger *log.Logger
UserService portainer.UserService UserService portainer.UserService
ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService CryptoService portainer.CryptoService
middleWareService *middleWareService
} }
// NewUserHandler returns a new instance of UserHandler. // NewUserHandler returns a new instance of UserHandler.
func NewUserHandler(middleWareService *middleWareService) *UserHandler { func NewUserHandler(mw *middleWareService) *UserHandler {
h := &UserHandler{ h := &UserHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
} }
h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.Handle("/users",
h.handlePostUsers(w, r) mw.administrator(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
}))) h.Handle("/users",
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mw.administrator(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
h.handleGetUser(w, r) h.Handle("/users/{id}",
}))).Methods(http.MethodGet) mw.administrator(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.Handle("/users/{id}",
h.handlePutUser(w, r) mw.authenticated(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
}))).Methods(http.MethodPut) h.Handle("/users/{id}",
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mw.administrator(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
h.handlePostUserPasswd(w, r) h.Handle("/users/{id}/passwd",
}))) mw.authenticated(http.HandlerFunc(h.handlePostUserPasswd)))
h.HandleFunc("/users/admin/check", h.handleGetAdminCheck) h.Handle("/users/{userId}/resources/{resourceType}",
h.HandleFunc("/users/admin/init", h.handlePostAdminInit) 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 return h
} }
// handlePostUsers handles POST requests on /users // handlePostUsers handles POST requests on /users
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) { func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
return
}
var req postUsersRequest var req postUsersRequest
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) Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
@ -64,8 +67,26 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
return 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, Username: req.Username,
Role: role,
} }
user.Password, err = handler.CryptoService.Hash(req.Password) user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil { if err != nil {
@ -73,7 +94,7 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
return return
} }
err = handler.UserService.UpdateUser(user) err = handler.UserService.CreateUser(user)
if err != nil { if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger) Error(w, err, http.StatusInternalServerError, handler.Logger)
return return
@ -83,9 +104,24 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
type postUsersRequest struct { type postUsersRequest struct {
Username string `valid:"alphanum,required"` Username string `valid:"alphanum,required"`
Password string `valid:"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) { func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost}) handleNotAllowed(w, []string{http.MethodPost})
@ -93,15 +129,21 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
} }
vars := mux.Vars(r) 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 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) Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return return
} }
_, err := govalidator.ValidateStruct(req) _, err = govalidator.ValidateStruct(req)
if err != nil { if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return return
@ -109,7 +151,7 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
var password = req.Password var password = req.Password
u, err := handler.UserService.User(username) u, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound { if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger) Error(w, err, http.StatusNotFound, handler.Logger)
return return
@ -135,12 +177,18 @@ type postUserPasswdResponse struct {
Valid bool `json:"valid"` 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) { func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) 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 { if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger) Error(w, err, http.StatusNotFound, handler.Logger)
return return
@ -153,30 +201,74 @@ func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request
encodeJSON(w, &user, handler.Logger) 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) { 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 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) Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return return
} }
_, err := govalidator.ValidateStruct(req) _, err = govalidator.ValidateStruct(req)
if err != nil { if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return return
} }
user := &portainer.User{ if req.Password == "" && req.Role == 0 {
Username: req.Username, Error(w, ErrInvalidRequestFormat, 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
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Password != "" {
user.Password, err = handler.CryptoService.Hash(req.Password) user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil { if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger) Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return return
} }
}
err = handler.UserService.UpdateUser(user) 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 { if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger) Error(w, err, http.StatusInternalServerError, handler.Logger)
return return
@ -184,8 +276,8 @@ func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request
} }
type putUserRequest struct { type putUserRequest struct {
Username string `valid:"alphanum,required"` Password string `valid:"-"`
Password string `valid:"required"` Role int `valid:"-"`
} }
// handlePostAdminInit handles GET requests on /users/admin/check // handlePostAdminInit handles GET requests on /users/admin/check
@ -195,17 +287,15 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R
return return
} }
user, err := handler.UserService.User("admin") users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
if err == portainer.ErrUserNotFound { if err != nil {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger) Error(w, err, http.StatusInternalServerError, handler.Logger)
return return
} }
if len(users) == 0 {
user.Password = "" Error(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
encodeJSON(w, &user, handler.Logger) return
}
} }
// handlePostAdminInit handles POST requests on /users/admin/init // handlePostAdminInit handles POST requests on /users/admin/init
@ -227,10 +317,11 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
return return
} }
user, err := handler.UserService.User("admin") user, err := handler.UserService.UserByUsername("admin")
if err == portainer.ErrUserNotFound { if err == portainer.ErrUserNotFound {
user := &portainer.User{ user := &portainer.User{
Username: "admin", Username: "admin",
Role: portainer.AdministratorRole,
} }
user.Password, err = handler.CryptoService.Hash(req.Password) user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil { if err != nil {
@ -238,7 +329,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
return return
} }
err = handler.UserService.UpdateUser(user) err = handler.UserService.CreateUser(user)
if err != nil { if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger) Error(w, err, http.StatusInternalServerError, handler.Logger)
return return
@ -256,3 +347,134 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
type postAdminInitRequest struct { type postAdminInitRequest struct {
Password string `valid:"required"` 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
}
}

View File

@ -1,8 +1,6 @@
package http package http
import ( import (
"github.com/portainer/portainer"
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
@ -14,9 +12,11 @@ import (
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"os" "os"
"strconv"
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/portainer/portainer"
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
) )
@ -24,8 +24,7 @@ import (
type WebSocketHandler struct { type WebSocketHandler struct {
*mux.Router *mux.Router
Logger *log.Logger Logger *log.Logger
middleWareService *middleWareService EndpointService portainer.EndpointService
endpoint *portainer.Endpoint
} }
// NewWebSocketHandler returns a new instance of WebSocketHandler. // NewWebSocketHandler returns a new instance of WebSocketHandler.
@ -41,34 +40,47 @@ func NewWebSocketHandler() *WebSocketHandler {
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) { func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
qry := ws.Request().URL.Query() qry := ws.Request().URL.Query()
execID := qry.Get("id") execID := qry.Get("id")
edpID := qry.Get("endpointId")
// Should not be managed here parsedID, err := strconv.Atoi(edpID)
endpoint, err := url.Parse(handler.endpoint.URL)
if err != nil { 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 return
} }
var host string var host string
if endpoint.Scheme == "tcp" { if endpointURL.Scheme == "tcp" {
host = endpoint.Host host = endpointURL.Host
} else if endpoint.Scheme == "unix" { } else if endpointURL.Scheme == "unix" {
host = endpoint.Path host = endpointURL.Path
} }
// Should not be managed here // Should not be managed here
var tlsConfig *tls.Config var tlsConfig *tls.Config
if handler.endpoint.TLS { if endpoint.TLS {
tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath, tlsConfig, err = createTLSConfiguration(endpoint.TLSCACertPath,
handler.endpoint.TLSCertPath, endpoint.TLSCertPath,
handler.endpoint.TLSKeyPath) endpoint.TLSKeyPath)
if err != nil { if err != nil {
log.Fatalf("Unable to create TLS configuration: %s", err) log.Fatalf("Unable to create TLS configuration: %s", err)
return 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) log.Fatalf("error during hijack: %s", err)
return return
} }

View File

@ -4,9 +4,10 @@ import (
"github.com/portainer/portainer" "github.com/portainer/portainer"
"fmt" "fmt"
"time"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"time"
) )
// Service represents a service for managing JWT tokens. // Service represents a service for managing JWT tokens.
@ -15,7 +16,9 @@ type Service struct {
} }
type claims struct { type claims struct {
UserID int `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Role int `json:"role"`
jwt.StandardClaims jwt.StandardClaims
} }
@ -35,7 +38,9 @@ func NewService() (*Service, error) {
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) { func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
expireToken := time.Now().Add(time.Hour * 8).Unix() expireToken := time.Now().Add(time.Hour * 8).Unix()
cl := claims{ cl := claims{
int(data.ID),
data.Username, data.Username,
int(data.Role),
jwt.StandardClaims{ jwt.StandardClaims{
ExpiresAt: expireToken, ExpiresAt: expireToken,
}, },
@ -50,17 +55,25 @@ func (service *Service) GenerateToken(data *portainer.TokenData) (string, error)
return signedToken, nil return signedToken, nil
} }
// VerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid. // ParseAndVerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
func (service *Service) VerifyToken(token string) error { func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, error) {
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { parsedToken, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
return nil, msg return nil, msg
} }
return service.secret, nil return service.secret, nil
}) })
if err != nil || parsedToken == nil || !parsedToken.Valid { if err == nil && parsedToken != nil {
return portainer.ErrInvalidJWTToken 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 nil return tokenData, nil
}
}
return nil, portainer.ErrInvalidJWTToken
} }

View File

@ -41,13 +41,24 @@ type (
// User represent a user account. // User represent a user account.
User struct { User struct {
ID UserID `json:"Id"`
Username string `json:"Username"` Username string `json:"Username"`
Password string `json:"Password,omitempty"` 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 represents the data embedded in a JWT token.
TokenData struct { TokenData struct {
ID UserID
Username string Username string
Role UserRole
} }
// EndpointID represents an endpoint identifier. // EndpointID represents an endpoint identifier.
@ -63,8 +74,24 @@ type (
TLSCACertPath string `json:"TLSCACert,omitempty"` TLSCACertPath string `json:"TLSCACert,omitempty"`
TLSCertPath string `json:"TLSCert,omitempty"` TLSCertPath string `json:"TLSCert,omitempty"`
TLSKeyPath string `json:"TLSKey,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. // 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. // It can be either a TLS CA file, a TLS certificate file or a TLS key file.
TLSFileType int TLSFileType int
@ -79,32 +106,49 @@ type (
DataStore interface { DataStore interface {
Open() error Open() error
Close() error Close() error
MigrateData() error
} }
// Server defines the interface to serve the data. // Server defines the interface to serve the API.
Server interface { Server interface {
Start() error Start() error
} }
// UserService represents a service for managing users. // UserService represents a service for managing user data.
UserService interface { UserService interface {
User(username string) (*User, error) User(ID UserID) (*User, error)
UpdateUser(user *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 { EndpointService interface {
Endpoint(ID EndpointID) (*Endpoint, error) Endpoint(ID EndpointID) (*Endpoint, error)
Endpoints() ([]Endpoint, error) Endpoints() ([]Endpoint, error)
CreateEndpoint(endpoint *Endpoint) error CreateEndpoint(endpoint *Endpoint) error
UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error
DeleteEndpoint(ID EndpointID) error DeleteEndpoint(ID EndpointID) error
GetActive() (*Endpoint, error)
SetActive(endpoint *Endpoint) error
DeleteActive() error
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) 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 represents a service for encrypting/hashing data.
CryptoService interface { CryptoService interface {
Hash(data string) (string, error) Hash(data string) (string, error)
@ -114,7 +158,7 @@ type (
// JWTService represents a service for managing JWT tokens. // JWTService represents a service for managing JWT tokens.
JWTService interface { JWTService interface {
GenerateToken(data *TokenData) (string, error) GenerateToken(data *TokenData) (string, error)
VerifyToken(token string) error ParseAndVerifyToken(token string) (*TokenData, error)
} }
// FileService represents a service for managing files. // FileService represents a service for managing files.
@ -131,8 +175,10 @@ type (
) )
const ( const (
// APIVersion is the version number of portainer API. // APIVersion is the version number of Portainer API.
APIVersion = "1.11.4" APIVersion = "1.11.4"
// DBVersion is the version number of Portainer database.
DBVersion = 1
) )
const ( const (
@ -143,3 +189,27 @@ const (
// TLSFileKey represents a TLS key file. // TLSFileKey represents a TLS key file.
TLSFileKey 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
)

View File

@ -30,6 +30,7 @@ angular.module('portainer', [
'createVolume', 'createVolume',
'docker', 'docker',
'endpoint', 'endpoint',
'endpointAccess',
'endpointInit', 'endpointInit',
'endpoints', 'endpoints',
'events', 'events',
@ -47,6 +48,8 @@ angular.module('portainer', [
'swarm', 'swarm',
'task', 'task',
'templates', 'templates',
'user',
'users',
'volumes']) 'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider) { .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider) {
'use strict'; 'use strict';
@ -292,6 +295,19 @@ angular.module('portainer', [
} }
} }
}) })
.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', { .state('endpointInit', {
url: '/init/endpoint', url: '/init/endpoint',
views: { views: {
@ -457,6 +473,32 @@ angular.module('portainer', [
} }
} }
}) })
.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', { .state('swarm', {
url: '/swarm/', url: '/swarm/',
views: { views: {
@ -488,7 +530,8 @@ angular.module('portainer', [
}; };
}); });
}]) }])
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'Messages', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, Messages, Analytics) { .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) { StateManager.initialize().then(function success(state) {
if (state.application.authentication) { if (state.application.authentication) {
authManager.checkAuthOnRefresh(); authManager.checkAuthOnRefresh();

View File

@ -1,6 +1,6 @@
angular.module('auth', []) angular.module('auth', [])
.controller('AuthenticationController', ['$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, Messages) { function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, EndpointProvider, Messages) {
$scope.authData = { $scope.authData = {
username: 'admin', username: 'admin',
@ -14,18 +14,34 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
}; };
if (!$scope.applicationState.application.authentication) { if (!$scope.applicationState.application.authentication) {
EndpointService.getActive().then(function success(data) { 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) StateManager.updateEndpointState(true)
.then(function success() { .then(function success() {
$state.go('dashboard'); $state.go('dashboard');
}, function error(err) { }, function error(err) {
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint'); Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
}); });
}, function error(err) { }
if (err.status === 404) { else {
$state.go('endpointInit'); $state.go('endpointInit');
}
}, function error(err) {
Messages.error("Failure", err, 'Unable to retrieve endpoints');
});
} else { } else {
Messages.error("Failure", err, 'Unable to verify Docker endpoint existence'); Users.checkAdminUser({}, function () {},
function (e) {
if (e.status === 404) {
$scope.initPassword = true;
} else {
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; $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() { $scope.createAdminUser = function() {
var password = $sanitize($scope.initPasswordData.password); var password = $sanitize($scope.initPasswordData.password);
Users.initAdminUser({password: password}, function (d) { Users.initAdminUser({password: password}, function (d) {
@ -75,23 +82,33 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
$scope.authenticationError = false; $scope.authenticationError = false;
var username = $sanitize($scope.authData.username); var username = $sanitize($scope.authData.username);
var password = $sanitize($scope.authData.password); var password = $sanitize($scope.authData.password);
Authentication.login(username, password).then(function success() { Authentication.login(username, password)
EndpointService.getActive().then(function success(data) { .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) StateManager.updateEndpointState(true)
.then(function success() { .then(function success() {
$state.go('dashboard'); $state.go('dashboard');
}, function error(err) { }, function error(err) {
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint'); 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');
} }
}); else if (data.length === 0 && userDetails.role === 1) {
}, function error() { $state.go('endpointInit');
$scope.authData.error = 'Invalid credentials'; } 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';
}); });
}; };
}]); }]);

View File

@ -1,6 +1,6 @@
angular.module('containerConsole', []) angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$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, Messages) { function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, EndpointProvider, Messages) {
$scope.state = {}; $scope.state = {};
$scope.state.loaded = false; $scope.state.loaded = false;
$scope.state.connected = false; $scope.state.connected = false;
@ -55,7 +55,7 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
} else { } else {
var execId = d.Id; var execId = d.Id;
resizeTTY(execId, termHeight, termWidth); 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) { if (url.indexOf('https') > -1) {
url = url.replace('https://', 'wss://'); url = url.replace('https://', 'wss://');
} else { } else {

View File

@ -90,6 +90,13 @@
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th ng-if="applicationState.application.authentication">
<a ui-sref="containers" ng-click="order('Metadata.ResourceControl.OwnerId')">
Ownership
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -107,12 +114,43 @@
</a> </a>
<span ng-if="container.Ports.length == 0" >-</span> <span ng-if="container.Ports.length == 0" >-</span>
</td> </td>
<td ng-if="applicationState.application.authentication">
<span ng-if="!container.Metadata.ResourceControl">
<i class="fa fa-eye" aria-hidden="true"></i>
<span ng-if="container.Labels['com.docker.swarm.service.id']">
Public service
</span>
<span ng-if="!container.Labels['com.docker.swarm.service.id']">
Public
</span>
</span>
<span ng-if="container.Metadata.ResourceControl.OwnerId === user.ID">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
<span ng-if="container.Labels['com.docker.swarm.service.id']">
Private service
</span>
<span ng-if="!container.Labels['com.docker.swarm.service.id']">
Private
<a ng-click="switchOwnership(container)" class="interactive"><i class="fa fa-eye" aria-hidden="true" style="margin-left: 7px;"></i> Switch to public</a>
</span>
</span>
<span ng-if="container.Metadata.ResourceControl && container.Metadata.ResourceControl.OwnerId !== user.ID">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
<span ng-if="container.Labels['com.docker.swarm.service.id']">
Private service <span ng-if="container.Owner">(owner: {{ container.Owner }})</span>
</span>
<span ng-if="!container.Labels['com.docker.swarm.service.id']">
Private <span ng-if="container.Owner">(owner: {{ container.Owner }})</span>
<a ng-click="switchOwnership(container)" class="interactive"><i class="fa fa-eye" aria-hidden="true" style="margin-left: 7px;"></i> Switch to public</a>
</span>
</span>
</td>
</tr> </tr>
<tr ng-if="!containers"> <tr ng-if="!containers">
<td colspan="8" class="text-center text-muted">Loading...</td> <td colspan="9" class="text-center text-muted">Loading...</td>
</tr> </tr>
<tr ng-if="containers.length == 0"> <tr ng-if="containers.length == 0">
<td colspan="8" class="text-center text-muted">No containers available.</td> <td colspan="9" class="text-center text-muted">No containers available.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,6 +1,6 @@
angular.module('containers', []) angular.module('containers', [])
.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination', 'EntityListService', .controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination', 'EntityListService', 'ModalService', 'Authentication', 'ResourceControlService', 'UserService',
function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination, EntityListService) { function ($q, $scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination, EntityListService, ModalService, Authentication, ResourceControlService, UserService) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers'); $scope.state.pagination_count = Pagination.getPaginationCount('containers');
$scope.state.displayAll = Settings.displayAll; $scope.state.displayAll = Settings.displayAll;
@ -17,8 +17,51 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
Pagination.setPaginationCount('containers', $scope.state.pagination_count); 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) { var update = function (data) {
$('#loadContainersSpinner').show(); $('#loadContainersSpinner').show();
var userDetails = Authentication.getUserDetails();
$scope.user = userDetails;
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
Container.query(data, function (d) { Container.query(data, function (d) {
var containers = d; var containers = d;
@ -41,7 +84,20 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
} }
return model; return model;
}); });
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(); $('#loadContainersSpinner').hide();
});
} else {
$('#loadContainersSpinner').hide();
}
}, function (e) { }, function (e) {
$('#loadContainersSpinner').hide(); $('#loadContainersSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve containers"); Messages.error("Failure", e, "Unable to retrieve containers");
@ -77,7 +133,17 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
Messages.send("Error", d.message); Messages.send("Error", d.message);
} }
else { else {
if (c.Metadata && c.Metadata.ResourceControl) {
ResourceControlService.removeContainerResourceControl(c.Metadata.ResourceControl.OwnerId, c.Id)
.then(function success() {
Messages.send("Container " + msg, c.Id); 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(); complete();
}, function (e) { }, function (e) {

View File

@ -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('createContainer', []) angular.module('createContainer', [])
.controller('CreateContainerController', ['$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, Messages) { function ($scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Messages) {
$scope.formValues = { $scope.formValues = {
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
alwaysPull: true, alwaysPull: true,
Console: 'none', Console: 'none',
Volumes: [], Volumes: [],
@ -116,20 +119,14 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
}); });
}); });
// TODO: centralize, already present in templatesController function startContainer(containerID) {
function createContainer(config) { Container.start({id: containerID}, {}, function (cd) {
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 (cd.message) {
$('#createContainerSpinner').hide(); $('#createContainerSpinner').hide();
Messages.error('Error', {}, cd.message); Messages.error('Error', {}, cd.message);
} else { } else {
$('#createContainerSpinner').hide(); $('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id); Messages.send('Container Started', containerID);
$state.go('containers', {}, {reload: true}); $state.go('containers', {}, {reload: true});
} }
}, function (e) { }, function (e) {
@ -137,13 +134,32 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
Messages.error("Failure", e, 'Unable to start container'); 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 {
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("Failure", err, 'Unable to apply resource control on container');
});
} else {
startContainer(d.Id);
}
}
}, function (e) { }, function (e) {
$('#createContainerSpinner').hide(); $('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to create container'); Messages.error("Failure", e, 'Unable to create container');
}); });
} }
// TODO: centralize, already present in templatesController
function pullImageAndCreateContainer(config) { function pullImageAndCreateContainer(config) {
Image.create($scope.imageConfig, function (data) { Image.create($scope.imageConfig, function (data) {
createContainer(config); createContainer(config);

View File

@ -100,6 +100,26 @@
<!-- !port-mapping-input-list --> <!-- !port-mapping-input-list -->
</div> </div>
<!-- !port-mapping --> <!-- !port-mapping -->
<!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Ownership
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
Private
</label>
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i>
Public
</label>
</div>
</div>
</div>
<!-- !ownership -->
</form> </form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>

View File

@ -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', []) angular.module('createService', [])
.controller('CreateServiceController', ['$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, Messages) { function ($scope, $state, Service, Volume, Network, ImageHelper, Authentication, ResourceControlService, Messages) {
$scope.formValues = { $scope.formValues = {
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
Name: '', Name: '',
Image: '', Image: '',
Registry: '', Registry: '',
@ -205,9 +208,22 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
function createNewService(config) { function createNewService(config) {
Service.create(config, function (d) { Service.create(config, function (d) {
if ($scope.formValues.Ownership === 'private') {
ResourceControlService.setServiceResourceControl(Authentication.getUserDetails().ID, d.ID)
.then(function success() {
$('#createServiceSpinner').hide(); $('#createServiceSpinner').hide();
Messages.send('Service created', d.ID); Messages.send('Service created', d.ID);
$state.go('services', {}, {reload: true}); $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) { }, function (e) {
$('#createServiceSpinner').hide(); $('#createServiceSpinner').hide();
Messages.error("Failure", e, 'Unable to create service'); Messages.error("Failure", e, 'Unable to create service');

View File

@ -87,6 +87,26 @@
<!-- !port-mapping-input-list --> <!-- !port-mapping-input-list -->
</div> </div>
<!-- !port-mapping --> <!-- !port-mapping -->
<!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Ownership
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
Private
</label>
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i>
Public
</label>
</div>
</div>
</div>
<!-- !ownership -->
</form> </form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>

View File

@ -1,8 +1,9 @@
angular.module('createVolume', []) angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages', .controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'ResourceControlService', 'Authentication', 'Messages',
function ($scope, $state, Volume, Messages) { function ($scope, $state, Volume, ResourceControlService, Authentication, Messages) {
$scope.formValues = { $scope.formValues = {
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
DriverOptions: [] DriverOptions: []
}; };
@ -25,9 +26,22 @@ function ($scope, $state, Volume, Messages) {
$('#createVolumeSpinner').hide(); $('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', {}, d.message); Messages.error('Unable to create volume', {}, d.message);
} else { } else {
if ($scope.formValues.Ownership === 'private') {
ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, d.Name)
.then(function success() {
Messages.send("Volume created", d.Name); Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide(); $('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true}); $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) { }, function (e) {
$('#createVolumeSpinner').hide(); $('#createVolumeSpinner').hide();

View File

@ -55,6 +55,26 @@
<!-- !driver-options-input-list --> <!-- !driver-options-input-list -->
</div> </div>
<!-- !driver-options --> <!-- !driver-options -->
<!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Ownership
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
Private
</label>
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i>
Public
</label>
</div>
</div>
</div>
<!-- !ownership -->
</form> </form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>

View File

@ -18,14 +18,18 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
$scope.updateEndpoint = function() { $scope.updateEndpoint = function() {
var ID = $scope.endpoint.Id; var ID = $scope.endpoint.Id;
var name = $scope.endpoint.Name; var endpointParams = {
var URL = $scope.endpoint.URL; name: $scope.endpoint.Name,
var TLS = $scope.endpoint.TLS; URL: $scope.endpoint.URL,
var TLSCACert = $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null; TLS: $scope.endpoint.TLS,
var TLSCert = $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null; TLSCACert: $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null,
var TLSKey = $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null; TLSCert: $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null,
var type = $scope.endpointType; TLSKey: $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null,
EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey, type).then(function success(data) { type: $scope.endpointType
};
EndpointService.updateEndpoint(ID, endpointParams)
.then(function success(data) {
Messages.send("Endpoint updated", $scope.endpoint.Name); Messages.send("Endpoint updated", $scope.endpoint.Name);
$state.go('endpoints'); $state.go('endpoints');
}, function error(err) { }, function error(err) {

View File

@ -0,0 +1,177 @@
<rd-header>
<rd-header-title title="Endpoint access">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a> > Access management
</rd-header-content>
</rd-header>
<div class="row" ng-if="endpoint">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title="Endpoint"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
{{ endpoint.Name }}
</td>
</tr>
<tr>
<td>URL</td>
<td>
{{ endpoint.URL | stripprotocol }}
</td>
</tr>
<tr>
<td colspan="2">
<span class="small text-muted">
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.
</span>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="endpoint">
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="state.pagination_count_users" ng-change="changePaginationCountUsers()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="authorizeAllUsers()" ng-disabled="users.length === 0 || filteredUsers.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all users</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="state.filterUsers" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderUsers('Username')">
Name
<span ng-show="sortTypeUsers == 'Username' && !sortReverseUsers" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeUsers == 'Username' && sortReverseUsers" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderUsers('Role')">
Role
<span ng-show="sortTypeUsers == 'Role' && !sortReverseUsers" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeUsers == 'Role' && sortReverseUsers" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="authorizeUser(user)" class="interactive" dir-paginate="user in (state.filteredUsers = (users | filter:state.filterUsers | orderBy:sortTypeUsers:sortReverseUsers | itemsPerPage: state.pagination_count_users))">
<td>{{ user.Username }}</td>
<td>
{{ user.RoleName }}
<i class="fa" ng-class="user.RoleId === 1 ? 'fa-user-circle-o' : 'fa-user'" aria-hidden="true" style="margin-left: 2px;"></i>
</td>
</tr>
<tr ng-if="!users">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="users.length === 0 || state.filteredUsers.length === 0">
<td colspan="2" class="text-center text-muted">No users.</td>
</tr>
</tbody>
</table>
<div ng-if="users" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="state.pagination_count_authorizedUsers" ng-change="changePaginationCountAuthorizedUsers()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="unauthorizeAllUsers()" ng-disabled="authorizedUsers.length === 0 || filteredAuthorizedUsers.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all users</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="state.filterAuthorizedUsers" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAuthorizedUsers('Username')">
Name
<span ng-show="sortTypeAuthorizedUsers == 'Username' && !sortReverseAuthorizedUsers" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeAuthorizedUsers == 'Username' && sortReverseAuthorizedUsers" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAuthorizedUsers('Role')">
Role
<span ng-show="sortTypeAuthorizedUsers == 'Role' && !sortReverseAuthorizedUsers" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeAuthorizedUsers == 'Role' && sortReverseAuthorizedUsers" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="unauthorizeUser(user)" class="interactive" dir-paginate="user in (state.filteredAuthorizedUsers = (authorizedUsers | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedUsers:sortReverseAuthorizedUsers | itemsPerPage: state.pagination_count_authorizedUsers))">
<td>{{ user.Username }}</td>
<td>
{{ user.RoleName }}
<i class="fa" ng-class="user.RoleId === 1 ? 'fa-user-circle-o' : 'fa-user'" aria-hidden="true" style="margin-left: 2px;"></i>
</td>
</tr>
<tr ng-if="!authorizedUsers">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="authorizedUsers.length === 0 || state.filteredAuthorizedUsers.length === 0">
<td colspan="2" class="text-center text-muted">No authorized users.</td>
</tr>
</tbody>
</table>
<div ng-if="authorizedUsers" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

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

View File

@ -1,6 +1,6 @@
angular.module('endpointInit', []) angular.module('endpointInit', [])
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Messages', .controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Messages',
function ($scope, $state, EndpointService, StateManager, Messages) { function ($scope, $state, EndpointService, StateManager, EndpointProvider, Messages) {
$scope.state = { $scope.state = {
error: '', error: '',
uploadInProgress: false uploadInProgress: false
@ -29,20 +29,28 @@ function ($scope, $state, EndpointService, StateManager, Messages) {
var name = "local"; var name = "local";
var URL = "unix:///var/run/docker.sock"; var URL = "unix:///var/run/docker.sock";
var TLS = false; var TLS = false;
EndpointService.createLocalEndpoint(name, URL, TLS, true).then(function success(data) {
StateManager.updateEndpointState(false) EndpointService.createLocalEndpoint(name, URL, TLS, true)
.then(function success() { .then(
function success(data) {
var endpointID = data.Id;
EndpointProvider.setEndpointID(endpointID);
StateManager.updateEndpointState(false).then(
function success() {
$state.go('dashboard'); $state.go('dashboard');
}, function error(err) { },
EndpointService.deleteEndpoint(0) function error(err) {
EndpointService.deleteEndpoint(endpointID)
.then(function success() { .then(function success() {
$('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to connect to the Docker endpoint'; $scope.state.error = 'Unable to connect to the Docker endpoint';
}); });
}); });
}, function error(err) { },
$('#initEndpointSpinner').hide(); function error() {
$scope.state.error = 'Unable to create endpoint'; $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; var TLSKeyFile = $scope.formValues.TLSKey;
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true) EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true)
.then(function success(data) { .then(function success(data) {
var endpointID = data.Id;
EndpointProvider.setEndpointID(endpointID);
StateManager.updateEndpointState(false) StateManager.updateEndpointState(false)
.then(function success() { .then(function success() {
$state.go('dashboard'); $state.go('dashboard');
}, function error(err) { }, function error(err) {
EndpointService.deleteEndpoint(0) EndpointService.deleteEndpoint(endpointID)
.then(function success() { .then(function success() {
$('#initEndpointSpinner').hide(); $('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to connect to the Docker endpoint'; $scope.state.error = 'Unable to connect to the Docker endpoint';

View File

@ -14,7 +14,7 @@
<rd-widget-header icon="fa-exclamation-triangle" title="Endpoint management is not available"> <rd-widget-header icon="fa-exclamation-triangle" title="Endpoint management is not available">
</rd-widget-header> </rd-widget-header>
<rd-widget-body> <rd-widget-body>
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag. Endpoint management via the UI is disabled.</span> <span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag. Endpoint management via the UI is disabled. You can still manage endpoint access.</span>
</rd-wigdet-body> </rd-wigdet-body>
</rd-widget> </rd-widget>
</div> </div>
@ -152,14 +152,7 @@
<span ng-show="sortType == 'URL' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'URL' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th> <th></th>
<a ui-sref="endpoints" ng-click="order('TLS')">
TLS
<span ng-show="sortType == 'TLS' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'TLS' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.endpointManagement"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -167,14 +160,18 @@
<td ng-if="applicationState.application.endpointManagement"><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td> <td ng-if="applicationState.application.endpointManagement"><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
<td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td> <td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td>
<td>{{ endpoint.URL | stripprotocol }}</td> <td>{{ endpoint.URL | stripprotocol }}</td>
<td><i class="fa fa-shield" aria-hidden="true" ng-if="endpoint.TLS"></i></td> <td>
<td ng-if="applicationState.application.endpointManagement"> <span ng-if="applicationState.application.endpointManagement">
<span ng-if="endpoint.Id !== activeEndpoint.Id"> <span ng-if="endpoint.Id !== activeEndpointID">
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a> <a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
</span> </span>
<span class="small text-muted" ng-if="endpoint.Id === activeEndpoint.Id"> <span class="small text-muted" ng-if="endpoint.Id === activeEndpointID">
<i class="fa fa-lock" aria-hidden="true"></i> You cannot edit the active endpoint <i class="fa fa-lock" aria-hidden="true"></i> You cannot edit the active endpoint
</span> </span>
</span>
<span ng-if="applicationState.application.authentication">
<a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
</span>
</td> </td>
</tr> </tr>
<tr ng-if="!endpoints"> <tr ng-if="!endpoints">

View File

@ -1,6 +1,6 @@
angular.module('endpoints', []) angular.module('endpoints', [])
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Messages', 'Pagination', .controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Messages', 'Pagination',
function ($scope, $state, EndpointService, Messages, Pagination) { function ($scope, $state, EndpointService, EndpointProvider, Messages, Pagination) {
$scope.state = { $scope.state = {
error: '', error: '',
uploadInProgress: false, uploadInProgress: false,
@ -84,19 +84,17 @@ function ($scope, $state, EndpointService, Messages, Pagination) {
function fetchEndpoints() { function fetchEndpoints() {
$('#loadEndpointsSpinner').show(); $('#loadEndpointsSpinner').show();
EndpointService.endpoints().then(function success(data) { EndpointService.endpoints()
.then(function success(data) {
$scope.endpoints = data; $scope.endpoints = data;
EndpointService.getActive().then(function success(data) { $scope.activeEndpointID = EndpointProvider.endpointID();
$scope.activeEndpoint = data; })
$('#loadEndpointsSpinner').hide(); .catch(function error(err) {
}, function error(err) {
$('#loadEndpointsSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve active endpoint");
});
}, function error(err) {
$('#loadEndpointsSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve endpoints"); Messages.error("Failure", err, "Unable to retrieve endpoints");
$scope.endpoints = []; $scope.endpoints = [];
})
.finally(function final() {
$('#loadEndpointsSpinner').hide();
}); });
} }

View File

@ -60,21 +60,9 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, Moda
}; };
$scope.confirmRemovalAction = function (force) { $scope.confirmRemovalAction = function (force) {
ModalService.confirm({ ModalService.confirmImageForceRemoval(function (confirmed) {
title: "Are you sure?",
message: "Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.",
buttons: {
confirm: {
label: 'Remove the image',
},
cancel: {
label: 'Cancel'
}
},
callback: function (confirmed) {
if(!confirmed) { return; } if(!confirmed) { return; }
$scope.removeAction(force); $scope.removeAction(force);
}
}); });
}; };

View File

@ -58,6 +58,13 @@
<span ng-show="sortType == 'Mode' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Mode' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th ng-if="applicationState.application.authentication">
<a ui-sref="containers" ng-click="order('Metadata.ResourceControl.OwnerId')">
Ownership
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
@ -76,12 +83,33 @@
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a> <a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
</span> </span>
</td> </td>
<td ng-if="applicationState.application.authentication">
<span ng-if="user.role === 1 && service.Metadata.ResourceControl">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
<span ng-if="service.Metadata.ResourceControl.OwnerId === user.ID">
Private
</span>
<span ng-if="service.Metadata.ResourceControl.OwnerId !== user.ID">
Private <span ng-if="service.Owner">(owner: {{ service.Owner }})</span>
</span>
<a ng-click="switchOwnership(service)" class="interactive"><i class="fa fa-eye" aria-hidden="true" style="margin-left: 7px;"></i> Switch to public</a>
</span>
<span ng-if="user.role !== 1 && service.Metadata.ResourceControl.OwnerId === user.ID">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
Private
<a ng-click="switchOwnership(service)" class="interactive"><i class="fa fa-eye" aria-hidden="true" style="margin-left: 7px;"></i> Switch to public</a>
</span>
<span ng-if="!service.Metadata.ResourceControl">
<i class="fa fa-eye" aria-hidden="true"></i>
Public
</span>
</td>
</tr> </tr>
<tr ng-if="!services"> <tr ng-if="!services">
<td colspan="4" class="text-center text-muted">Loading...</td> <td colspan="5" class="text-center text-muted">Loading...</td>
</tr> </tr>
<tr ng-if="services.length == 0"> <tr ng-if="services.length == 0">
<td colspan="4" class="text-center text-muted">No services available.</td> <td colspan="5" class="text-center text-muted">No services available.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,12 +1,40 @@
angular.module('services', []) angular.module('services', [])
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination', .controller('ServicesController', ['$q', '$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination', 'Authentication', 'UserService', 'ModalService', 'ResourceControlService',
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination) { function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination, Authentication, UserService, ModalService, ResourceControlService) {
$scope.state = {}; $scope.state = {};
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('services'); $scope.state.pagination_count = Pagination.getPaginationCount('services');
$scope.sortType = 'Name'; $scope.sortType = 'Name';
$scope.sortReverse = false; $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() { $scope.changePaginationCount = function() {
Pagination.setPaginationCount('services', $scope.state.pagination_count); Pagination.setPaginationCount('services', $scope.state.pagination_count);
}; };
@ -57,9 +85,21 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagina
$('#loadServicesSpinner').hide(); $('#loadServicesSpinner').hide();
Messages.error("Unable to remove service", {}, d[0].message); Messages.error("Unable to remove service", {}, d[0].message);
} else { } else {
if (service.Metadata && service.Metadata.ResourceControl) {
ResourceControlService.removeServiceResourceControl(service.Metadata.ResourceControl.OwnerId, service.Id)
.then(function success() {
Messages.send("Service deleted", service.Id); Messages.send("Service deleted", service.Id);
var index = $scope.services.indexOf(service); var index = $scope.services.indexOf(service);
$scope.services.splice(index, 1); $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(); complete();
}, function (e) { }, function (e) {
@ -70,12 +110,42 @@ 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() { function fetchServices() {
$('#loadServicesSpinner').show(); $('#loadServicesSpinner').show();
var userDetails = Authentication.getUserDetails();
$scope.user = userDetails;
Service.query({}, function (d) { Service.query({}, function (d) {
$scope.services = d.map(function (service) { $scope.services = d.map(function (service) {
return new ServiceViewModel(service); return new ServiceViewModel(service);
}); });
if (userDetails.role === 1) {
UserService.users()
.then(function success(data) {
mapUsersToServices(data);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to retrieve users");
})
.finally(function final() {
$('#loadServicesSpinner').hide();
});
}
$('#loadServicesSpinner').hide(); $('#loadServicesSpinner').hide();
}, function(e) { }, function(e) {
$('#loadServicesSpinner').hide(); $('#loadServicesSpinner').hide();

View File

@ -1,6 +1,6 @@
angular.module('settings', []) angular.module('settings', [])
.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Users', 'Messages', .controller('SettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Messages',
function ($scope, $state, $sanitize, Users, Messages) { function ($scope, $state, $sanitize, Authentication, UserService, Messages) {
$scope.formValues = { $scope.formValues = {
currentPassword: '', currentPassword: '',
newPassword: '', newPassword: '',
@ -9,22 +9,21 @@ function ($scope, $state, $sanitize, Users, Messages) {
$scope.updatePassword = function() { $scope.updatePassword = function() {
$scope.invalidPassword = false; $scope.invalidPassword = false;
$scope.error = false; var userID = Authentication.getUserDetails().ID;
var currentPassword = $sanitize($scope.formValues.currentPassword); var currentPassword = $sanitize($scope.formValues.currentPassword);
Users.checkPassword({ username: $scope.username, password: currentPassword }, function (d) {
if (d.valid) {
var newPassword = $sanitize($scope.formValues.newPassword); var newPassword = $sanitize($scope.formValues.newPassword);
Users.update({ username: $scope.username, password: newPassword }, function (d) {
UserService.updateUserPassword(userID, currentPassword, newPassword)
.then(function success() {
Messages.send("Success", "Password successfully updated"); Messages.send("Success", "Password successfully updated");
$state.reload(); $state.reload();
}, function (e) { })
Messages.error("Failure", e, "Unable to update password"); .catch(function error(err) {
}); if (err.invalidPassword) {
} else {
$scope.invalidPassword = true; $scope.invalidPassword = true;
} else {
Messages.error("Failure", err, err.msg);
} }
}, function (e) {
Messages.error("Failure", e, "Unable to check password validity");
}); });
}; };
}]); }]);

View File

@ -50,7 +50,10 @@
<li class="sidebar-list" ng-if="applicationState.application.authentication"> <li class="sidebar-list" ng-if="applicationState.application.authentication">
<a ui-sref="settings" ui-sref-active="active">Password <span class="menu-icon fa fa-lock"></span></a> <a ui-sref="settings" ui-sref-active="active">Password <span class="menu-icon fa fa-lock"></span></a>
</li> </li>
<li class="sidebar-list"> <li class="sidebar-list" ng-if="applicationState.application.authentication && userRole === 1">
<a ui-sref="users" ui-sref-active="active">Users <span class="menu-icon fa fa-user"></span></a>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || userRole === 1">
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a> <a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a>
</li> </li>
</ul> </ul>

View File

@ -1,40 +1,41 @@
angular.module('sidebar', []) angular.module('sidebar', [])
.controller('SidebarController', ['$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, Messages) { function ($scope, $state, Settings, Config, EndpointService, StateManager, EndpointProvider, Messages, Authentication) {
Config.$promise.then(function (c) { Config.$promise.then(function (c) {
$scope.logo = c.logo; $scope.logo = c.logo;
}); });
$scope.uiVersion = Settings.uiVersion; $scope.uiVersion = Settings.uiVersion;
$scope.userRole = Authentication.getUserDetails().role;
$scope.switchEndpoint = function(endpoint) { $scope.switchEndpoint = function(endpoint) {
EndpointService.setActive(endpoint.Id) var activeEndpointID = EndpointProvider.endpointID();
.then(function success(data) { EndpointProvider.setEndpointID(endpoint.Id);
StateManager.updateEndpointState(true) StateManager.updateEndpointState(true)
.then(function success() { .then(function success() {
$state.go('dashboard'); $state.go('dashboard');
}, function error(err) { })
.catch(function error(err) {
Messages.error("Failure", err, "Unable to connect to the Docker endpoint"); Messages.error("Failure", err, "Unable to connect to the Docker endpoint");
}); EndpointProvider.setEndpointID(activeEndpointID);
}, function error(err) { StateManager.updateEndpointState(true)
Messages.error("Failure", err, "Unable to switch to new endpoint"); .then(function success() {});
}); });
}; };
function fetchEndpoints() { function fetchEndpoints() {
EndpointService.endpoints().then(function success(data) { EndpointService.endpoints()
.then(function success(data) {
$scope.endpoints = data; $scope.endpoints = data;
EndpointService.getActive().then(function success(data) { var activeEndpointID = EndpointProvider.endpointID();
angular.forEach($scope.endpoints, function (endpoint) { angular.forEach($scope.endpoints, function (endpoint) {
if (endpoint.Id === data.Id) { if (endpoint.Id === activeEndpointID) {
$scope.activeEndpoint = endpoint; $scope.activeEndpoint = endpoint;
} }
}); });
}, function error(err) { })
Messages.error("Failure", err, "Unable to retrieve active endpoint"); .catch(function error(err) {
});
}, function error(err) {
$scope.endpoints = []; $scope.endpoints = [];
}); });
} }

View File

@ -39,6 +39,7 @@
</div> </div>
</div> </div>
<!-- !name-and-network-inputs --> <!-- !name-and-network-inputs -->
<!-- env -->
<div ng-repeat="var in state.selectedTemplate.Env" ng-if="!var.set" class="form-group"> <div ng-repeat="var in state.selectedTemplate.Env" ng-if="!var.set" class="form-group">
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label> <label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -51,6 +52,21 @@
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}"> <input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
</div> </div>
</div> </div>
<!-- !env -->
<!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Ownership
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'">Private</label>
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'">Public</label>
</div>
</div>
</div>
<!-- !ownership -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;"> <a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">

View File

@ -1,12 +1,13 @@
angular.module('templates', []) angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', .controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', 'ResourceControlService', 'Authentication',
function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination) { function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination, ResourceControlService, Authentication) {
$scope.state = { $scope.state = {
selectedTemplate: null, selectedTemplate: null,
showAdvancedOptions: false, showAdvancedOptions: false,
pagination_count: Pagination.getPaginationCount('templates') pagination_count: Pagination.getPaginationCount('templates')
}; };
$scope.formValues = { $scope.formValues = {
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
network: "", network: "",
name: "", name: "",
}; };
@ -36,18 +37,30 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container
var template = $scope.state.selectedTemplate; var template = $scope.state.selectedTemplate;
var templateConfiguration = createTemplateConfiguration(template); var templateConfiguration = createTemplateConfiguration(template);
var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes); var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes);
VolumeService.createXAutoGeneratedLocalVolumes(generatedVolumeCount) VolumeService.createXAutoGeneratedLocalVolumes(generatedVolumeCount)
.then(function success(data) { .then(function success(data) {
var volumeResourceControlQueries = [];
if ($scope.formValues.Ownership === 'private') {
angular.forEach(data, function (volume) {
volumeResourceControlQueries.push(ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, volume.Name));
});
}
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration.container, template, data); TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration.container, template, data);
return ImageService.pullImage(templateConfiguration.image); return $q.all(volumeResourceControlQueries).then(ImageService.pullImage(templateConfiguration.image));
}) })
.then(function success(data) { .then(function success(data) {
return ContainerService.createAndStartContainer(templateConfiguration.container); return ContainerService.createAndStartContainer(templateConfiguration.container);
}) })
.then(function success(data) { .then(function success(data) {
Messages.send('Container Started', data.Id); Messages.send('Container Started', data.Id);
if ($scope.formValues.Ownership === 'private') {
ResourceControlService.setContainerResourceControl(Authentication.getUserDetails().ID, data.Id)
.then(function success(data) {
$state.go('containers', {}, {reload: true}); $state.go('containers', {}, {reload: true});
});
} else {
$state.go('containers', {}, {reload: true});
}
}) })
.catch(function error(err) { .catch(function error(err) {
Messages.error('Failure', err, err.msg); Messages.error('Failure', err, err.msg);

View File

@ -0,0 +1,88 @@
<rd-header>
<rd-header-title title="User details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="users">Users</a> > <a ui-sref="user({id: user.Id})">{{ user.Username }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-user" title="User details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
{{ user.Username }}
<button class="btn btn-xs btn-danger" ng-click="deleteUser()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this user</button>
</td>
</tr>
<td>Permissions</td>
<td>
<div class="btn-group btn-group-sm">
<label class="btn btn-default" ng-model="user.RoleId" uib-btn-radio="2" ng-change="updatePermissions()">
<i class="fa fa-user" aria-hidden="true"></i>
User
</label>
<label class="btn btn-default" ng-model="user.RoleId" uib-btn-radio="1" ng-change="updatePermissions()">
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
Administrator
</label>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-lock" title="Change user password"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" style="margin-top: 15px;">
<!-- new-password-input -->
<div class="form-group">
<label for="new_password" class="col-sm-2 control-label text-left">New password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.newPassword" id="new_password">
</div>
</div>
</div>
<!-- !new-password-input -->
<!-- confirm-password-input -->
<div class="form-group">
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.confirmPassword" id="confirm_password">
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword !== '' && formValues.newPassword === formValues.confirmPassword]" aria-hidden="true"></i></span>
</div>
</div>
</div>
<!-- !confirm-password-input -->
<div class="form-group">
<div class="col-sm-2">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="formValues.newPassword === '' || formValues.newPassword !== formValues.confirmPassword" ng-click="updatePassword()">Update password</button>
</div>
<div class="col-sm-10">
<p class="pull-left text-danger" ng-if="state.updatePasswordError" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.updatePasswordError }}
</p>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -0,0 +1,85 @@
angular.module('user', [])
.controller('UserController', ['$scope', '$state', '$stateParams', 'UserService', 'ModalService', 'Messages',
function ($scope, $state, $stateParams, UserService, ModalService, Messages) {
$scope.state = {
updatePasswordError: '',
};
$scope.formValues = {
newPassword: '',
confirmPassword: ''
};
$scope.deleteUser = function() {
ModalService.confirmDeletion(
'Do you want to delete this user? This user will not be able to login into Portainer anymore.',
function onConfirm(confirmed) {
if(!confirmed) { return; }
deleteUser();
}
);
};
$scope.updatePermissions = function() {
$('#loadingViewSpinner').show();
UserService.updateUser($scope.user.Id, undefined, $scope.user.RoleId)
.then(function success(data) {
var newRole = $scope.user.RoleId === 1 ? 'administrator' : 'user';
Messages.send('Permissions successfully updated', $scope.user.Username + ' is now ' + newRole);
$state.reload();
})
.catch(function error(err) {
Messages.error("Failure", err, 'Unable to update user permissions');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
$scope.updatePassword = function() {
$('#loadingViewSpinner').show();
UserService.updateUser($scope.user.Id, $scope.formValues.newPassword, undefined)
.then(function success(data) {
Messages.send('Password successfully updated');
$state.reload();
})
.catch(function error(err) {
$scope.state.updatePasswordError = 'Unable to update password';
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
function deleteUser() {
$('#loadingViewSpinner').show();
UserService.deleteUser($scope.user.Id)
.then(function success(data) {
Messages.send('User successfully deleted', $scope.user.Username);
$state.go('users');
})
.catch(function error(err) {
Messages.error("Failure", err, 'Unable to remove user');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
function getUser() {
$('#loadingViewSpinner').show();
UserService.user($stateParams.id)
.then(function success(data) {
$scope.user = new UserViewModel(data);
})
.catch(function error(err) {
Messages.error("Failure", err, 'Unable to retrieve user information');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
getUser();
}]);

View File

@ -0,0 +1,159 @@
<rd-header>
<rd-header-title title="Users">
<a data-toggle="tooltip" title="Refresh" ui-sref="users" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadUsersSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>User management</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plus" title="Add a new user">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="username" class="col-sm-2 control-label text-left">Username</label>
<div class="col-sm-8">
<div class="input-group">
<input type="text" class="form-control" id="username" ng-model="formValues.Username" ng-change="checkUsernameValidity()" placeholder="e.g. jdoe">
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[state.validUsername]" aria-hidden="true"></i></span>
</div>
</div>
</div>
<!-- !name-input -->
<!-- new-password-input -->
<div class="form-group">
<label for="password" class="col-sm-2 control-label text-left">Password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.Password" id="password">
</div>
</div>
</div>
<!-- !new-password-input -->
<!-- confirm-password-input -->
<div class="form-group">
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.ConfirmPassword" id="confirm_password">
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.Password !== '' && formValues.Password === formValues.ConfirmPassword]" aria-hidden="true"></i></span>
</div>
</div>
</div>
<!-- !confirm-password-input -->
<!-- role-checkbox -->
<div class="form-group">
<label for="permissions" class="col-sm-2 control-label text-left">Permissions</label>
<div class="col-sm-8">
<div class="btn-group btn-group-sm">
<label class="btn btn-default" ng-model="formValues.Role" uib-btn-radio="2">
<i class="fa fa-user" aria-hidden="true"></i>
User
</label>
<label class="btn btn-default" ng-model="formValues.Role" uib-btn-radio="1">
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
Administrator
</label>
</div>
</div>
</div>
<!-- !role-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!state.validUsername || formValues.Username === '' || formValues.Password === '' || formValues.Password !== formValues.ConfirmPassword" ng-click="addUser()"><i class="fa fa-user-plus" aria-hidden="true"></i> Add user</button>
<i id="createUserSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.userCreationError" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.userCreationError }}
</span>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-user" title="Users">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="users" ng-click="order('Username')">
Name
<span ng-show="sortType == 'Username' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Username' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="users" ng-click="order('RoleName')">
Role
<span ng-show="sortType == 'RoleName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'RoleName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr dir-paginate="user in (state.filteredUsers = (users | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="user.Checked" ng-change="selectItem(user)" /></td>
<td>{{ user.Username }}</td>
<td>
{{ user.RoleName }}
<i class="fa" ng-class="user.RoleId === 1 ? 'fa-user-circle-o' : 'fa-user'" aria-hidden="true" style="margin-left: 2px;"></i>
</td>
<td>
<a ui-sref="user({id: user.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
</td>
</tr>
<tr ng-if="!users">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="users.length == 0">
<td colspan="4" class="text-center text-muted">No users available.</td>
</tr>
</tbody>
</table>
<div ng-if="users" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>

View File

@ -0,0 +1,132 @@
angular.module('users', [])
.controller('UsersController', ['$scope', '$state', 'UserService', 'ModalService', 'Messages', 'Pagination',
function ($scope, $state, UserService, ModalService, Messages, Pagination) {
$scope.state = {
userCreationError: '',
selectedItemCount: 0,
validUsername: false,
pagination_count: Pagination.getPaginationCount('users')
};
$scope.sortType = 'RoleName';
$scope.sortReverse = false;
$scope.formValues = {
Username: '',
Password: '',
ConfirmPassword: '',
Role: 2,
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('endpoints', $scope.state.pagination_count);
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredUsers, function (user) {
if (user.Checked !== allSelected) {
user.Checked = allSelected;
$scope.selectItem(user);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.checkUsernameValidity = function() {
var valid = true;
for (var i = 0; i < $scope.users.length; i++) {
if ($scope.formValues.Username === $scope.users[i].Username) {
valid = false;
break;
}
}
$scope.state.validUsername = valid;
$scope.state.userCreationError = valid ? '' : 'Username already taken';
};
$scope.addUser = function() {
$scope.state.userCreationError = '';
var username = $scope.formValues.Username;
var password = $scope.formValues.Password;
var role = $scope.formValues.Role;
UserService.createUser(username, password, role)
.then(function success(data) {
Messages.send("User created", username);
$state.reload();
})
.catch(function error(err) {
$scope.state.userCreationError = err.msg;
})
.finally(function final() {
});
};
function deleteSelectedUsers() {
$('#loadUsersSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadUsersSpinner').hide();
}
};
angular.forEach($scope.users, function (user) {
if (user.Checked) {
counter = counter + 1;
UserService.deleteUser(user.Id)
.then(function success(data) {
var index = $scope.users.indexOf(user);
$scope.users.splice(index, 1);
Messages.send('User successfully deleted', user.Username);
})
.catch(function error(err) {
Messages.error("Failure", err, 'Unable to remove user');
})
.finally(function final() {
complete();
});
}
});
}
$scope.removeAction = function () {
ModalService.confirmDeletion(
'Do you want to delete the selected users? They will not be able to login into Portainer anymore.',
function onConfirm(confirmed) {
if(!confirmed) { return; }
deleteSelectedUsers();
}
);
};
function fetchUsers() {
$('#loadUsersSpinner').show();
UserService.users()
.then(function success(data) {
$scope.users = data.map(function(user) {
return new UserViewModel(user);
});
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to retrieve users");
$scope.users = [];
})
.finally(function final() {
$('#loadUsersSpinner').hide();
});
}
fetchUsers();
}]);

View File

@ -60,6 +60,13 @@
<span ng-show="sortType == 'Mountpoint' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Mountpoint' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th ng-if="applicationState.application.authentication">
<a ui-sref="volumes" ng-click="order('Metadata.ResourceControl.OwnerId')">
Ownership
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -68,12 +75,33 @@
<td>{{ volume.Name|truncate:50 }}</td> <td>{{ volume.Name|truncate:50 }}</td>
<td>{{ volume.Driver }}</td> <td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint }}</td> <td>{{ volume.Mountpoint }}</td>
<td ng-if="applicationState.application.authentication">
<span ng-if="user.role === 1 && volume.Metadata.ResourceControl">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
<span ng-if="volume.Metadata.ResourceControl.OwnerId === user.ID">
Private
</span>
<span ng-if="volume.Metadata.ResourceControl.OwnerId !== user.ID">
Private <span ng-if="volume.Owner">(owner: {{ volume.Owner }})</span>
</span>
<a ng-click="switchOwnership(volume)" class="interactive"><i class="fa fa-eye" aria-hidden="true" style="margin-left: 7px;"></i> Switch to public</a>
</span>
<span ng-if="user.role !== 1 && volume.Metadata.ResourceControl.OwnerId === user.ID">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
Private
<a ng-click="switchOwnership(volume)" class="interactive"><i class="fa fa-eye" aria-hidden="true" style="margin-left: 7px;"></i> Switch to public</a>
</span>
<span ng-if="!volume.Metadata.ResourceControl">
<i class="fa fa-eye" aria-hidden="true"></i>
Public
</span>
</td>
</tr> </tr>
<tr ng-if="!volumes"> <tr ng-if="!volumes">
<td colspan="4" class="text-center text-muted">Loading...</td> <td colspan="6" class="text-center text-muted">Loading...</td>
</tr> </tr>
<tr ng-if="volumes.length == 0"> <tr ng-if="volumes.length == 0">
<td colspan="4" class="text-center text-muted">No volumes available.</td> <td colspan="6" class="text-center text-muted">No volumes available.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,6 +1,6 @@
angular.module('volumes', []) angular.module('volumes', [])
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Pagination', .controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Pagination', 'ModalService', 'Authentication', 'ResourceControlService', 'UserService',
function ($scope, $state, Volume, Messages, Pagination) { function ($scope, $state, Volume, Messages, Pagination, ModalService, Authentication, ResourceControlService, UserService) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('volumes'); $scope.state.pagination_count = Pagination.getPaginationCount('volumes');
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
@ -10,6 +10,24 @@ function ($scope, $state, Volume, Messages, Pagination) {
Name: '' Name: ''
}; };
function removeVolumeResourceControl(volume) {
ResourceControlService.removeVolumeResourceControl(volume.Metadata.ResourceControl.OwnerId, volume.Name)
.then(function success() {
delete volume.Metadata.ResourceControl;
Messages.send('Ownership changed to public', volume.Name);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to change volume ownership");
});
}
$scope.switchOwnership = function(volume) {
ModalService.confirmVolumeOwnershipChange(function (confirmed) {
if(!confirmed) { return; }
removeVolumeResourceControl(volume);
});
};
$scope.changePaginationCount = function() { $scope.changePaginationCount = function() {
Pagination.setPaginationCount('volumes', $scope.state.pagination_count); Pagination.setPaginationCount('volumes', $scope.state.pagination_count);
}; };
@ -52,9 +70,21 @@ function ($scope, $state, Volume, Messages, Pagination) {
if (d.message) { if (d.message) {
Messages.error("Unable to remove volume", {}, d.message); Messages.error("Unable to remove volume", {}, d.message);
} else { } else {
if (volume.Metadata && volume.Metadata.ResourceControl) {
ResourceControlService.removeVolumeResourceControl(volume.Metadata.ResourceControl.OwnerId, volume.Name)
.then(function success() {
Messages.send("Volume deleted", volume.Name); Messages.send("Volume deleted", volume.Name);
var index = $scope.volumes.indexOf(volume); var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1); $scope.volumes.splice(index, 1);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to remove volume ownership");
});
} else {
Messages.send("Volume deleted", volume.Name);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
}
} }
complete(); complete();
}, function (e) { }, function (e) {
@ -65,11 +95,45 @@ function ($scope, $state, Volume, Messages, Pagination) {
}); });
}; };
function mapUsersToVolumes(users) {
angular.forEach($scope.volumes, function (volume) {
if (volume.Metadata) {
var volumeRC = volume.Metadata.ResourceControl;
if (volumeRC && volumeRC.OwnerId != $scope.user.ID) {
angular.forEach(users, function (user) {
if (volumeRC.OwnerId === user.Id) {
volume.Owner = user.Username;
}
});
}
}
});
}
function fetchVolumes() { function fetchVolumes() {
$('#loadVolumesSpinner').show(); $('#loadVolumesSpinner').show();
var userDetails = Authentication.getUserDetails();
$scope.user = userDetails;
Volume.query({}, function (d) { Volume.query({}, function (d) {
$scope.volumes = d.Volumes || []; var volumes = d.Volumes || [];
$scope.volumes = volumes.map(function (v) {
return new VolumeViewModel(v);
});
if (userDetails.role === 1) {
UserService.users()
.then(function success(data) {
mapUsersToVolumes(data);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to retrieve users");
})
.finally(function final() {
$('#loadVolumesSpinner').hide(); $('#loadVolumesSpinner').hide();
});
} else {
$('#loadVolumesSpinner').hide();
}
}, function (e) { }, function (e) {
$('#loadVolumesSpinner').hide(); $('#loadVolumesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve volumes"); Messages.error("Failure", e, "Unable to retrieve volumes");

View File

@ -5,7 +5,7 @@ angular
requires: '^rdHeader', requires: '^rdHeader',
transclude: true, transclude: true,
link: function (scope, iElement, iAttrs) { link: function (scope, iElement, iAttrs) {
scope.username = Authentication.getCredentials().username; scope.username = Authentication.getUserDetails().username;
}, },
template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u>log out <i class="fa fa-sign-out" aria-hidden="true"></i></u></a></div></div>', template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u>log out <i class="fa fa-sign-out" aria-hidden="true"></i></u></a></div></div>',
restrict: 'E' restrict: 'E'

View File

@ -7,7 +7,7 @@ angular
title: '@' title: '@'
}, },
link: function (scope, iElement, iAttrs) { link: function (scope, iElement, iAttrs) {
scope.username = Authentication.getCredentials().username; scope.username = Authentication.getUserDetails().username;
}, },
transclude: true, transclude: true,
template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box" ng-if="username"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span></div>', template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box" ng-if="username"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span></div>',

13
app/directives/tooltip.js Normal file
View File

@ -0,0 +1,13 @@
angular
.module('portainer')
.directive('portainerTooltip', [function portainerTooltip() {
var directive = {
scope: {
message: '@',
position: '@'
},
template: '<span class="interactive" tooltip-placement="{{position}}" tooltip-class="portainer-tooltip" uib-tooltip="{{message}}"><i class="fa fa-question-circle tooltip-icon" aria-hidden="true"></i></span>',
restrict: 'E'
};
return directive;
}]);

View File

@ -5,10 +5,11 @@ angular
requires: '^rdWidget', requires: '^rdWidget',
scope: { scope: {
title: '@', title: '@',
icon: '@' icon: '@',
classes: '@?'
}, },
transclude: true, transclude: true,
template: '<div class="widget-header"><div class="row"><span class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>', template: '<div class="widget-header"><div class="row"><span ng-class="classes" class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </span><span ng-class="classes" class="pull-right" ng-transclude></span></div></div>',
restrict: 'E' restrict: 'E'
}; };
return directive; return directive;

View File

@ -10,11 +10,21 @@ function ContainerViewModel(data) {
this.Image = data.Image; this.Image = data.Image;
this.Command = data.Command; this.Command = data.Command;
this.Checked = false; this.Checked = false;
this.Labels = data.Labels;
this.Ports = []; this.Ports = [];
this.Mounts = data.Mounts;
for (var i = 0; i < data.Ports.length; ++i) { for (var i = 0; i < data.Ports.length; ++i) {
var p = data.Ports[i]; var p = data.Ports[i];
if (p.PublicPort) { if (p.PublicPort) {
this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort }); this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort });
} }
} }
if (data.Portainer) {
this.Metadata = {};
if (data.Portainer.ResourceControl) {
this.Metadata.ResourceControl = {
OwnerId: data.Portainer.ResourceControl.OwnerId
};
}
}
} }

View File

@ -17,6 +17,10 @@ function ServiceViewModel(data) {
if (data.Spec.TaskTemplate.ContainerSpec.Env) { if (data.Spec.TaskTemplate.ContainerSpec.Env) {
this.Env = data.Spec.TaskTemplate.ContainerSpec.Env; this.Env = data.Spec.TaskTemplate.ContainerSpec.Env;
} }
this.Mounts = [];
if (data.Spec.TaskTemplate.ContainerSpec.Mounts) {
this.Mounts = data.Spec.TaskTemplate.ContainerSpec.Mounts;
}
if (data.Endpoint.Ports) { if (data.Endpoint.Ports) {
this.Ports = data.Endpoint.Ports; this.Ports = data.Endpoint.Ports;
} }
@ -33,4 +37,13 @@ function ServiceViewModel(data) {
this.Checked = false; this.Checked = false;
this.Scale = false; this.Scale = false;
this.EditName = false; this.EditName = false;
if (data.Portainer) {
this.Metadata = {};
if (data.Portainer.ResourceControl) {
this.Metadata.ResourceControl = {
OwnerId: data.Portainer.ResourceControl.OwnerId
};
}
}
} }

11
app/models/user.js Normal file
View File

@ -0,0 +1,11 @@
function UserViewModel(data) {
this.Id = data.Id;
this.Username = data.Username;
this.RoleId = data.Role;
if (data.Role === 1) {
this.RoleName = "administrator";
} else {
this.RoleName = "user";
}
this.Checked = false;
}

14
app/models/volume.js Normal file
View File

@ -0,0 +1,14 @@
function VolumeViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Driver = data.Driver;
this.Mountpoint = data.Mountpoint;
if (data.Portainer) {
this.Metadata = {};
if (data.Portainer.ResourceControl) {
this.Metadata.ResourceControl = {
OwnerId: data.Portainer.ResourceControl.OwnerId
};
}
}
}

View File

@ -1,9 +1,11 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) { .factory('Container', ['$resource', 'Settings', 'EndpointProvider', function ContainerFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/containers/:id/:action', { return $resource(Settings.url + '/:endpointId/containers/:id/:action', {
name: '@name' name: '@name',
}, { endpointId: EndpointProvider.endpointID
},
{
query: {method: 'GET', params: {all: 0, action: 'json', filters: '@filters' }, isArray: true}, query: {method: 'GET', params: {all: 0, action: 'json', filters: '@filters' }, isArray: true},
get: {method: 'GET', params: {action: 'json'}}, get: {method: 'GET', params: {action: 'json'}},
stop: {method: 'POST', params: {id: '@id', t: 5, action: 'stop'}}, stop: {method: 'POST', params: {id: '@id', t: 5, action: 'stop'}},
@ -11,7 +13,6 @@ angular.module('portainer.rest')
kill: {method: 'POST', params: {id: '@id', action: 'kill'}}, kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
pause: {method: 'POST', params: {id: '@id', action: 'pause'}}, pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}}, unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
changes: {method: 'GET', params: {action: 'changes'}, isArray: true},
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000}, stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
start: { start: {
method: 'POST', params: {id: '@id', action: 'start'}, method: 'POST', params: {id: '@id', action: 'start'},

View File

@ -1,7 +1,10 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('ContainerCommit', ['$resource', 'Settings', function ContainerCommitFactory($resource, Settings) { .factory('ContainerCommit', ['$resource', 'Settings', 'EndpointProvider', function ContainerCommitFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/commit', {}, { return $resource(Settings.url + '/:endpointId/commit', {
endpointId: EndpointProvider.endpointID
},
{
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}} commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}}
}); });
}]); }]);

View File

@ -1,11 +1,11 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('ContainerLogs', ['$http', 'Settings', function ContainerLogsFactory($http, Settings) { .factory('ContainerLogs', ['$http', 'Settings', 'EndpointProvider', function ContainerLogsFactory($http, Settings, EndpointProvider) {
'use strict'; 'use strict';
return { return {
get: function (id, params, callback) { get: function (id, params, callback) {
$http({ $http({
method: 'GET', method: 'GET',
url: Settings.url + '/containers/' + id + '/logs', url: Settings.url + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/logs',
params: { params: {
'stdout': params.stdout || 0, 'stdout': params.stdout || 0,
'stderr': params.stderr || 0, 'stderr': params.stderr || 0,

View File

@ -1,11 +1,11 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('ContainerTop', ['$http', 'Settings', function ($http, Settings) { .factory('ContainerTop', ['$http', 'Settings', 'EndpointProvider', function ($http, Settings, EndpointProvider) {
'use strict'; 'use strict';
return { return {
get: function (id, params, callback, errorCallback) { get: function (id, params, callback, errorCallback) {
$http({ $http({
method: 'GET', method: 'GET',
url: Settings.url + '/containers/' + id + '/top', url: Settings.url + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/top',
params: { params: {
ps_args: params.ps_args ps_args: params.ps_args
} }

View File

@ -6,8 +6,7 @@ angular.module('portainer.rest')
query: { method: 'GET', isArray: true }, query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } }, get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } },
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
remove: { method: 'DELETE', params: { id: '@id'} }, remove: { method: 'DELETE', params: { id: '@id'} },
getActiveEndpoint: { method: 'GET', params: { id: '0' } },
setActiveEndpoint: { method: 'POST', params: { id: '@id', action: 'active' } }
}); });
}]); }]);

View File

@ -1,7 +1,10 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Events', ['$resource', 'Settings', function EventFactory($resource, Settings) { .factory('Events', ['$resource', 'Settings', 'EndpointProvider', function EventFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/events', {}, { return $resource(Settings.url + '/:endpointId/events', {
endpointId: EndpointProvider.endpointID
},
{
query: { query: {
method: 'GET', params: {since: '@since', until: '@until'}, method: 'GET', params: {since: '@since', until: '@until'},
isArray: true, transformResponse: jsonObjectsToArrayHandler isArray: true, transformResponse: jsonObjectsToArrayHandler

View File

@ -1,7 +1,10 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Exec', ['$resource', 'Settings', function ExecFactory($resource, Settings) { .factory('Exec', ['$resource', 'Settings', 'EndpointProvider', function ExecFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/exec/:id/:action', {}, { return $resource(Settings.url + '/:endpointId/exec/:id/:action', {
endpointId: EndpointProvider.endpointID
},
{
resize: { resize: {
method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'}, method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'},
transformResponse: genericHandler transformResponse: genericHandler

View File

@ -1,7 +1,10 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Image', ['$resource', 'Settings', function ImageFactory($resource, Settings) { .factory('Image', ['$resource', 'Settings', 'EndpointProvider', function ImageFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/images/:id/:action', {}, { return $resource(Settings.url + '/:endpointId/images/:id/:action', {
endpointId: EndpointProvider.endpointID
},
{
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true}, query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true},
get: {method: 'GET', params: {action: 'json'}}, get: {method: 'GET', params: {action: 'json'}},
search: {method: 'GET', params: {action: 'search'}}, search: {method: 'GET', params: {action: 'search'}},

View File

@ -1,5 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Info', ['$resource', 'Settings', function InfoFactory($resource, Settings) { .factory('Info', ['$resource', 'Settings', 'EndpointProvider', function InfoFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/info', {}); return $resource(Settings.url + '/:endpointId/info', {
endpointId: EndpointProvider.endpointID
});
}]); }]);

View File

@ -1,7 +1,11 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Network', ['$resource', 'Settings', function NetworkFactory($resource, Settings) { .factory('Network', ['$resource', 'Settings', 'EndpointProvider', function NetworkFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/networks/:id/:action', {id: '@id'}, { return $resource(Settings.url + '/:endpointId/networks/:id/:action', {
id: '@id',
endpointId: EndpointProvider.endpointID
},
{
query: {method: 'GET', isArray: true}, query: {method: 'GET', isArray: true},
get: {method: 'GET'}, get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler}, create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},

View File

@ -1,7 +1,10 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Node', ['$resource', 'Settings', function NodeFactory($resource, Settings) { .factory('Node', ['$resource', 'Settings', 'EndpointProvider', function NodeFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/nodes/:id/:action', {}, { return $resource(Settings.url + '/:endpointId/nodes/:id/:action', {
endpointId: EndpointProvider.endpointID
},
{
query: {method: 'GET', isArray: true}, query: {method: 'GET', isArray: true},
get: {method: 'GET', params: {id: '@id'}}, get: {method: 'GET', params: {id: '@id'}},
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} }, update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },

View File

@ -0,0 +1,8 @@
angular.module('portainer.rest')
.factory('ResourceControl', ['$resource', 'USERS_ENDPOINT', function ResourceControlFactory($resource, USERS_ENDPOINT) {
'use strict';
return $resource(USERS_ENDPOINT + '/:userId/resources/:resourceType/:resourceId', {}, {
create: { method: 'POST', params: { userId: '@userId', resourceType: '@resourceType' } },
remove: { method: 'DELETE', params: { userId: '@userId', resourceId: '@resourceId', resourceType: '@resourceType' } },
});
}]);

View File

@ -1,7 +1,10 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Service', ['$resource', 'Settings', function ServiceFactory($resource, Settings) { .factory('Service', ['$resource', 'Settings', 'EndpointProvider', function ServiceFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/services/:id/:action', {}, { return $resource(Settings.url + '/:endpointId/services/:id/:action', {
endpointId: EndpointProvider.endpointID
},
{
get: { method: 'GET', params: {id: '@id'} }, get: { method: 'GET', params: {id: '@id'} },
query: { method: 'GET', isArray: true }, query: { method: 'GET', isArray: true },
create: { method: 'POST', params: {action: 'create'} }, create: { method: 'POST', params: {action: 'create'} },

View File

@ -1,7 +1,10 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Swarm', ['$resource', 'Settings', function SwarmFactory($resource, Settings) { .factory('Swarm', ['$resource', 'Settings', 'EndpointProvider', function SwarmFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/swarm', {}, { return $resource(Settings.url + '/:endpointId/swarm', {
endpointId: EndpointProvider.endpointID
},
{
get: {method: 'GET'} get: {method: 'GET'}
}); });
}]); }]);

View File

@ -1,7 +1,10 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Task', ['$resource', 'Settings', function TaskFactory($resource, Settings) { .factory('Task', ['$resource', 'Settings', 'EndpointProvider', function TaskFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/tasks/:id', {}, { return $resource(Settings.url + '/:endpointId/tasks/:id', {
endpointId: EndpointProvider.endpointID
},
{
get: { method: 'GET', params: {id: '@id'} }, get: { method: 'GET', params: {id: '@id'} },
query: { method: 'GET', isArray: true, params: {filters: '@filters'} } query: { method: 'GET', isArray: true, params: {filters: '@filters'} }
}); });

View File

@ -1,12 +1,15 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Users', ['$resource', 'USERS_ENDPOINT', function UsersFactory($resource, USERS_ENDPOINT) { .factory('Users', ['$resource', 'USERS_ENDPOINT', function UsersFactory($resource, USERS_ENDPOINT) {
'use strict'; 'use strict';
return $resource(USERS_ENDPOINT + '/:username/:action', {}, { return $resource(USERS_ENDPOINT + '/:id/:action', {}, {
create: { method: 'POST' }, create: { method: 'POST' },
get: { method: 'GET', params: { username: '@username' } }, query: { method: 'GET', isArray: true },
update: { method: 'PUT', params: { username: '@username' } }, get: { method: 'GET', params: { id: '@id' } },
checkPassword: { method: 'POST', params: { username: '@username', action: 'passwd' } }, update: { method: 'PUT', params: { id: '@id' } },
checkAdminUser: { method: 'GET', params: { username: 'admin', action: 'check' } }, remove: { method: 'DELETE', params: { id: '@id'} },
initAdminUser: { method: 'POST', params: { username: 'admin', action: 'init' } } // RPCs should be moved to a specific endpoint
checkPassword: { method: 'POST', params: { id: '@id', action: 'passwd' } },
checkAdminUser: { method: 'GET', params: { id: 'admin', action: 'check' }, isArray: true },
initAdminUser: { method: 'POST', params: { id: 'admin', action: 'init' } }
}); });
}]); }]);

View File

@ -1,5 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Version', ['$resource', 'Settings', function VersionFactory($resource, Settings) { .factory('Version', ['$resource', 'Settings', 'EndpointProvider', function VersionFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/version', {}); return $resource(Settings.url + '/:endpointId/version', {
endpointId: EndpointProvider.endpointID
});
}]); }]);

View File

@ -1,7 +1,12 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Volume', ['$resource', 'Settings', function VolumeFactory($resource, Settings) { .factory('Volume', ['$resource', 'Settings', 'EndpointProvider', function VolumeFactory($resource, Settings, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, { return $resource(Settings.url + '/:endpointId/volumes/:name/:action',
{
name: '@name',
endpointId: EndpointProvider.endpointID
},
{
query: {method: 'GET'}, query: {method: 'GET'},
get: {method: 'GET'}, get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler}, create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},

View File

@ -1,14 +1,16 @@
angular.module('portainer.services') angular.module('portainer.services')
.factory('Authentication', ['$q', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', function AuthenticationFactory($q, Auth, jwtHelper, LocalStorage, StateManager) { .factory('Authentication', ['$q', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', function AuthenticationFactory($q, Auth, jwtHelper, LocalStorage, StateManager, EndpointProvider) {
'use strict'; 'use strict';
var credentials = {}; var user = {};
return { return {
init: function() { init: function() {
var jwt = LocalStorage.getJWT(); var jwt = LocalStorage.getJWT();
if (jwt) { if (jwt) {
var tokenPayload = jwtHelper.decodeToken(jwt); var tokenPayload = jwtHelper.decodeToken(jwt);
credentials.username = tokenPayload.username; user.username = tokenPayload.username;
user.ID = tokenPayload.id;
user.role = tokenPayload.role;
} }
}, },
login: function(username, password) { login: function(username, password) {
@ -16,7 +18,10 @@ angular.module('portainer.services')
Auth.login({username: username, password: password}).$promise Auth.login({username: username, password: password}).$promise
.then(function(data) { .then(function(data) {
LocalStorage.storeJWT(data.jwt); LocalStorage.storeJWT(data.jwt);
credentials.username = username; var tokenPayload = jwtHelper.decodeToken(data.jwt);
user.username = username;
user.ID = tokenPayload.id;
user.role = tokenPayload.role;
resolve(); resolve();
}, function() { }, function() {
reject(); reject();
@ -25,14 +30,15 @@ angular.module('portainer.services')
}, },
logout: function() { logout: function() {
StateManager.clean(); StateManager.clean();
EndpointProvider.clean();
LocalStorage.clean(); LocalStorage.clean();
}, },
isAuthenticated: function() { isAuthenticated: function() {
var jwt = LocalStorage.getJWT(); var jwt = LocalStorage.getJWT();
return jwt && !jwtHelper.isTokenExpired(jwt); return jwt && !jwtHelper.isTokenExpired(jwt);
}, },
getCredentials: function() { getUserDetails: function() {
return credentials; return user;
} }
}; };
}]); }]);

View File

@ -0,0 +1,23 @@
angular.module('portainer.services')
.factory('EndpointProvider', ['LocalStorage', function EndpointProviderFactory(LocalStorage) {
'use strict';
var endpoint = {};
var service = {};
service.initialize = function() {
var endpointID = LocalStorage.getEndpointID();
if (endpointID) {
endpoint.ID = endpointID;
}
};
service.clean = function() {
endpoint = {};
};
service.endpointID = function() {
return endpoint.ID;
};
service.setEndpointID = function(id) {
endpoint.ID = id;
LocalStorage.storeEndpointID(id);
};
return service;
}]);

View File

@ -1,52 +1,59 @@
angular.module('portainer.services') angular.module('portainer.services')
.factory('EndpointService', ['$q', 'Endpoints', 'FileUploadService', function EndpointServiceFactory($q, Endpoints, FileUploadService) { .factory('EndpointService', ['$q', 'Endpoints', 'FileUploadService', function EndpointServiceFactory($q, Endpoints, FileUploadService) {
'use strict'; 'use strict';
return { var service = {};
getActive: function() {
return Endpoints.getActiveEndpoint().$promise; service.endpoint = function(endpointID) {
},
setActive: function(endpointID) {
return Endpoints.setActiveEndpoint({id: endpointID}).$promise;
},
endpoint: function(endpointID) {
return Endpoints.get({id: endpointID}).$promise; return Endpoints.get({id: endpointID}).$promise;
},
endpoints: function() {
return Endpoints.query({}).$promise;
},
updateEndpoint: function(ID, name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, type) {
var endpoint = {
id: ID,
Name: name,
URL: type === 'local' ? ("unix://" + URL) : ("tcp://" + URL),
TLS: TLS
}; };
service.endpoints = function() {
return Endpoints.query({}).$promise;
};
service.updateAuthorizedUsers = function(id, authorizedUserIDs) {
return Endpoints.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs}).$promise;
};
service.updateEndpoint = function(id, endpointParams) {
var query = {
name: endpointParams.name,
TLS: endpointParams.TLS,
authorizedUsers: endpointParams.authorizedUsers
};
if (endpointParams.type && endpointParams.URL) {
query.URL = endpointParams.type === 'local' ? ("unix://" + endpointParams.URL) : ("tcp://" + endpointParams.URL);
}
var deferred = $q.defer(); var deferred = $q.defer();
Endpoints.update({}, endpoint, function success(data) { Endpoints.update({id: id}, query).$promise
FileUploadService.uploadTLSFilesForEndpoint(ID, TLSCAFile, TLSCertFile, TLSKeyFile).then(function success(data) { .then(function success() {
return FileUploadService.uploadTLSFilesForEndpoint(id, endpointParams.TLSCAFile, endpointParams.TLSCertFile, endpointParams.TLSKeyFile);
})
.then(function success(data) {
deferred.notify({upload: false}); deferred.notify({upload: false});
deferred.resolve(data); deferred.resolve(data);
}, function error(err) { })
.catch(function error(err) {
deferred.notify({upload: false}); deferred.notify({upload: false});
deferred.reject({msg: 'Unable to upload TLS certs', err: err});
});
}, function error(err) {
deferred.reject({msg: 'Unable to update endpoint', err: err}); deferred.reject({msg: 'Unable to update endpoint', err: err});
}); });
return deferred.promise; return deferred.promise;
}, };
deleteEndpoint: function(endpointID) {
service.deleteEndpoint = function(endpointID) {
return Endpoints.remove({id: endpointID}).$promise; return Endpoints.remove({id: endpointID}).$promise;
}, };
createLocalEndpoint: function(name, URL, TLS, active) {
service.createLocalEndpoint = function(name, URL, TLS, active) {
var endpoint = { var endpoint = {
Name: "local", Name: "local",
URL: "unix:///var/run/docker.sock", URL: "unix:///var/run/docker.sock",
TLS: false TLS: false
}; };
return Endpoints.create({active: active}, endpoint).$promise; return Endpoints.create({}, endpoint).$promise;
}, };
createRemoteEndpoint: function(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, active) {
service.createRemoteEndpoint = function(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, active) {
var endpoint = { var endpoint = {
Name: name, Name: name,
URL: 'tcp://' + URL, URL: 'tcp://' + URL,
@ -79,6 +86,7 @@ angular.module('portainer.services')
deferred.reject({msg: 'Unable to create endpoint', err: err}); deferred.reject({msg: 'Unable to create endpoint', err: err});
}); });
return deferred.promise; return deferred.promise;
}
}; };
return service;
}]); }]);

View File

@ -19,15 +19,15 @@ angular.module('portainer.services')
var deferred = $q.defer(); var deferred = $q.defer();
var queue = []; var queue = [];
if (TLSCAFile !== null) { if (TLSCAFile) {
var uploadTLSCA = uploadFile('api/upload/tls/' + endpointID + '/ca', TLSCAFile); var uploadTLSCA = uploadFile('api/upload/tls/' + endpointID + '/ca', TLSCAFile);
queue.push(uploadTLSCA); queue.push(uploadTLSCA);
} }
if (TLSCertFile !== null) { if (TLSCertFile) {
var uploadTLSCert = uploadFile('api/upload/tls/' + endpointID + '/cert', TLSCertFile); var uploadTLSCert = uploadFile('api/upload/tls/' + endpointID + '/cert', TLSCertFile);
queue.push(uploadTLSCert); queue.push(uploadTLSCert);
} }
if (TLSKeyFile !== null) { if (TLSKeyFile) {
var uploadTLSKey = uploadFile('api/upload/tls/' + endpointID + '/key', TLSKeyFile); var uploadTLSKey = uploadFile('api/upload/tls/' + endpointID + '/key', TLSKeyFile);
queue.push(uploadTLSKey); queue.push(uploadTLSKey);
} }

View File

@ -2,6 +2,12 @@ angular.module('portainer.services')
.factory('LocalStorage', ['localStorageService', function LocalStorageFactory(localStorageService) { .factory('LocalStorage', ['localStorageService', function LocalStorageFactory(localStorageService) {
'use strict'; 'use strict';
return { return {
storeEndpointID: function(id) {
localStorageService.set('ENDPOINT_ID', id);
},
getEndpointID: function() {
return localStorageService.get('ENDPOINT_ID');
},
storeEndpointState: function(state) { storeEndpointState: function(state) {
localStorageService.set('ENDPOINT_STATE', state); localStorageService.set('ENDPOINT_STATE', state);
}, },

View File

@ -2,6 +2,7 @@ angular.module('portainer.services')
.factory('ModalService', [function ModalServiceFactory() { .factory('ModalService', [function ModalServiceFactory() {
'use strict'; 'use strict';
var service = {}; var service = {};
service.confirm = function(options){ service.confirm = function(options){
var box = bootbox.confirm({ var box = bootbox.confirm({
title: options.title, title: options.title,
@ -9,10 +10,10 @@ angular.module('portainer.services')
buttons: { buttons: {
confirm: { confirm: {
label: options.buttons.confirm.label, label: options.buttons.confirm.label,
className: 'btn-danger' className: options.buttons.confirm.className
}, },
cancel: { cancel: {
label: options.buttons.cancel.label label: options.buttons.cancel && options.buttons.cancel.label ? options.buttons.cancel.label : 'Cancel'
} }
}, },
callback: options.callback callback: options.callback
@ -24,5 +25,62 @@ angular.module('portainer.services')
} }
}); });
}; };
service.confirmOwnershipChange = function(callback, msg) {
service.confirm({
title: 'Are you sure ?',
message: msg,
buttons: {
confirm: {
label: 'Change ownership',
className: 'btn-primary'
}
},
callback: callback,
});
};
service.confirmContainerOwnershipChange = function(callback) {
var msg = 'You can change the ownership of a container one way only. You will not be able to make this container private again. <b>Changing ownership on this container will also change the ownership on any attached volume.</b>';
service.confirmOwnershipChange(callback, msg);
};
service.confirmServiceOwnershipChange = function(callback) {
var msg = 'You can change the ownership of a service one way only. You will not be able to make this service private again. <b>Changing ownership on this service will also change the ownership on any attached volume.</b>';
service.confirmOwnershipChange(callback, msg);
};
service.confirmVolumeOwnershipChange = function(callback) {
var msg = 'You can change the ownership of a volume one way only. You will not be able to make this volume private again.';
service.confirmOwnershipChange(callback, msg);
};
service.confirmImageForceRemoval = function(callback) {
service.confirm({
title: "Are you sure?",
message: "Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.",
buttons: {
confirm: {
label: 'Remove the image',
className: 'btn-danger'
}
},
callback: callback,
});
};
service.confirmDeletion = function(message, callback) {
service.confirm({
title: 'Are you sure ?',
message: message,
buttons: {
confirm: {
label: 'Delete',
className: 'btn-danger'
}
},
callback: callback,
});
};
return service; return service;
}]); }]);

View File

@ -0,0 +1,31 @@
angular.module('portainer.services')
.factory('ResourceControlService', ['$q', 'ResourceControl', function ResourceControlServiceFactory($q, ResourceControl) {
'use strict';
var service = {};
service.setContainerResourceControl = function(userID, resourceID) {
return ResourceControl.create({ userId: userID, resourceType: 'container' }, { ResourceID: resourceID }).$promise;
};
service.removeContainerResourceControl = function(userID, resourceID) {
return ResourceControl.remove({ userId: userID, resourceId: resourceID, resourceType: 'container' }).$promise;
};
service.setServiceResourceControl = function(userID, resourceID) {
return ResourceControl.create({ userId: userID, resourceType: 'service' }, { ResourceID: resourceID }).$promise;
};
service.removeServiceResourceControl = function(userID, resourceID) {
return ResourceControl.remove({ userId: userID, resourceId: resourceID, resourceType: 'service' }).$promise;
};
service.setVolumeResourceControl = function(userID, resourceID) {
return ResourceControl.create({ userId: userID, resourceType: 'volume' }, { ResourceID: resourceID }).$promise;
};
service.removeVolumeResourceControl = function(userID, resourceID) {
return ResourceControl.remove({ userId: userID, resourceId: resourceID, resourceType: 'volume' }).$promise;
};
return service;
}]);

View File

@ -0,0 +1,48 @@
angular.module('portainer.services')
.factory('UserService', ['$q', 'Users', function UserServiceFactory($q, Users) {
'use strict';
var service = {};
service.users = function() {
return Users.query({}).$promise;
};
service.user = function(id) {
return Users.get({id: id}).$promise;
};
service.createUser = function(username, password, role) {
return Users.create({}, {username: username, password: password, role: role}).$promise;
};
service.deleteUser = function(id) {
return Users.remove({id: id}).$promise;
};
service.updateUser = function(id, password, role) {
var query = {
password: password,
role: role
};
return Users.update({id: id}, query).$promise;
};
service.updateUserPassword = function(id, currentPassword, newPassword) {
var deferred = $q.defer();
Users.checkPassword({id: id}, {password: currentPassword}).$promise
.then(function success(data) {
if (!data.valid) {
deferred.reject({invalidPassword: true});
}
return service.updateUser(id, newPassword, undefined);
})
.then(function success(data) {
deferred.resolve();
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to update user password', err: err});
});
return deferred.promise;
};
return service;
}]);

View File

@ -84,6 +84,26 @@ a[ng-click]{
color: #23ae89; color: #23ae89;
} }
.tooltip.portainer-tooltip .tooltip-inner {
font-family: Montserrat;
background-color: #ffffff;
padding: .833em 1em;
color: #333333;
border: 1px solid #d4d4d5;
border-radius: .14285714rem;
box-shadow: 0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);
}
.tooltip.portainer-tooltip .tooltip-arrow {
display: none;
}
.fa.tooltip-icon {
margin-left: 5px;
font-size: 1.3em;
color: #337ab7;
}
.fa.red-icon { .fa.red-icon {
color: #ae2323; color: #ae2323;
} }
@ -272,3 +292,40 @@ ul.sidebar .sidebar-list a.active {
border-left: 3px solid #fff; border-left: 3px solid #fff;
background: #2d3e63; background: #2d3e63;
} }
@media (min-width: 768px) {
.pull-sm-left {
float: left !important;
}
.pull-sm-right {
float: right !important;
}
.pull-sm-none {
float: none !important;
}
}
@media (min-width: 992px) {
.pull-md-left {
float: left !important;
}
.pull-md-right {
float: right !important;
}
.pull-md-none {
float: none !important;
}
}
@media (min-width: 1200px) {
.pull-lg-left {
float: left !important;
}
.pull-lg-right {
float: right !important;
}
.pull-lg-none {
float: none !important;
}
}
.pull-none {
float: none !important;
}