mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 21:35:23 +02:00
feat(uac): add multi user management and UAC (#647)
This commit is contained in:
parent
f28f223624
commit
80d50378c5
91 changed files with 3973 additions and 866 deletions
76
api/bolt/data_migration.go
Normal file
76
api/bolt/data_migration.go
Normal 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
|
||||
})
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
|
@ -13,29 +16,49 @@ type Store struct {
|
|||
Path string
|
||||
|
||||
// Services
|
||||
UserService *UserService
|
||||
EndpointService *EndpointService
|
||||
UserService *UserService
|
||||
EndpointService *EndpointService
|
||||
ResourceControlService *ResourceControlService
|
||||
VersionService *VersionService
|
||||
|
||||
db *bolt.DB
|
||||
db *bolt.DB
|
||||
checkForDataMigration bool
|
||||
}
|
||||
|
||||
const (
|
||||
databaseFileName = "portainer.db"
|
||||
userBucketName = "users"
|
||||
endpointBucketName = "endpoints"
|
||||
activeEndpointBucketName = "activeEndpoint"
|
||||
databaseFileName = "portainer.db"
|
||||
versionBucketName = "version"
|
||||
userBucketName = "users"
|
||||
endpointBucketName = "endpoints"
|
||||
containerResourceControlBucketName = "containerResourceControl"
|
||||
serviceResourceControlBucketName = "serviceResourceControl"
|
||||
volumeResourceControlBucketName = "volumeResourceControl"
|
||||
)
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
func NewStore(storePath string) *Store {
|
||||
func NewStore(storePath string) (*Store, error) {
|
||||
store := &Store{
|
||||
Path: storePath,
|
||||
UserService: &UserService{},
|
||||
EndpointService: &EndpointService{},
|
||||
Path: storePath,
|
||||
UserService: &UserService{},
|
||||
EndpointService: &EndpointService{},
|
||||
ResourceControlService: &ResourceControlService{},
|
||||
VersionService: &VersionService{},
|
||||
}
|
||||
store.UserService.store = store
|
||||
store.EndpointService.store = store
|
||||
return store
|
||||
store.ResourceControlService.store = store
|
||||
store.VersionService.store = store
|
||||
|
||||
_, err := os.Stat(storePath)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
store.checkForDataMigration = false
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
store.checkForDataMigration = true
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
|
@ -47,7 +70,11 @@ func (store *Store) Open() error {
|
|||
}
|
||||
store.db = db
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(versionBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(userBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -55,7 +82,15 @@ func (store *Store) Open() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(activeEndpointBucketName))
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(containerResourceControlBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(serviceResourceControlBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(volumeResourceControlBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -70,3 +105,32 @@ func (store *Store) Close() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
func (store *Store) MigrateData() error {
|
||||
if !store.checkForDataMigration {
|
||||
err := store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == portainer.ErrDBVersionNotFound {
|
||||
version = 0
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version < portainer.DBVersion {
|
||||
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
|
||||
migrator := NewMigrator(store, version)
|
||||
err = migrator.Migrate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -12,10 +12,6 @@ type EndpointService struct {
|
|||
store *Store
|
||||
}
|
||||
|
||||
const (
|
||||
activeEndpointID = 0
|
||||
)
|
||||
|
||||
// Endpoint returns an endpoint by ID.
|
||||
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||
var data []byte
|
||||
|
@ -138,62 +134,6 @@ func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error {
|
|||
})
|
||||
}
|
||||
|
||||
// GetActive returns the active endpoint.
|
||||
func (service *EndpointService) GetActive() (*portainer.Endpoint, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||
value := bucket.Get(internal.Itob(activeEndpointID))
|
||||
if value == nil {
|
||||
return portainer.ErrEndpointNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var endpoint portainer.Endpoint
|
||||
err = internal.UnmarshalEndpoint(data, &endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
// SetActive saves an endpoint as active.
|
||||
func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||
|
||||
data, err := internal.MarshalEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(activeEndpointID), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteActive deletes the active endpoint.
|
||||
func (service *EndpointService) DeleteActive() error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||
err := bucket.Delete(internal.Itob(activeEndpointID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
|
||||
data, err := internal.MarshalEndpoint(endpoint)
|
||||
if err != nil {
|
||||
|
@ -210,6 +150,5 @@ func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket)
|
|||
func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
|
||||
id, _ := bucket.NextSequence()
|
||||
endpoint.ID = portainer.EndpointID(id)
|
||||
|
||||
return marshalAndStoreEndpoint(endpoint, bucket)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,16 @@ func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
|
|||
return json.Unmarshal(data, endpoint)
|
||||
}
|
||||
|
||||
// MarshalResourceControl encodes a resource control object to binary format.
|
||||
func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) {
|
||||
return json.Marshal(rc)
|
||||
}
|
||||
|
||||
// UnmarshalResourceControl decodes a resource control object from a binary data.
|
||||
func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error {
|
||||
return json.Unmarshal(data, rc)
|
||||
}
|
||||
|
||||
// Itob returns an 8-byte big endian representation of v.
|
||||
// This function is typically used for encoding integer IDs to byte slices
|
||||
// so that they can be used as BoltDB keys.
|
||||
|
|
110
api/bolt/resourcecontrol_service.go
Normal file
110
api/bolt/resourcecontrol_service.go
Normal 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
|
||||
})
|
||||
}
|
|
@ -12,12 +12,12 @@ type UserService struct {
|
|||
store *Store
|
||||
}
|
||||
|
||||
// User returns a user by username.
|
||||
func (service *UserService) User(username string) (*portainer.User, error) {
|
||||
// User returns a user by ID
|
||||
func (service *UserService) User(ID portainer.UserID) (*portainer.User, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
value := bucket.Get([]byte(username))
|
||||
value := bucket.Get(internal.Itob(int(ID)))
|
||||
if value == nil {
|
||||
return portainer.ErrUserNotFound
|
||||
}
|
||||
|
@ -38,8 +38,88 @@ func (service *UserService) User(username string) (*portainer.User, error) {
|
|||
return &user, nil
|
||||
}
|
||||
|
||||
// UserByUsername returns a user by username.
|
||||
func (service *UserService) UserByUsername(username string) (*portainer.User, error) {
|
||||
var user *portainer.User
|
||||
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var u portainer.User
|
||||
err := internal.UnmarshalUser(v, &u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Username == username {
|
||||
user = &u
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return portainer.ErrUserNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Users return an array containing all the users.
|
||||
func (service *UserService) Users() ([]portainer.User, error) {
|
||||
var users = make([]portainer.User, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var user portainer.User
|
||||
err := internal.UnmarshalUser(v, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UsersByRole return an array containing all the users with the specified role.
|
||||
func (service *UserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
|
||||
var users = make([]portainer.User, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var user portainer.User
|
||||
err := internal.UnmarshalUser(v, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Role == role {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UpdateUser saves a user.
|
||||
func (service *UserService) UpdateUser(user *portainer.User) error {
|
||||
func (service *UserService) UpdateUser(ID portainer.UserID, user *portainer.User) error {
|
||||
data, err := internal.MarshalUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -47,7 +127,41 @@ func (service *UserService) UpdateUser(user *portainer.User) error {
|
|||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
err = bucket.Put([]byte(user.Username), data)
|
||||
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (service *UserService) CreateUser(user *portainer.User) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
user.ID = portainer.UserID(id)
|
||||
|
||||
data, err := internal.MarshalUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(int(user.ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user.
|
||||
func (service *UserService) DeleteUser(ID portainer.UserID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
err := bucket.Delete(internal.Itob(int(ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
58
api/bolt/version_service.go
Normal file
58
api/bolt/version_service.go
Normal 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
|
||||
})
|
||||
}
|
|
@ -36,8 +36,17 @@ func initFileService(dataStorePath string) portainer.FileService {
|
|||
}
|
||||
|
||||
func initStore(dataStorePath string) *bolt.Store {
|
||||
var store = bolt.NewStore(dataStorePath)
|
||||
err := store.Open()
|
||||
store, err := bolt.NewStore(dataStorePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.MigrateData()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -91,31 +100,6 @@ func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService
|
|||
return &endpoints[0]
|
||||
}
|
||||
|
||||
func initActiveEndpoint(endpointService portainer.EndpointService, flags *portainer.CLIFlags) *portainer.Endpoint {
|
||||
activeEndpoint, err := endpointService.GetActive()
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
if *flags.Endpoint != "" {
|
||||
activeEndpoint = &portainer.Endpoint{
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
}
|
||||
err = endpointService.CreateEndpoint(activeEndpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else if *flags.ExternalEndpoints != "" {
|
||||
activeEndpoint = retrieveFirstEndpointFromDatabase(endpointService)
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return activeEndpoint
|
||||
}
|
||||
|
||||
func main() {
|
||||
flags := initCLI()
|
||||
|
||||
|
@ -132,21 +116,43 @@ func main() {
|
|||
|
||||
settings := initSettings(authorizeEndpointMgmt, flags)
|
||||
|
||||
activeEndpoint := initActiveEndpoint(store.EndpointService, flags)
|
||||
if *flags.Endpoint != "" {
|
||||
var endpoints []portainer.Endpoint
|
||||
endpoints, err := store.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
}
|
||||
err = store.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
|
||||
}
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
Settings: settings,
|
||||
TemplatesURL: *flags.Templates,
|
||||
AuthDisabled: *flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
UserService: store.UserService,
|
||||
EndpointService: store.EndpointService,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
ActiveEndpoint: activeEndpoint,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
Settings: settings,
|
||||
TemplatesURL: *flags.Templates,
|
||||
AuthDisabled: *flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
UserService: store.UserService,
|
||||
EndpointService: store.EndpointService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||
|
|
|
@ -66,7 +66,10 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint
|
|||
|
||||
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
|
||||
var endpoint *portainer.Endpoint
|
||||
if original.URL != updated.URL || original.TLS != updated.TLS {
|
||||
if original.URL != updated.URL || original.TLS != updated.TLS ||
|
||||
(updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) ||
|
||||
(updated.TLS && original.TLSCertPath != updated.TLSCertPath) ||
|
||||
(updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) {
|
||||
endpoint = original
|
||||
endpoint.URL = updated.URL
|
||||
if updated.TLS {
|
||||
|
|
|
@ -2,19 +2,26 @@ package portainer
|
|||
|
||||
// General errors.
|
||||
const (
|
||||
ErrUnauthorized = Error("Unauthorized")
|
||||
ErrUnauthorized = Error("Unauthorized")
|
||||
ErrResourceAccessDenied = Error("Access denied to resource")
|
||||
)
|
||||
|
||||
// User errors.
|
||||
const (
|
||||
ErrUserNotFound = Error("User not found")
|
||||
ErrUserAlreadyExists = Error("User already exists")
|
||||
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
|
||||
)
|
||||
|
||||
// Endpoint errors.
|
||||
const (
|
||||
ErrEndpointNotFound = Error("Endpoint not found")
|
||||
ErrNoActiveEndpoint = Error("Undefined Docker endpoint")
|
||||
ErrEndpointNotFound = Error("Endpoint not found")
|
||||
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
||||
)
|
||||
|
||||
// Version errors.
|
||||
const (
|
||||
ErrDBVersionNotFound = Error("DB version not found")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
|
@ -24,8 +31,9 @@ const (
|
|||
|
||||
// JWT errors.
|
||||
const (
|
||||
ErrSecretGeneration = Error("Unable to generate secret key")
|
||||
ErrInvalidJWTToken = Error("Invalid JWT token")
|
||||
ErrSecretGeneration = Error("Unable to generate secret key")
|
||||
ErrInvalidJWTToken = Error("Invalid JWT token")
|
||||
ErrMissingContextData = Error("Unable to find JWT data in request context")
|
||||
)
|
||||
|
||||
// File errors.
|
||||
|
|
|
@ -33,12 +33,14 @@ const (
|
|||
)
|
||||
|
||||
// NewAuthHandler returns a new instance of AuthHandler.
|
||||
func NewAuthHandler() *AuthHandler {
|
||||
func NewAuthHandler(mw *middleWareService) *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.HandleFunc("/auth", h.handlePostAuth)
|
||||
h.Handle("/auth",
|
||||
mw.public(http.HandlerFunc(h.handlePostAuth)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
@ -68,7 +70,7 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
|
|||
var username = req.Username
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(username)
|
||||
u, err := handler.UserService.UserByUsername(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
|
@ -84,7 +86,9 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
username,
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
}
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
@ -18,142 +16,95 @@ import (
|
|||
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||
type DockerHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
proxy http.Handler
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
ProxyFactory ProxyFactory
|
||||
proxies map[portainer.EndpointID]http.Handler
|
||||
}
|
||||
|
||||
// NewDockerHandler returns a new instance of DockerHandler.
|
||||
func NewDockerHandler(middleWareService *middleWareService) *DockerHandler {
|
||||
func NewDockerHandler(mw *middleWareService, resourceControlService portainer.ResourceControlService) *DockerHandler {
|
||||
h := &DockerHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
ProxyFactory: ProxyFactory{
|
||||
ResourceControlService: resourceControlService,
|
||||
},
|
||||
proxies: make(map[portainer.EndpointID]http.Handler),
|
||||
}
|
||||
h.PathPrefix("/").Handler(middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.proxyRequestsToDockerAPI(w, r)
|
||||
})))
|
||||
h.PathPrefix("/{id}/").Handler(
|
||||
mw.authenticated(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
func checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if handler.proxy != nil {
|
||||
handler.proxy.ServeHTTP(w, r)
|
||||
} else {
|
||||
Error(w, portainer.ErrNoActiveEndpoint, http.StatusNotFound, handler.Logger)
|
||||
}
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
func (handler *DockerHandler) setupProxy(endpoint *portainer.Endpoint) error {
|
||||
var proxy http.Handler
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
parsedID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return err
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLS {
|
||||
proxy, err = newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
proxy = newHTTPProxy(endpointURL)
|
||||
}
|
||||
} else {
|
||||
// Assume unix:// scheme
|
||||
proxy = newSocketProxy(endpointURL.Path)
|
||||
}
|
||||
handler.proxy = proxy
|
||||
return nil
|
||||
}
|
||||
|
||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves
|
||||
func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
req.Host = req.URL.Host
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
// explicitly disable User-Agent so it's not set to default value
|
||||
req.Header.Set("User-Agent", "")
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.Role != portainer.AdministratorRole && !checkEndpointAccessControl(endpoint, tokenData.ID) {
|
||||
Error(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := handler.proxies[endpointID]
|
||||
if proxy == nil {
|
||||
proxy, err = handler.createAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func newHTTPProxy(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return NewSingleHostReverseProxyWithHostHeader(u)
|
||||
}
|
||||
func (handler *DockerHandler) createAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
var proxy http.Handler
|
||||
|
||||
func newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
u.Scheme = "https"
|
||||
proxy := NewSingleHostReverseProxyWithHostHeader(u)
|
||||
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: config,
|
||||
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLS {
|
||||
proxy, err = handler.ProxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
proxy = handler.ProxyFactory.newHTTPProxy(endpointURL)
|
||||
}
|
||||
} else {
|
||||
// Assume unix:// scheme
|
||||
proxy = handler.ProxyFactory.newSocketProxy(endpointURL.Path)
|
||||
}
|
||||
|
||||
handler.proxies[endpoint.ID] = proxy
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func newSocketProxy(path string) http.Handler {
|
||||
return &unixSocketHandler{path}
|
||||
}
|
||||
|
||||
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
|
||||
type unixSocketHandler struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
}
|
||||
|
|
121
api/http/docker_proxy.go
Normal file
121
api/http/docker_proxy.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -20,8 +20,7 @@ type EndpointHandler struct {
|
|||
authorizeEndpointManagement bool
|
||||
EndpointService portainer.EndpointService
|
||||
FileService portainer.FileService
|
||||
server *Server
|
||||
middleWareService *middleWareService
|
||||
// server *Server
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -31,30 +30,24 @@ const (
|
|||
)
|
||||
|
||||
// NewEndpointHandler returns a new instance of EndpointHandler.
|
||||
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
|
||||
func NewEndpointHandler(mw *middleWareService) *EndpointHandler {
|
||||
h := &EndpointHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostEndpoints(w, r)
|
||||
}))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetEndpoints(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetEndpoint(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePutEndpoint(w, r)
|
||||
}))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleDeleteEndpoint(w, r)
|
||||
}))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/active", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostEndpoint(w, r)
|
||||
}))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
mw.administrator(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
mw.authenticated(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}/access",
|
||||
mw.administrator(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
@ -65,12 +58,35 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
|
|||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
encodeJSON(w, endpoints, handler.Logger)
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData == nil {
|
||||
Error(w, portainer.ErrInvalidJWTToken, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var allowedEndpoints []portainer.Endpoint
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
allowedEndpoints = make([]portainer.Endpoint, 0)
|
||||
for _, endpoint := range endpoints {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == tokenData.ID {
|
||||
allowedEndpoints = append(allowedEndpoints, endpoint)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
allowedEndpoints = endpoints
|
||||
}
|
||||
|
||||
encodeJSON(w, allowedEndpoints, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpoints handles POST requests on /endpoints
|
||||
// if the active URL parameter is specified, will also define the new endpoint as the active endpoint.
|
||||
// /endpoints(?active=true|false)
|
||||
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
|
@ -90,9 +106,10 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
|||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
|
@ -115,22 +132,6 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
|||
}
|
||||
}
|
||||
|
||||
activeEndpointParameter := r.FormValue("active")
|
||||
if activeEndpointParameter != "" {
|
||||
active, err := strconv.ParseBool(activeEndpointParameter)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
if active == true {
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
|
@ -145,7 +146,6 @@ type postEndpointsResponse struct {
|
|||
}
|
||||
|
||||
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||
// GET /endpoints/0 returns active endpoint
|
||||
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
@ -156,48 +156,6 @@ func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http
|
|||
return
|
||||
}
|
||||
|
||||
var endpoint *portainer.Endpoint
|
||||
if id == "0" {
|
||||
endpoint, err = handler.EndpointService.GetActive()
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if handler.server.ActiveEndpoint == nil {
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpoint handles POST requests on /endpoints/:id/active
|
||||
func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
|
@ -207,12 +165,58 @@ func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *htt
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
encodeJSON(w, endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
|
||||
func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointAccessRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range req.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
endpoint.AuthorizedUsers = authorizedUserIDs
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type putEndpointAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"required"`
|
||||
}
|
||||
|
||||
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
|
@ -241,14 +245,25 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
|||
return
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
endpoint.Name = req.Name
|
||||
}
|
||||
|
||||
if req.URL != "" {
|
||||
endpoint.URL = req.URL
|
||||
}
|
||||
|
||||
if req.TLS {
|
||||
endpoint.TLS = true
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||
|
@ -256,6 +271,10 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
|||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
} else {
|
||||
endpoint.TLS = false
|
||||
endpoint.TLSCACertPath = ""
|
||||
endpoint.TLSCertPath = ""
|
||||
endpoint.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
|
@ -271,13 +290,12 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
|||
}
|
||||
|
||||
type putEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
TLS bool
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
}
|
||||
|
||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||
// DELETE /endpoints/0 deletes the active endpoint
|
||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
|
@ -293,13 +311,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
|||
return
|
||||
}
|
||||
|
||||
var endpoint *portainer.Endpoint
|
||||
if id == "0" {
|
||||
endpoint, err = handler.EndpointService.GetActive()
|
||||
endpointID = int(endpoint.ID)
|
||||
} else {
|
||||
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
}
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
|
@ -314,13 +326,6 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
|||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if id == "0" {
|
||||
err = handler.EndpointService.DeleteActive()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
|
||||
|
|
|
@ -27,6 +27,10 @@ const (
|
|||
ErrInvalidJSON = portainer.Error("Invalid JSON")
|
||||
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
|
||||
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
||||
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
|
||||
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
|
||||
// ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse
|
||||
ErrEmptyResponseBody = portainer.Error("Empty response body")
|
||||
)
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
|
|
|
@ -1,33 +1,61 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Service represents a service to manage HTTP middlewares
|
||||
type middleWareService struct {
|
||||
jwtService portainer.JWTService
|
||||
authDisabled bool
|
||||
}
|
||||
|
||||
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
|
||||
for _, mw := range middleware {
|
||||
h = mw(h)
|
||||
type (
|
||||
// middleWareService represents a service to manage HTTP middlewares
|
||||
middleWareService struct {
|
||||
jwtService portainer.JWTService
|
||||
authDisabled bool
|
||||
}
|
||||
contextKey int
|
||||
)
|
||||
|
||||
const (
|
||||
contextAuthenticationKey contextKey = iota
|
||||
)
|
||||
|
||||
func extractTokenDataFromRequestContext(request *http.Request) (*portainer.TokenData, error) {
|
||||
contextData := request.Context().Value(contextAuthenticationKey)
|
||||
if contextData == nil {
|
||||
return nil, portainer.ErrMissingContextData
|
||||
}
|
||||
|
||||
tokenData := contextData.(*portainer.TokenData)
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
// public defines a chain of middleware for public endpoints (no authentication required)
|
||||
func (service *middleWareService) public(h http.Handler) http.Handler {
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
func (service *middleWareService) addMiddleWares(h http.Handler) http.Handler {
|
||||
h = service.middleWareSecureHeaders(h)
|
||||
h = service.middleWareAuthenticate(h)
|
||||
// authenticated defines a chain of middleware for private endpoints (authentication required)
|
||||
func (service *middleWareService) authenticated(h http.Handler) http.Handler {
|
||||
h = service.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// middleWareAuthenticate provides secure headers middleware for handlers
|
||||
func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handler {
|
||||
// administrator defines a chain of middleware for private administrator restricted endpoints
|
||||
// (authentication and role admin required)
|
||||
func (service *middleWareService) administrator(h http.Handler) http.Handler {
|
||||
h = mwCheckAdministratorRole(h)
|
||||
h = service.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// mwSecureHeaders provides secure headers middleware for handlers
|
||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
|
@ -35,9 +63,28 @@ func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handle
|
|||
})
|
||||
}
|
||||
|
||||
// middleWareAuthenticate provides Authentication middleware for handlers
|
||||
func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler {
|
||||
// mwCheckAdministratorRole check the role of the user associated to the request
|
||||
func mwCheckAdministratorRole(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// mwCheckAuthentication provides Authentication middleware for handlers
|
||||
func (service *middleWareService) mwCheckAuthentication(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var tokenData *portainer.TokenData
|
||||
if !service.authDisabled {
|
||||
var token string
|
||||
|
||||
|
@ -53,14 +100,20 @@ func (service *middleWareService) middleWareAuthenticate(next http.Handler) http
|
|||
return
|
||||
}
|
||||
|
||||
err := service.jwtService.VerifyToken(token)
|
||||
var err error
|
||||
tokenData, err = service.jwtService.ParseAndVerifyToken(token)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
tokenData = &portainer.TokenData{
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
ctx := context.WithValue(r.Context(), contextAuthenticationKey, tokenData)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
})
|
||||
}
|
||||
|
|
664
api/http/proxy_transport.go
Normal file
664
api/http/proxy_transport.go
Normal 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
|
||||
}
|
|
@ -8,35 +8,19 @@ import (
|
|||
|
||||
// Server implements the portainer.Server interface
|
||||
type Server struct {
|
||||
BindAddress string
|
||||
AssetsPath string
|
||||
AuthDisabled bool
|
||||
EndpointManagement bool
|
||||
UserService portainer.UserService
|
||||
EndpointService portainer.EndpointService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
ActiveEndpoint *portainer.Endpoint
|
||||
Handler *Handler
|
||||
}
|
||||
|
||||
func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
|
||||
if endpoint != nil {
|
||||
server.ActiveEndpoint = endpoint
|
||||
server.Handler.WebSocketHandler.endpoint = endpoint
|
||||
err := server.Handler.DockerHandler.setupProxy(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = server.EndpointService.SetActive(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
BindAddress string
|
||||
AssetsPath string
|
||||
AuthDisabled bool
|
||||
EndpointManagement bool
|
||||
UserService portainer.UserService
|
||||
EndpointService portainer.EndpointService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
Handler *Handler
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
|
@ -46,7 +30,7 @@ func (server *Server) Start() error {
|
|||
authDisabled: server.AuthDisabled,
|
||||
}
|
||||
|
||||
var authHandler = NewAuthHandler()
|
||||
var authHandler = NewAuthHandler(middleWareService)
|
||||
authHandler.UserService = server.UserService
|
||||
authHandler.CryptoService = server.CryptoService
|
||||
authHandler.JWTService = server.JWTService
|
||||
|
@ -54,18 +38,19 @@ func (server *Server) Start() error {
|
|||
var userHandler = NewUserHandler(middleWareService)
|
||||
userHandler.UserService = server.UserService
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
userHandler.ResourceControlService = server.ResourceControlService
|
||||
var settingsHandler = NewSettingsHandler(middleWareService)
|
||||
settingsHandler.settings = server.Settings
|
||||
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||
templatesHandler.templatesURL = server.TemplatesURL
|
||||
var dockerHandler = NewDockerHandler(middleWareService)
|
||||
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
|
||||
dockerHandler.EndpointService = server.EndpointService
|
||||
var websocketHandler = NewWebSocketHandler()
|
||||
// EndpointHandler requires a reference to the server to be able to update the active endpoint.
|
||||
websocketHandler.EndpointService = server.EndpointService
|
||||
var endpointHandler = NewEndpointHandler(middleWareService)
|
||||
endpointHandler.authorizeEndpointManagement = server.EndpointManagement
|
||||
endpointHandler.EndpointService = server.EndpointService
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.server = server
|
||||
var uploadHandler = NewUploadHandler(middleWareService)
|
||||
uploadHandler.FileService = server.FileService
|
||||
var fileHandler = newFileHandler(server.AssetsPath)
|
||||
|
@ -81,10 +66,6 @@ func (server *Server) Start() error {
|
|||
FileHandler: fileHandler,
|
||||
UploadHandler: uploadHandler,
|
||||
}
|
||||
err := server.updateActiveEndpoint(server.ActiveEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return http.ListenAndServe(server.BindAddress, server.Handler)
|
||||
}
|
||||
|
|
|
@ -13,19 +13,19 @@ import (
|
|||
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||
type SettingsHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
settings *portainer.Settings
|
||||
Logger *log.Logger
|
||||
settings *portainer.Settings
|
||||
}
|
||||
|
||||
// NewSettingsHandler returns a new instance of SettingsHandler.
|
||||
func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
|
||||
func NewSettingsHandler(mw *middleWareService) *SettingsHandler {
|
||||
h := &SettingsHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.HandleFunc("/settings", h.handleGetSettings)
|
||||
h.Handle("/settings",
|
||||
mw.public(http.HandlerFunc(h.handleGetSettings)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
|
|
@ -12,21 +12,18 @@ import (
|
|||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||
type TemplatesHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
templatesURL string
|
||||
Logger *log.Logger
|
||||
templatesURL string
|
||||
}
|
||||
|
||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||
func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler {
|
||||
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
||||
h := &TemplatesHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/templates", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetTemplates(w, r)
|
||||
})))
|
||||
h.Handle("/templates",
|
||||
mw.authenticated(http.HandlerFunc(h.handleGetTemplates)))
|
||||
return h
|
||||
}
|
||||
|
||||
|
|
|
@ -14,21 +14,18 @@ import (
|
|||
// UploadHandler represents an HTTP API handler for managing file uploads.
|
||||
type UploadHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
FileService portainer.FileService
|
||||
middleWareService *middleWareService
|
||||
Logger *log.Logger
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewUploadHandler returns a new instance of UploadHandler.
|
||||
func NewUploadHandler(middleWareService *middleWareService) *UploadHandler {
|
||||
func NewUploadHandler(mw *middleWareService) *UploadHandler {
|
||||
h := &UploadHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUploadTLS(w, r)
|
||||
})))
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUploadTLS)))
|
||||
return h
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
|
@ -15,43 +17,44 @@ import (
|
|||
// UserHandler represents an HTTP API handler for managing users.
|
||||
type UserHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
middleWareService *middleWareService
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
}
|
||||
|
||||
// NewUserHandler returns a new instance of UserHandler.
|
||||
func NewUserHandler(middleWareService *middleWareService) *UserHandler {
|
||||
func NewUserHandler(mw *middleWareService) *UserHandler {
|
||||
h := &UserHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUsers(w, r)
|
||||
})))
|
||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetUser(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePutUser(w, r)
|
||||
}))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUserPasswd(w, r)
|
||||
})))
|
||||
h.HandleFunc("/users/admin/check", h.handleGetAdminCheck)
|
||||
h.HandleFunc("/users/admin/init", h.handlePostAdminInit)
|
||||
h.Handle("/users",
|
||||
mw.administrator(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
|
||||
h.Handle("/users",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUserPasswd)))
|
||||
h.Handle("/users/{userId}/resources/{resourceType}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUserResource))).Methods(http.MethodPost)
|
||||
h.Handle("/users/{userId}/resources/{resourceType}/{resourceId}",
|
||||
mw.authenticated(http.HandlerFunc(h.handleDeleteUserResource))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/admin/check",
|
||||
mw.public(http.HandlerFunc(h.handleGetAdminCheck)))
|
||||
h.Handle("/users/admin/init",
|
||||
mw.public(http.HandlerFunc(h.handlePostAdminInit)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handlePostUsers handles POST requests on /users
|
||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
var req postUsersRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
|
@ -64,8 +67,26 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
var role portainer.UserRole
|
||||
if req.Role == 1 {
|
||||
role = portainer.AdministratorRole
|
||||
} else {
|
||||
role = portainer.StandardUserRole
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername(req.Username)
|
||||
if err != nil && err != portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
Error(w, portainer.ErrUserAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: req.Username,
|
||||
Role: role,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
|
@ -73,7 +94,7 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -83,9 +104,24 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
|||
type postUsersRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:username/passwd
|
||||
// handleGetUsers handles GET requests on /users
|
||||
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := handler.UserService.Users()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
encodeJSON(w, users, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:id/passwd
|
||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
|
@ -93,15 +129,21 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
|
|||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postUserPasswdRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
|
@ -109,7 +151,7 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
|
|||
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(username)
|
||||
u, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
|
@ -135,12 +177,18 @@ type postUserPasswdResponse struct {
|
|||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
// handleGetUser handles GET requests on /users/:username
|
||||
// handleGetUser handles GET requests on /users/:id
|
||||
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
id := vars["id"]
|
||||
|
||||
user, err := handler.UserService.User(username)
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
|
@ -153,30 +201,74 @@ func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request
|
|||
encodeJSON(w, &user, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutUser handles PUT requests on /users/:username
|
||||
// handlePutUser handles PUT requests on /users/:id
|
||||
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: req.Username,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
if req.Password == "" && req.Role == 0 {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != "" {
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Role != 0 {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
if req.Role == 1 {
|
||||
user.Role = portainer.AdministratorRole
|
||||
} else {
|
||||
user.Role = portainer.StandardUserRole
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user.ID, user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -184,8 +276,8 @@ func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
type putUserRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
Password string `valid:"-"`
|
||||
Role int `valid:"-"`
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles GET requests on /users/admin/check
|
||||
|
@ -195,17 +287,15 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
encodeJSON(w, &user, handler.Logger)
|
||||
if len(users) == 0 {
|
||||
Error(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||
|
@ -227,10 +317,11 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User("admin")
|
||||
user, err := handler.UserService.UserByUsername("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
|
@ -238,7 +329,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -256,3 +347,134 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
|||
type postAdminInitRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
// handleDeleteUser handles DELETE requests on /users/:id
|
||||
func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.DeleteUser(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostUserResource handles POST requests on /users/:userId/resources/:resourceType
|
||||
func (handler *UserHandler) handlePostUserResource(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["userId"]
|
||||
resourceType := vars["resourceType"]
|
||||
|
||||
uid, err := strconv.Atoi(userID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var rcType portainer.ResourceControlType
|
||||
if resourceType == "container" {
|
||||
rcType = portainer.ContainerResourceControl
|
||||
} else if resourceType == "service" {
|
||||
rcType = portainer.ServiceResourceControl
|
||||
} else if resourceType == "volume" {
|
||||
rcType = portainer.VolumeResourceControl
|
||||
} else {
|
||||
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.ID != portainer.UserID(uid) {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postUserResourceRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resource := portainer.ResourceControl{
|
||||
OwnerID: portainer.UserID(uid),
|
||||
ResourceID: req.ResourceID,
|
||||
AccessLevel: portainer.RestrictedResourceAccessLevel,
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.CreateResourceControl(req.ResourceID, &resource, rcType)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type postUserResourceRequest struct {
|
||||
ResourceID string `valid:"required"`
|
||||
}
|
||||
|
||||
// handleDeleteUserResource handles DELETE requests on /users/:userId/resources/:resourceType/:resourceId
|
||||
func (handler *UserHandler) handleDeleteUserResource(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["userId"]
|
||||
resourceID := vars["resourceId"]
|
||||
resourceType := vars["resourceType"]
|
||||
|
||||
uid, err := strconv.Atoi(userID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var rcType portainer.ResourceControlType
|
||||
if resourceType == "container" {
|
||||
rcType = portainer.ContainerResourceControl
|
||||
} else if resourceType == "service" {
|
||||
rcType = portainer.ServiceResourceControl
|
||||
} else if resourceType == "volume" {
|
||||
rcType = portainer.VolumeResourceControl
|
||||
} else {
|
||||
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(uid) {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.DeleteResourceControl(resourceID, rcType)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
|
@ -14,18 +12,19 @@ import (
|
|||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
||||
type WebSocketHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
endpoint *portainer.Endpoint
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
}
|
||||
|
||||
// NewWebSocketHandler returns a new instance of WebSocketHandler.
|
||||
|
@ -41,34 +40,47 @@ func NewWebSocketHandler() *WebSocketHandler {
|
|||
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||
qry := ws.Request().URL.Query()
|
||||
execID := qry.Get("id")
|
||||
edpID := qry.Get("endpointId")
|
||||
|
||||
// Should not be managed here
|
||||
endpoint, err := url.Parse(handler.endpoint.URL)
|
||||
parsedID, err := strconv.Atoi(edpID)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse endpoint URL: %s", err)
|
||||
log.Printf("Unable to parse endpoint ID: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
log.Printf("Unable to retrieve endpoint: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
log.Printf("Unable to parse endpoint URL: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
var host string
|
||||
if endpoint.Scheme == "tcp" {
|
||||
host = endpoint.Host
|
||||
} else if endpoint.Scheme == "unix" {
|
||||
host = endpoint.Path
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
host = endpointURL.Host
|
||||
} else if endpointURL.Scheme == "unix" {
|
||||
host = endpointURL.Path
|
||||
}
|
||||
|
||||
// Should not be managed here
|
||||
var tlsConfig *tls.Config
|
||||
if handler.endpoint.TLS {
|
||||
tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath,
|
||||
handler.endpoint.TLSCertPath,
|
||||
handler.endpoint.TLSKeyPath)
|
||||
if endpoint.TLS {
|
||||
tlsConfig, err = createTLSConfiguration(endpoint.TLSCACertPath,
|
||||
endpoint.TLSCertPath,
|
||||
endpoint.TLSKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := hijack(host, endpoint.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
if err := hijack(host, endpointURL.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
log.Fatalf("error during hijack: %s", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ import (
|
|||
"github.com/portainer/portainer"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gorilla/securecookie"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Service represents a service for managing JWT tokens.
|
||||
|
@ -15,7 +16,9 @@ type Service struct {
|
|||
}
|
||||
|
||||
type claims struct {
|
||||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
|
@ -35,7 +38,9 @@ func NewService() (*Service, error) {
|
|||
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
|
||||
expireToken := time.Now().Add(time.Hour * 8).Unix()
|
||||
cl := claims{
|
||||
int(data.ID),
|
||||
data.Username,
|
||||
int(data.Role),
|
||||
jwt.StandardClaims{
|
||||
ExpiresAt: expireToken,
|
||||
},
|
||||
|
@ -50,17 +55,25 @@ func (service *Service) GenerateToken(data *portainer.TokenData) (string, error)
|
|||
return signedToken, nil
|
||||
}
|
||||
|
||||
// VerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
|
||||
func (service *Service) VerifyToken(token string) error {
|
||||
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||
// ParseAndVerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
|
||||
func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, error) {
|
||||
parsedToken, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
return nil, msg
|
||||
}
|
||||
return service.secret, nil
|
||||
})
|
||||
if err != nil || parsedToken == nil || !parsedToken.Valid {
|
||||
return portainer.ErrInvalidJWTToken
|
||||
if err == nil && parsedToken != nil {
|
||||
if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: portainer.UserID(cl.UserID),
|
||||
Username: cl.Username,
|
||||
Role: portainer.UserRole(cl.Role),
|
||||
}
|
||||
return tokenData, nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return nil, portainer.ErrInvalidJWTToken
|
||||
}
|
||||
|
|
108
api/portainer.go
108
api/portainer.go
|
@ -41,13 +41,24 @@ type (
|
|||
|
||||
// User represent a user account.
|
||||
User struct {
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
ID UserID `json:"Id"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
Role UserRole `json:"Role"`
|
||||
}
|
||||
|
||||
// UserID represents a user identifier
|
||||
UserID int
|
||||
|
||||
// UserRole represents the role of a user. It can be either an administrator
|
||||
// or a regular user.
|
||||
UserRole int
|
||||
|
||||
// TokenData represents the data embedded in a JWT token.
|
||||
TokenData struct {
|
||||
ID UserID
|
||||
Username string
|
||||
Role UserRole
|
||||
}
|
||||
|
||||
// EndpointID represents an endpoint identifier.
|
||||
|
@ -56,15 +67,31 @@ type (
|
|||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it.
|
||||
Endpoint struct {
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
TLS bool `json:"TLS"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
TLS bool `json:"TLS"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
}
|
||||
|
||||
// ResourceControl represent a reference to a Docker resource with specific controls
|
||||
ResourceControl struct {
|
||||
OwnerID UserID `json:"OwnerId"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
||||
}
|
||||
|
||||
// ResourceControlType represents a type of resource control.
|
||||
// Can be one of: container, service or volume.
|
||||
ResourceControlType int
|
||||
|
||||
// ResourceAccessLevel represents the level of control associated to a resource for a specific owner.
|
||||
// Can be one of: full, restricted, limited.
|
||||
ResourceAccessLevel int
|
||||
|
||||
// TLSFileType represents a type of TLS file required to connect to a Docker endpoint.
|
||||
// It can be either a TLS CA file, a TLS certificate file or a TLS key file.
|
||||
TLSFileType int
|
||||
|
@ -79,32 +106,49 @@ type (
|
|||
DataStore interface {
|
||||
Open() error
|
||||
Close() error
|
||||
MigrateData() error
|
||||
}
|
||||
|
||||
// Server defines the interface to serve the data.
|
||||
// Server defines the interface to serve the API.
|
||||
Server interface {
|
||||
Start() error
|
||||
}
|
||||
|
||||
// UserService represents a service for managing users.
|
||||
// UserService represents a service for managing user data.
|
||||
UserService interface {
|
||||
User(username string) (*User, error)
|
||||
UpdateUser(user *User) error
|
||||
User(ID UserID) (*User, error)
|
||||
UserByUsername(username string) (*User, error)
|
||||
Users() ([]User, error)
|
||||
UsersByRole(role UserRole) ([]User, error)
|
||||
CreateUser(user *User) error
|
||||
UpdateUser(ID UserID, user *User) error
|
||||
DeleteUser(ID UserID) error
|
||||
}
|
||||
|
||||
// EndpointService represents a service for managing endpoints.
|
||||
// EndpointService represents a service for managing endpoint data.
|
||||
EndpointService interface {
|
||||
Endpoint(ID EndpointID) (*Endpoint, error)
|
||||
Endpoints() ([]Endpoint, error)
|
||||
CreateEndpoint(endpoint *Endpoint) error
|
||||
UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error
|
||||
DeleteEndpoint(ID EndpointID) error
|
||||
GetActive() (*Endpoint, error)
|
||||
SetActive(endpoint *Endpoint) error
|
||||
DeleteActive() error
|
||||
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
|
||||
}
|
||||
|
||||
// VersionService represents a service for managing version data.
|
||||
VersionService interface {
|
||||
DBVersion() (int, error)
|
||||
StoreDBVersion(version int) error
|
||||
}
|
||||
|
||||
// ResourceControlService represents a service for managing resource control data.
|
||||
ResourceControlService interface {
|
||||
ResourceControl(resourceID string, rcType ResourceControlType) (*ResourceControl, error)
|
||||
ResourceControls(rcType ResourceControlType) ([]ResourceControl, error)
|
||||
CreateResourceControl(resourceID string, rc *ResourceControl, rcType ResourceControlType) error
|
||||
DeleteResourceControl(resourceID string, rcType ResourceControlType) error
|
||||
}
|
||||
|
||||
// CryptoService represents a service for encrypting/hashing data.
|
||||
CryptoService interface {
|
||||
Hash(data string) (string, error)
|
||||
|
@ -114,7 +158,7 @@ type (
|
|||
// JWTService represents a service for managing JWT tokens.
|
||||
JWTService interface {
|
||||
GenerateToken(data *TokenData) (string, error)
|
||||
VerifyToken(token string) error
|
||||
ParseAndVerifyToken(token string) (*TokenData, error)
|
||||
}
|
||||
|
||||
// FileService represents a service for managing files.
|
||||
|
@ -131,8 +175,10 @@ type (
|
|||
)
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of portainer API.
|
||||
// APIVersion is the version number of Portainer API.
|
||||
APIVersion = "1.11.4"
|
||||
// DBVersion is the version number of Portainer database.
|
||||
DBVersion = 1
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -143,3 +189,27 @@ const (
|
|||
// TLSFileKey represents a TLS key file.
|
||||
TLSFileKey
|
||||
)
|
||||
|
||||
const (
|
||||
_ UserRole = iota
|
||||
// AdministratorRole represents an administrator user role
|
||||
AdministratorRole
|
||||
// StandardUserRole represents a regular user role
|
||||
StandardUserRole
|
||||
)
|
||||
|
||||
const (
|
||||
_ ResourceControlType = iota
|
||||
// ContainerResourceControl represents a resource control for a container
|
||||
ContainerResourceControl
|
||||
// ServiceResourceControl represents a resource control for a service
|
||||
ServiceResourceControl
|
||||
// VolumeResourceControl represents a resource control for a volume
|
||||
VolumeResourceControl
|
||||
)
|
||||
|
||||
const (
|
||||
_ ResourceAccessLevel = iota
|
||||
// RestrictedResourceAccessLevel represents a restricted access level on a resource (private ownership)
|
||||
RestrictedResourceAccessLevel
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue