Merge branch 'release/1.22.1'

pull/3219/head 1.22.1
Anthony Lapenna 2019-10-11 10:40:52 +13:00
commit 80ad5079f7
181 changed files with 3701 additions and 949 deletions

1
.github/stale.yml vendored
View File

@ -15,6 +15,7 @@ issues:
- kind/feature - kind/feature
- kind/question - kind/question
- kind/style - kind/style
- kind/workaround
- bug/need-confirmation - bug/need-confirmation
- bug/confirmed - bug/confirmed
- status/discuss - status/discuss

View File

@ -11,7 +11,7 @@
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters). **_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too). **_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage your all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*. **_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*.
## Demo ## Demo
@ -45,6 +45,10 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
* Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new). * Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
* Want to help us build **_portainer_**? Follow our [contribution guidelines](https://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get! * Want to help us build **_portainer_**? Follow our [contribution guidelines](https://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get!
## Security
* Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## Limitations ## Limitations
**_Portainer_** has full support for the following Docker versions: **_Portainer_** has full support for the following Docker versions:

419
api/authorizations.go Normal file
View File

@ -0,0 +1,419 @@
package portainer
// AuthorizationService represents a service used to
// update authorizations associated to a user or team.
type AuthorizationService struct {
endpointService EndpointService
endpointGroupService EndpointGroupService
registryService RegistryService
roleService RoleService
teamMembershipService TeamMembershipService
userService UserService
}
// AuthorizationServiceParameters are the required parameters
// used to create a new AuthorizationService.
type AuthorizationServiceParameters struct {
EndpointService EndpointService
EndpointGroupService EndpointGroupService
RegistryService RegistryService
RoleService RoleService
TeamMembershipService TeamMembershipService
UserService UserService
}
// NewAuthorizationService returns a point to a new AuthorizationService instance.
func NewAuthorizationService(parameters *AuthorizationServiceParameters) *AuthorizationService {
return &AuthorizationService{
endpointService: parameters.EndpointService,
endpointGroupService: parameters.EndpointGroupService,
registryService: parameters.RegistryService,
roleService: parameters.RoleService,
teamMembershipService: parameters.TeamMembershipService,
userService: parameters.UserService,
}
}
// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
func DefaultPortainerAuthorizations() Authorizations {
return map[Authorization]bool{
OperationPortainerDockerHubInspect: true,
OperationPortainerEndpointGroupList: true,
OperationPortainerEndpointList: true,
OperationPortainerEndpointInspect: true,
OperationPortainerEndpointExtensionAdd: true,
OperationPortainerEndpointExtensionRemove: true,
OperationPortainerExtensionList: true,
OperationPortainerMOTD: true,
OperationPortainerRegistryList: true,
OperationPortainerRegistryInspect: true,
OperationPortainerTeamList: true,
OperationPortainerTemplateList: true,
OperationPortainerTemplateInspect: true,
OperationPortainerUserList: true,
OperationPortainerUserInspect: true,
OperationPortainerUserMemberships: true,
}
}
// UpdateVolumeBrowsingAuthorizations will update all the volume browsing authorizations for each role (except endpoint administrator)
// based on the specified removeAuthorizations parameter. If removeAuthorizations is set to true, all
// the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations
// will be reset based for each role.
func (service AuthorizationService) UpdateVolumeBrowsingAuthorizations(remove bool) error {
roles, err := service.roleService.Roles()
if err != nil {
return err
}
for _, role := range roles {
// all roles except endpoint administrator
if role.ID != RoleID(1) {
updateRoleVolumeBrowsingAuthorizations(&role, remove)
err := service.roleService.UpdateRole(role.ID, &role)
if err != nil {
return err
}
}
}
return nil
}
func updateRoleVolumeBrowsingAuthorizations(role *Role, removeAuthorizations bool) {
if !removeAuthorizations {
delete(role.Authorizations, OperationDockerAgentBrowseDelete)
delete(role.Authorizations, OperationDockerAgentBrowseGet)
delete(role.Authorizations, OperationDockerAgentBrowseList)
delete(role.Authorizations, OperationDockerAgentBrowsePut)
delete(role.Authorizations, OperationDockerAgentBrowseRename)
return
}
role.Authorizations[OperationDockerAgentBrowseGet] = true
role.Authorizations[OperationDockerAgentBrowseList] = true
// Standard-user
if role.ID == RoleID(3) {
role.Authorizations[OperationDockerAgentBrowseDelete] = true
role.Authorizations[OperationDockerAgentBrowsePut] = true
role.Authorizations[OperationDockerAgentBrowseRename] = true
}
}
// RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team
func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) error {
endpoints, err := service.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
for policyTeamID := range endpoint.TeamAccessPolicies {
if policyTeamID == teamID {
delete(endpoint.TeamAccessPolicies, policyTeamID)
err := service.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
break
}
}
}
endpointGroups, err := service.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
for policyTeamID := range endpointGroup.TeamAccessPolicies {
if policyTeamID == teamID {
delete(endpointGroup.TeamAccessPolicies, policyTeamID)
err := service.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
break
}
}
}
registries, err := service.registryService.Registries()
if err != nil {
return err
}
for _, registry := range registries {
for policyTeamID := range registry.TeamAccessPolicies {
if policyTeamID == teamID {
delete(registry.TeamAccessPolicies, policyTeamID)
err := service.registryService.UpdateRegistry(registry.ID, &registry)
if err != nil {
return err
}
break
}
}
}
return nil
}
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) error {
endpoints, err := service.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
for policyUserID := range endpoint.UserAccessPolicies {
if policyUserID == userID {
delete(endpoint.UserAccessPolicies, policyUserID)
err := service.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
break
}
}
}
endpointGroups, err := service.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
for policyUserID := range endpointGroup.UserAccessPolicies {
if policyUserID == userID {
delete(endpointGroup.UserAccessPolicies, policyUserID)
err := service.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
break
}
}
}
registries, err := service.registryService.Registries()
if err != nil {
return err
}
for _, registry := range registries {
for policyUserID := range registry.UserAccessPolicies {
if policyUserID == userID {
delete(registry.UserAccessPolicies, policyUserID)
err := service.registryService.UpdateRegistry(registry.ID, &registry)
if err != nil {
return err
}
break
}
}
}
return nil
}
// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users.
func (service *AuthorizationService) UpdateUsersAuthorizations() error {
users, err := service.userService.Users()
if err != nil {
return err
}
for _, user := range users {
err := service.updateUserAuthorizations(user.ID)
if err != nil {
return err
}
}
return nil
}
func (service *AuthorizationService) updateUserAuthorizations(userID UserID) error {
user, err := service.userService.User(userID)
if err != nil {
return err
}
endpointAuthorizations, err := service.getAuthorizations(user)
if err != nil {
return err
}
user.EndpointAuthorizations = endpointAuthorizations
return service.userService.UpdateUser(userID, user)
}
func (service *AuthorizationService) getAuthorizations(user *User) (EndpointAuthorizations, error) {
endpointAuthorizations := EndpointAuthorizations{}
if user.Role == AdministratorRole {
return endpointAuthorizations, nil
}
userMemberships, err := service.teamMembershipService.TeamMembershipsByUserID(user.ID)
if err != nil {
return endpointAuthorizations, err
}
endpoints, err := service.endpointService.Endpoints()
if err != nil {
return endpointAuthorizations, err
}
endpointGroups, err := service.endpointGroupService.EndpointGroups()
if err != nil {
return endpointAuthorizations, err
}
roles, err := service.roleService.Roles()
if err != nil {
return endpointAuthorizations, err
}
endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships)
return endpointAuthorizations, nil
}
func getUserEndpointAuthorizations(user *User, endpoints []Endpoint, endpointGroups []EndpointGroup, roles []Role, userMemberships []TeamMembership) EndpointAuthorizations {
endpointAuthorizations := make(EndpointAuthorizations)
groupUserAccessPolicies := map[EndpointGroupID]UserAccessPolicies{}
groupTeamAccessPolicies := map[EndpointGroupID]TeamAccessPolicies{}
for _, endpointGroup := range endpointGroups {
groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies
groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies
}
for _, endpoint := range endpoints {
authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
authorizations = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
}
}
return endpointAuthorizations
}
func getAuthorizationsFromUserEndpointPolicy(user *User, endpoint *Endpoint, roles []Role) Authorizations {
policyRoles := make([]RoleID, 0)
policy, ok := endpoint.UserAccessPolicies[user.ID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromUserEndpointGroupPolicy(user *User, endpoint *Endpoint, roles []Role, groupAccessPolicies map[EndpointGroupID]UserAccessPolicies) Authorizations {
policyRoles := make([]RoleID, 0)
policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromTeamEndpointPolicies(memberships []TeamMembership, endpoint *Endpoint, roles []Role) Authorizations {
policyRoles := make([]RoleID, 0)
for _, membership := range memberships {
policy, ok := endpoint.TeamAccessPolicies[membership.TeamID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []TeamMembership, endpoint *Endpoint, roles []Role, groupAccessPolicies map[EndpointGroupID]TeamAccessPolicies) Authorizations {
policyRoles := make([]RoleID, 0)
for _, membership := range memberships {
policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromRoles(roleIdentifiers []RoleID, roles []Role) Authorizations {
var roleAuthorizations []Authorizations
for _, id := range roleIdentifiers {
for _, role := range roles {
if role.ID == id {
roleAuthorizations = append(roleAuthorizations, role.Authorizations)
break
}
}
}
processedAuthorizations := make(Authorizations)
if len(roleAuthorizations) > 0 {
processedAuthorizations = roleAuthorizations[0]
for idx, authorizations := range roleAuthorizations {
if idx == 0 {
continue
}
processedAuthorizations = mergeAuthorizations(processedAuthorizations, authorizations)
}
}
return processedAuthorizations
}
func mergeAuthorizations(a, b Authorizations) Authorizations {
c := make(map[Authorization]bool)
for k := range b {
if _, ok := a[k]; ok {
c[k] = true
}
}
return c
}

View File

@ -124,8 +124,11 @@ func (store *Store) MigrateData() error {
ExtensionService: store.ExtensionService, ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService, RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService, ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService, SettingsService: store.SettingsService,
StackService: store.StackService, StackService: store.StackService,
TeamMembershipService: store.TeamMembershipService,
TemplateService: store.TemplateService, TemplateService: store.TemplateService,
UserService: store.UserService, UserService: store.UserService,
VersionService: store.VersionService, VersionService: store.VersionService,

View File

@ -218,8 +218,6 @@ func (store *Store) Init() error {
portainer.OperationDockerAgentPing: true, portainer.OperationDockerAgentPing: true,
portainer.OperationDockerAgentList: true, portainer.OperationDockerAgentList: true,
portainer.OperationDockerAgentHostInfo: true, portainer.OperationDockerAgentHostInfo: true,
portainer.OperationDockerAgentBrowseGet: true,
portainer.OperationDockerAgentBrowseList: true,
portainer.OperationPortainerStackList: true, portainer.OperationPortainerStackList: true,
portainer.OperationPortainerStackInspect: true, portainer.OperationPortainerStackInspect: true,
portainer.OperationPortainerStackFile: true, portainer.OperationPortainerStackFile: true,
@ -342,11 +340,6 @@ func (store *Store) Init() error {
portainer.OperationDockerAgentPing: true, portainer.OperationDockerAgentPing: true,
portainer.OperationDockerAgentList: true, portainer.OperationDockerAgentList: true,
portainer.OperationDockerAgentHostInfo: true, portainer.OperationDockerAgentHostInfo: true,
portainer.OperationDockerAgentBrowseDelete: true,
portainer.OperationDockerAgentBrowseGet: true,
portainer.OperationDockerAgentBrowseList: true,
portainer.OperationDockerAgentBrowsePut: true,
portainer.OperationDockerAgentBrowseRename: true,
portainer.OperationDockerAgentUndefined: true, portainer.OperationDockerAgentUndefined: true,
portainer.OperationPortainerResourceControlCreate: true, portainer.OperationPortainerResourceControlCreate: true,
portainer.OperationPortainerResourceControlUpdate: true, portainer.OperationPortainerResourceControlUpdate: true,
@ -413,8 +406,6 @@ func (store *Store) Init() error {
portainer.OperationDockerAgentPing: true, portainer.OperationDockerAgentPing: true,
portainer.OperationDockerAgentList: true, portainer.OperationDockerAgentList: true,
portainer.OperationDockerAgentHostInfo: true, portainer.OperationDockerAgentHostInfo: true,
portainer.OperationDockerAgentBrowseGet: true,
portainer.OperationDockerAgentBrowseList: true,
portainer.OperationPortainerStackList: true, portainer.OperationPortainerStackList: true,
portainer.OperationPortainerStackInspect: true, portainer.OperationPortainerStackInspect: true,
portainer.OperationPortainerStackFile: true, portainer.OperationPortainerStackFile: true,

View File

@ -0,0 +1,67 @@
package migrator
import (
"strings"
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) updateUsersToDBVersion20() error {
authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
EndpointService: m.endpointService,
EndpointGroupService: m.endpointGroupService,
RegistryService: m.registryService,
RoleService: m.roleService,
TeamMembershipService: m.teamMembershipService,
UserService: m.userService,
}
authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)
return authorizationService.UpdateUsersAuthorizations()
}
func (m *Migrator) updateSettingsToDBVersion20() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowVolumeBrowserForRegularUsers = false
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) updateSchedulesToDBVersion20() error {
legacySchedules, err := m.scheduleService.Schedules()
if err != nil {
return err
}
for _, schedule := range legacySchedules {
if schedule.JobType == portainer.ScriptExecutionJobType {
if schedule.CronExpression == "0 0 * * *" {
schedule.CronExpression = "0 * * * *"
} else if schedule.CronExpression == "0 0 0/2 * *" {
schedule.CronExpression = "0 */2 * * *"
} else if schedule.CronExpression == "0 0 0 * *" {
schedule.CronExpression = "0 0 * * *"
} else {
revisedCronExpression := strings.Split(schedule.CronExpression, " ")
if len(revisedCronExpression) == 5 {
continue
}
revisedCronExpression = revisedCronExpression[1:]
schedule.CronExpression = strings.Join(revisedCronExpression, " ")
}
err := m.scheduleService.UpdateSchedule(schedule.ID, &schedule)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -8,8 +8,11 @@ import (
"github.com/portainer/portainer/api/bolt/extension" "github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/registry" "github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol" "github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
"github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings" "github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/stack" "github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/template" "github.com/portainer/portainer/api/bolt/template"
"github.com/portainer/portainer/api/bolt/user" "github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version" "github.com/portainer/portainer/api/bolt/version"
@ -25,8 +28,11 @@ type (
extensionService *extension.Service extensionService *extension.Service
registryService *registry.Service registryService *registry.Service
resourceControlService *resourcecontrol.Service resourceControlService *resourcecontrol.Service
roleService *role.Service
scheduleService *schedule.Service
settingsService *settings.Service settingsService *settings.Service
stackService *stack.Service stackService *stack.Service
teamMembershipService *teammembership.Service
templateService *template.Service templateService *template.Service
userService *user.Service userService *user.Service
versionService *version.Service versionService *version.Service
@ -42,8 +48,11 @@ type (
ExtensionService *extension.Service ExtensionService *extension.Service
RegistryService *registry.Service RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service ResourceControlService *resourcecontrol.Service
RoleService *role.Service
ScheduleService *schedule.Service
SettingsService *settings.Service SettingsService *settings.Service
StackService *stack.Service StackService *stack.Service
TeamMembershipService *teammembership.Service
TemplateService *template.Service TemplateService *template.Service
UserService *user.Service UserService *user.Service
VersionService *version.Service VersionService *version.Service
@ -61,7 +70,10 @@ func NewMigrator(parameters *Parameters) *Migrator {
extensionService: parameters.ExtensionService, extensionService: parameters.ExtensionService,
registryService: parameters.RegistryService, registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService, resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,
scheduleService: parameters.ScheduleService,
settingsService: parameters.SettingsService, settingsService: parameters.SettingsService,
teamMembershipService: parameters.TeamMembershipService,
templateService: parameters.TemplateService, templateService: parameters.TemplateService,
stackService: parameters.StackService, stackService: parameters.StackService,
userService: parameters.UserService, userService: parameters.UserService,
@ -257,5 +269,23 @@ func (m *Migrator) Migrate() error {
} }
} }
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return err
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return err
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion) return m.versionService.StoreDBVersion(portainer.DBVersion)
} }

View File

@ -66,18 +66,24 @@ func (service *Service) Roles() ([]portainer.Role, error) {
} }
// CreateRole creates a new Role. // CreateRole creates a new Role.
func (service *Service) CreateRole(set *portainer.Role) error { func (service *Service) CreateRole(role *portainer.Role) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
set.ID = portainer.RoleID(id) role.ID = portainer.RoleID(id)
data, err := internal.MarshalObject(set) data, err := internal.MarshalObject(role)
if err != nil { if err != nil {
return err return err
} }
return bucket.Put(internal.Itob(int(set.ID)), data) return bucket.Put(internal.Itob(int(role.ID)), data)
}) })
} }
// UpdateRole updates a role.
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, role)
}

View File

@ -102,7 +102,7 @@ func initLDAPService() portainer.LDAPService {
} }
func initGitService() portainer.GitService { func initGitService() portainer.GitService {
return &git.Service{} return git.NewService()
} }
func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory { func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
@ -271,6 +271,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
OAuthSettings: portainer.OAuthSettings{}, OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true, AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true, AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false, EnableHostManagementFeatures: false,
SnapshotInterval: *flags.SnapshotInterval, SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
@ -499,8 +500,13 @@ func initExtensionManager(fileService portainer.FileService, extensionService po
log.Printf("Unable to enable extension: %s [extension: %s]", err.Error(), extension.Name) log.Printf("Unable to enable extension: %s [extension: %s]", err.Error(), extension.Name)
extension.Enabled = false extension.Enabled = false
extension.License.Valid = false extension.License.Valid = false
extensionService.Persist(&extension)
} }
err = extensionService.Persist(&extension)
if err != nil {
return nil, err
}
} }
return extensionManager, nil return extensionManager, nil
@ -618,7 +624,7 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
adminPasswordHash, err = cryptoService.Hash(string(content)) adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -635,26 +641,10 @@ func main() {
if len(users) == 0 { if len(users) == 0 {
log.Printf("Creating admin user with password hash %s", adminPasswordHash) log.Printf("Creating admin user with password hash %s", adminPasswordHash)
user := &portainer.User{ user := &portainer.User{
Username: "admin", Username: "admin",
Role: portainer.AdministratorRole, Role: portainer.AdministratorRole,
Password: adminPasswordHash, Password: adminPasswordHash,
PortainerAuthorizations: map[portainer.Authorization]bool{ PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
portainer.OperationPortainerDockerHubInspect: true,
portainer.OperationPortainerEndpointGroupList: true,
portainer.OperationPortainerEndpointList: true,
portainer.OperationPortainerEndpointInspect: true,
portainer.OperationPortainerEndpointExtensionAdd: true,
portainer.OperationPortainerEndpointExtensionRemove: true,
portainer.OperationPortainerExtensionList: true,
portainer.OperationPortainerMOTD: true,
portainer.OperationPortainerRegistryList: true,
portainer.OperationPortainerRegistryInspect: true,
portainer.OperationPortainerTeamList: true,
portainer.OperationPortainerTemplateList: true,
portainer.OperationPortainerTemplateInspect: true,
portainer.OperationPortainerUserList: true,
portainer.OperationPortainerUserMemberships: true,
},
} }
err := store.UserService.CreateUser(user) err := store.UserService.CreateUser(user)
if err != nil { if err != nil {

View File

@ -19,7 +19,8 @@ func NewJobScheduler() *JobScheduler {
// ScheduleJob schedules the execution of a job via a runner // ScheduleJob schedules the execution of a job via a runner
func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error { func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error {
return scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner) _, err := scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner)
return err
} }
// UpdateSystemJobSchedule updates the first occurence of the specified // UpdateSystemJobSchedule updates the first occurence of the specified
@ -35,7 +36,7 @@ func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType
for _, entry := range cronEntries { for _, entry := range cronEntries {
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType { if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType {
err := newCron.AddJob(newCronExpression, entry.Job) _, err := newCron.AddJob(newCronExpression, entry.Job)
if err != nil { if err != nil {
return err return err
} }
@ -69,7 +70,7 @@ func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) err
jobRunner = entry.Job jobRunner = entry.Job
} }
err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner) _, err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
if err != nil { if err != nil {
return err return err
} }

View File

@ -2,6 +2,7 @@ package docker
import ( import (
"context" "context"
"log"
"time" "time"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -10,7 +11,7 @@ import (
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
) )
func snapshot(cli *client.Client) (*portainer.Snapshot, error) { func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
_, err := cli.Ping(context.Background()) _, err := cli.Ping(context.Background())
if err != nil { if err != nil {
return nil, err return nil, err
@ -22,44 +23,44 @@ func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
err = snapshotInfo(snapshot, cli) err = snapshotInfo(snapshot, cli)
if err != nil { if err != nil {
return nil, err log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine information] [endpoint: %s] [err: %s]", endpoint.Name, err)
} }
if snapshot.Swarm { if snapshot.Swarm {
err = snapshotSwarmServices(snapshot, cli) err = snapshotSwarmServices(snapshot, cli)
if err != nil { if err != nil {
return nil, err log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm services] [endpoint: %s] [err: %s]", endpoint.Name, err)
} }
err = snapshotNodes(snapshot, cli) err = snapshotNodes(snapshot, cli)
if err != nil { if err != nil {
return nil, err log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm nodes] [endpoint: %s] [err: %s]", endpoint.Name, err)
} }
} }
err = snapshotContainers(snapshot, cli) err = snapshotContainers(snapshot, cli)
if err != nil { if err != nil {
return nil, err log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot containers] [endpoint: %s] [err: %s]", endpoint.Name, err)
} }
err = snapshotImages(snapshot, cli) err = snapshotImages(snapshot, cli)
if err != nil { if err != nil {
return nil, err log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot images] [endpoint: %s] [err: %s]", endpoint.Name, err)
} }
err = snapshotVolumes(snapshot, cli) err = snapshotVolumes(snapshot, cli)
if err != nil { if err != nil {
return nil, err log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot volumes] [endpoint: %s] [err: %s]", endpoint.Name, err)
} }
err = snapshotNetworks(snapshot, cli) err = snapshotNetworks(snapshot, cli)
if err != nil { if err != nil {
return nil, err log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot networks] [endpoint: %s] [err: %s]", endpoint.Name, err)
} }
err = snapshotVersion(snapshot, cli) err = snapshotVersion(snapshot, cli)
if err != nil { if err != nil {
return nil, err log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine version] [endpoint: %s] [err: %s]", endpoint.Name, err)
} }
snapshot.Time = time.Now().Unix() snapshot.Time = time.Now().Unix()

View File

@ -24,5 +24,5 @@ func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*p
} }
defer cli.Close() defer cli.Close()
return snapshot(cli) return snapshot(cli, endpoint)
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"log"
"os/exec" "os/exec"
"path" "path"
"runtime" "runtime"
@ -193,6 +194,7 @@ func validateLicense(binaryPath, licenseKey string) ([]string, error) {
err := licenseCheckProcess.Run() err := licenseCheckProcess.Run()
if err != nil { if err != nil {
log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
return nil, errors.New("Invalid extension license key") return nil, errors.New("Invalid extension license key")
} }

View File

@ -1,21 +1,37 @@
package git package git
import ( import (
"crypto/tls"
"net/http"
"net/url" "net/url"
"strings" "strings"
"time"
"gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/transport/client"
githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
) )
// Service represents a service for managing Git. // Service represents a service for managing Git.
type Service struct{} type Service struct {
httpsCli *http.Client
}
// NewService initializes a new service. // NewService initializes a new service.
func NewService(dataStorePath string) (*Service, error) { func NewService() *Service {
service := &Service{} httpsCli := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: 300 * time.Second,
}
return service, nil client.InstallProtocol("https", githttp.NewClient(httpsCli))
return &Service{
httpsCli: httpsCli,
}
} }
// ClonePublicRepository clones a public git repository using the specified URL in the specified // ClonePublicRepository clones a public git repository using the specified URL in the specified
@ -32,7 +48,7 @@ func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, refer
return cloneRepository(repositoryURL, referenceName, destination) return cloneRepository(repositoryURL, referenceName, destination)
} }
func cloneRepository(repositoryURL, referenceName string, destination string) error { func cloneRepository(repositoryURL, referenceName, destination string) error {
options := &git.CloneOptions{ options := &git.CloneOptions{
URL: repositoryURL, URL: repositoryURL,
} }

View File

@ -52,7 +52,7 @@ func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
} }
if err == portainer.ErrObjectNotFound && settings.AuthenticationMethod == portainer.AuthenticationInternal { if err == portainer.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized} return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
} }
@ -98,25 +98,9 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
} }
user := &portainer.User{ user := &portainer.User{
Username: username, Username: username,
Role: portainer.StandardUserRole, Role: portainer.StandardUserRole,
PortainerAuthorizations: map[portainer.Authorization]bool{ PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
portainer.OperationPortainerDockerHubInspect: true,
portainer.OperationPortainerEndpointGroupList: true,
portainer.OperationPortainerEndpointList: true,
portainer.OperationPortainerEndpointInspect: true,
portainer.OperationPortainerEndpointExtensionAdd: true,
portainer.OperationPortainerEndpointExtensionRemove: true,
portainer.OperationPortainerExtensionList: true,
portainer.OperationPortainerMOTD: true,
portainer.OperationPortainerRegistryList: true,
portainer.OperationPortainerRegistryInspect: true,
portainer.OperationPortainerTeamList: true,
portainer.OperationPortainerTemplateList: true,
portainer.OperationPortainerTemplateInspect: true,
portainer.OperationPortainerUserList: true,
portainer.OperationPortainerUserMemberships: true,
},
} }
err = handler.UserService.CreateUser(user) err = handler.UserService.CreateUser(user)
@ -134,59 +118,14 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError { func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
tokenData := &portainer.TokenData{ tokenData := &portainer.TokenData{
ID: user.ID, ID: user.ID,
Username: user.Username, Username: user.Username,
Role: user.Role, Role: user.Role,
PortainerAuthorizations: user.PortainerAuthorizations,
} }
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err == portainer.ErrObjectNotFound {
return handler.persistAndWriteToken(w, tokenData)
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
}
endpointAuthorizations, err := handler.getAuthorizations(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve authorizations associated to the user", err}
}
tokenData.EndpointAuthorizations = endpointAuthorizations
return handler.persistAndWriteToken(w, tokenData) return handler.persistAndWriteToken(w, tokenData)
} }
func (handler *Handler) getAuthorizations(user *portainer.User) (portainer.EndpointAuthorizations, error) {
endpointAuthorizations := portainer.EndpointAuthorizations{}
if user.Role == portainer.AdministratorRole {
return endpointAuthorizations, nil
}
userMemberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(user.ID)
if err != nil {
return endpointAuthorizations, err
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return endpointAuthorizations, err
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return endpointAuthorizations, err
}
roles, err := handler.RoleService.Roles()
if err != nil {
return endpointAuthorizations, err
}
endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships)
return endpointAuthorizations, nil
}
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError { func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
token, err := handler.JWTService.GenerateToken(tokenData) token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil { if err != nil {

View File

@ -111,25 +111,9 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
if user == nil { if user == nil {
user = &portainer.User{ user = &portainer.User{
Username: username, Username: username,
Role: portainer.StandardUserRole, Role: portainer.StandardUserRole,
PortainerAuthorizations: map[portainer.Authorization]bool{ PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
portainer.OperationPortainerDockerHubInspect: true,
portainer.OperationPortainerEndpointGroupList: true,
portainer.OperationPortainerEndpointList: true,
portainer.OperationPortainerEndpointInspect: true,
portainer.OperationPortainerEndpointExtensionAdd: true,
portainer.OperationPortainerEndpointExtensionRemove: true,
portainer.OperationPortainerExtensionList: true,
portainer.OperationPortainerMOTD: true,
portainer.OperationPortainerRegistryList: true,
portainer.OperationPortainerRegistryInspect: true,
portainer.OperationPortainerTeamList: true,
portainer.OperationPortainerTemplateList: true,
portainer.OperationPortainerTemplateInspect: true,
portainer.OperationPortainerUserList: true,
portainer.OperationPortainerUserMemberships: true,
},
} }
err = handler.UserService.CreateUser(user) err = handler.UserService.CreateUser(user)

View File

@ -1,122 +0,0 @@
package auth
import portainer "github.com/portainer/portainer/api"
func getUserEndpointAuthorizations(user *portainer.User, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, roles []portainer.Role, userMemberships []portainer.TeamMembership) portainer.EndpointAuthorizations {
endpointAuthorizations := make(portainer.EndpointAuthorizations)
groupUserAccessPolicies := map[portainer.EndpointGroupID]portainer.UserAccessPolicies{}
groupTeamAccessPolicies := map[portainer.EndpointGroupID]portainer.TeamAccessPolicies{}
for _, endpointGroup := range endpointGroups {
groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies
groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies
}
for _, endpoint := range endpoints {
authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
endpointAuthorizations[endpoint.ID] = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
}
return endpointAuthorizations
}
func getAuthorizationsFromUserEndpointPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
policyRoles := make([]portainer.RoleID, 0)
policy, ok := endpoint.UserAccessPolicies[user.ID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromUserEndpointGroupPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.UserAccessPolicies) portainer.Authorizations {
policyRoles := make([]portainer.RoleID, 0)
policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromTeamEndpointPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
policyRoles := make([]portainer.RoleID, 0)
for _, membership := range memberships {
policy, ok := endpoint.TeamAccessPolicies[membership.TeamID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.TeamAccessPolicies) portainer.Authorizations {
policyRoles := make([]portainer.RoleID, 0)
for _, membership := range memberships {
policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []portainer.Role) portainer.Authorizations {
var roleAuthorizations []portainer.Authorizations
for _, id := range roleIdentifiers {
for _, role := range roles {
if role.ID == id {
roleAuthorizations = append(roleAuthorizations, role.Authorizations)
break
}
}
}
processedAuthorizations := make(portainer.Authorizations)
if len(roleAuthorizations) > 0 {
processedAuthorizations = roleAuthorizations[0]
for idx, authorizations := range roleAuthorizations {
if idx == 0 {
continue
}
processedAuthorizations = mergeAuthorizations(processedAuthorizations, authorizations)
}
}
return processedAuthorizations
}
func mergeAuthorizations(a, b portainer.Authorizations) portainer.Authorizations {
c := make(map[portainer.Authorization]bool)
for k := range b {
if _, ok := a[k]; ok {
c[k] = true
}
}
return c
}

View File

@ -25,9 +25,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/dockerhub", h.Handle("/dockerhub",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
h.Handle("/dockerhub", h.Handle("/dockerhub",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
return h return h
} }

View File

@ -37,8 +37,10 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
} }
updateAuthorizations := false
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) { if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
updateAuthorizations = true
endpoint.GroupID = portainer.EndpointGroupID(1) endpoint.GroupID = portainer.EndpointGroupID(1)
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil { if err != nil {
@ -47,5 +49,12 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
} }
} }
if updateAuthorizations {
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
return response.Empty(w) return response.Empty(w)
} }

View File

@ -2,6 +2,7 @@ package endpointgroups
import ( import (
"net/http" "net/http"
"reflect"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
@ -53,12 +54,15 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
endpointGroup.Tags = payload.Tags endpointGroup.Tags = payload.Tags
} }
if payload.UserAccessPolicies != nil { updateAuthorizations := false
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) {
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
updateAuthorizations = true
} }
if payload.TeamAccessPolicies != nil { if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
updateAuthorizations = true
} }
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup) err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
@ -66,5 +70,12 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
} }
if updateAuthorizations {
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
return response.JSON(w, endpointGroup) return response.JSON(w, endpointGroup)
} }

View File

@ -14,6 +14,7 @@ type Handler struct {
*mux.Router *mux.Router
EndpointService portainer.EndpointService EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService EndpointGroupService portainer.EndpointGroupService
AuthorizationService *portainer.AuthorizationService
} }
// NewHandler creates a handler to manage endpoint group operations. // NewHandler creates a handler to manage endpoint group operations.
@ -22,18 +23,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/endpoint_groups", h.Handle("/endpoint_groups",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
h.Handle("/endpoint_groups", h.Handle("/endpoint_groups",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
h.Handle("/endpoint_groups/{id}", h.Handle("/endpoint_groups/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
h.Handle("/endpoint_groups/{id}", h.Handle("/endpoint_groups/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
h.Handle("/endpoint_groups/{id}", h.Handle("/endpoint_groups/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}", h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupAddEndpoint))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupAddEndpoint))).Methods(http.MethodPut)
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}", h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete)
return h return h
} }

View File

@ -25,10 +25,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
requestBouncer: bouncer, requestBouncer: bouncer,
} }
h.PathPrefix("/{id}/azure").Handler( h.PathPrefix("/{id}/azure").Handler(
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
h.PathPrefix("/{id}/docker").Handler( h.PathPrefix("/{id}/docker").Handler(
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
h.PathPrefix("/{id}/storidge").Handler( h.PathPrefix("/{id}/storidge").Handler(
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI))) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
return h return h
} }

View File

@ -25,10 +25,6 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
} }
if endpoint.Type != portainer.EdgeAgentEnvironment && endpoint.Status == portainer.EndpointStatusDown {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}

View File

@ -2,12 +2,12 @@ package endpoints
import ( import (
"errors" "errors"
"log"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"runtime" "runtime"
"strconv" "strconv"
"strings"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
@ -192,9 +192,9 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
Snapshots: []portainer.Snapshot{}, Snapshots: []portainer.Snapshot{},
} }
err = handler.EndpointService.CreateEndpoint(endpoint) err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
if err != nil { if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err} return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
} }
return endpoint, nil return endpoint, nil
@ -238,9 +238,9 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
EdgeKey: edgeKey, EdgeKey: edgeKey,
} }
err = handler.EndpointService.CreateEndpoint(endpoint) err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
if err != nil { if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err} return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
} }
return endpoint, nil return endpoint, nil
@ -344,17 +344,37 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint)
snapshot, err := handler.Snapshotter.CreateSnapshot(endpoint) snapshot, err := handler.Snapshotter.CreateSnapshot(endpoint)
endpoint.Status = portainer.EndpointStatusUp endpoint.Status = portainer.EndpointStatusUp
if err != nil { if err != nil {
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) if strings.Contains(err.Error(), "Invalid request signature") {
endpoint.Status = portainer.EndpointStatusDown err = errors.New("agent already paired with another Portainer instance")
}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with endpoint", err}
} }
if snapshot != nil { if snapshot != nil {
endpoint.Snapshots = []portainer.Snapshot{*snapshot} endpoint.Snapshots = []portainer.Snapshot{*snapshot}
} }
err = handler.EndpointService.CreateEndpoint(endpoint) err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
}
return nil
}
func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error {
err := handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return err
}
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return err
}
if len(group.UserAccessPolicies) > 0 || len(group.TeamAccessPolicies) > 0 {
return handler.AuthorizationService.UpdateUsersAuthorizations()
} }
return nil return nil

View File

@ -43,5 +43,12 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
handler.ProxyManager.DeleteProxy(endpoint) handler.ProxyManager.DeleteProxy(endpoint)
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
return response.Empty(w) return response.Empty(w)
} }

View File

@ -2,6 +2,7 @@ package endpoints
import ( import (
"net/http" "net/http"
"reflect"
"strconv" "strconv"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
@ -76,12 +77,15 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.Tags = payload.Tags endpoint.Tags = payload.Tags
} }
if payload.UserAccessPolicies != nil { updateAuthorizations := false
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
endpoint.UserAccessPolicies = payload.UserAccessPolicies endpoint.UserAccessPolicies = payload.UserAccessPolicies
updateAuthorizations = true
} }
if payload.TeamAccessPolicies != nil { if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) {
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
updateAuthorizations = true
} }
if payload.Status != nil { if payload.Status != nil {
@ -173,5 +177,12 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
} }
if updateAuthorizations {
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
return response.JSON(w, endpoint) return response.JSON(w, endpoint)
} }

View File

@ -37,6 +37,7 @@ type Handler struct {
JobService portainer.JobService JobService portainer.JobService
ReverseTunnelService portainer.ReverseTunnelService ReverseTunnelService portainer.ReverseTunnelService
SettingsService portainer.SettingsService SettingsService portainer.SettingsService
AuthorizationService *portainer.AuthorizationService
} }
// NewHandler creates a handler to manage endpoint operations. // NewHandler creates a handler to manage endpoint operations.
@ -48,25 +49,25 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
} }
h.Handle("/endpoints", h.Handle("/endpoints",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
h.Handle("/endpoints/snapshot", h.Handle("/endpoints/snapshot",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
h.Handle("/endpoints", h.Handle("/endpoints",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}", h.Handle("/endpoints/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}", h.Handle("/endpoints/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}", h.Handle("/endpoints/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/extensions", h.Handle("/endpoints/{id}/extensions",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost) bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/extensions/{extensionType}", h.Handle("/endpoints/{id}/extensions/{extensionType}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete) bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/job", h.Handle("/endpoints/{id}/job",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/snapshot", h.Handle("/endpoints/{id}/snapshot",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/status", h.Handle("/endpoints/{id}/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet) bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)

View File

@ -17,6 +17,7 @@ type Handler struct {
EndpointGroupService portainer.EndpointGroupService EndpointGroupService portainer.EndpointGroupService
EndpointService portainer.EndpointService EndpointService portainer.EndpointService
RegistryService portainer.RegistryService RegistryService portainer.RegistryService
AuthorizationService *portainer.AuthorizationService
} }
// NewHandler creates a handler to manage extension operations. // NewHandler creates a handler to manage extension operations.
@ -26,15 +27,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
} }
h.Handle("/extensions", h.Handle("/extensions",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
h.Handle("/extensions", h.Handle("/extensions",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
h.Handle("/extensions/{id}", h.Handle("/extensions/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
h.Handle("/extensions/{id}", h.Handle("/extensions/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete)
h.Handle("/extensions/{id}/update", h.Handle("/extensions/{id}/update",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost)
return h return h
} }

View File

@ -1,6 +1,8 @@
package extensions package extensions
import portainer "github.com/portainer/portainer/api" import (
portainer "github.com/portainer/portainer/api"
)
func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) { func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
tmp := policies[key] tmp := policies[key]
@ -33,7 +35,6 @@ func (handler *Handler) upgradeRBACData() error {
if err != nil { if err != nil {
return err return err
} }
} }
endpoints, err := handler.EndpointService.Endpoints() endpoints, err := handler.EndpointService.Endpoints()
@ -55,5 +56,6 @@ func (handler *Handler) upgradeRBACData() error {
return err return err
} }
} }
return nil
return handler.AuthorizationService.UpdateUsersAuthorizations()
} }

View File

@ -4,6 +4,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/portainer/portainer/api/http/handler/support"
"github.com/portainer/portainer/api/http/handler/schedules" "github.com/portainer/portainer/api/http/handler/schedules"
"github.com/portainer/portainer/api/http/handler/roles" "github.com/portainer/portainer/api/http/handler/roles"
@ -48,6 +50,7 @@ type Handler struct {
SettingsHandler *settings.Handler SettingsHandler *settings.Handler
StackHandler *stacks.Handler StackHandler *stacks.Handler
StatusHandler *status.Handler StatusHandler *status.Handler
SupportHandler *support.Handler
TagHandler *tags.Handler TagHandler *tags.Handler
TeamMembershipHandler *teammemberships.Handler TeamMembershipHandler *teammemberships.Handler
TeamHandler *teams.Handler TeamHandler *teams.Handler
@ -96,6 +99,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/status"): case strings.HasPrefix(r.URL.Path, "/api/status"):
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/support"):
http.StripPrefix("/api", h.SupportHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/tags"): case strings.HasPrefix(r.URL.Path, "/api/tags"):
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/templates"): case strings.HasPrefix(r.URL.Path, "/api/templates"):

View File

@ -18,7 +18,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/motd", h.Handle("/motd",
bouncer.AuthorizedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet) bouncer.RestrictedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet)
return h return h
} }

View File

@ -33,19 +33,19 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
} }
h.Handle("/registries", h.Handle("/registries",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost)
h.Handle("/registries", h.Handle("/registries",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
h.Handle("/registries/{id}", h.Handle("/registries/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
h.Handle("/registries/{id}", h.Handle("/registries/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
h.Handle("/registries/{id}/configure", h.Handle("/registries/{id}/configure",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost)
h.Handle("/registries/{id}", h.Handle("/registries/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
h.PathPrefix("/registries/{id}/v2").Handler( h.PathPrefix("/registries/{id}/v2").Handler(
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI))) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
return h return h
} }

View File

@ -3,7 +3,6 @@ package registries
import ( import (
"net/http" "net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
@ -11,19 +10,16 @@ import (
) )
type registryUpdatePayload struct { type registryUpdatePayload struct {
Name string Name *string
URL string URL *string
Authentication bool Authentication *bool
Username string Username *string
Password string Password *string
UserAccessPolicies portainer.UserAccessPolicies UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies
} }
func (payload *registryUpdatePayload) Validate(r *http.Request) error { func (payload *registryUpdatePayload) Validate(r *http.Request) error {
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
}
return nil return nil
} }
@ -47,32 +43,41 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
} }
registries, err := handler.RegistryService.Registries() if payload.Name != nil {
if err != nil { registry.Name = *payload.Name
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
} }
for _, r := range registries {
if r.URL == payload.URL && r.ID != registry.ID { if payload.URL != nil {
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", portainer.ErrRegistryAlreadyExists} registries, err := handler.RegistryService.Registries()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
} }
for _, r := range registries {
if r.URL == *payload.URL && r.ID != registry.ID {
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
}
}
registry.URL = *payload.URL
} }
if payload.Name != "" { if payload.Authentication != nil {
registry.Name = payload.Name if *payload.Authentication {
} registry.Authentication = true
if payload.URL != "" { if payload.Username != nil {
registry.URL = payload.URL registry.Username = *payload.Username
} }
if payload.Authentication { if payload.Password != nil {
registry.Authentication = true registry.Password = *payload.Password
registry.Username = payload.Username }
registry.Password = payload.Password
} else { } else {
registry.Authentication = false registry.Authentication = false
registry.Username = "" registry.Username = ""
registry.Password = "" registry.Password = ""
}
} }
if payload.UserAccessPolicies != nil { if payload.UserAccessPolicies != nil {

View File

@ -21,11 +21,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/resource_controls", h.Handle("/resource_controls",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
h.Handle("/resource_controls/{id}", h.Handle("/resource_controls/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
h.Handle("/resource_controls/{id}", h.Handle("/resource_controls/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)
return h return h
} }

View File

@ -21,7 +21,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/roles", h.Handle("/roles",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.roleList))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.roleList))).Methods(http.MethodGet)
return h return h
} }

View File

@ -28,18 +28,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
} }
h.Handle("/schedules", h.Handle("/schedules",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet)
h.Handle("/schedules", h.Handle("/schedules",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleCreate))).Methods(http.MethodPost)
h.Handle("/schedules/{id}", h.Handle("/schedules/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet)
h.Handle("/schedules/{id}", h.Handle("/schedules/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut)
h.Handle("/schedules/{id}", h.Handle("/schedules/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
h.Handle("/schedules/{id}/file", h.Handle("/schedules/{id}/file",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet)
h.Handle("/schedules/{id}/tasks", h.Handle("/schedules/{id}/tasks",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet)
return h return h
} }

View File

@ -17,11 +17,14 @@ func hideFields(settings *portainer.Settings) {
// Handler is the HTTP handler used to handle settings operations. // Handler is the HTTP handler used to handle settings operations.
type Handler struct { type Handler struct {
*mux.Router *mux.Router
SettingsService portainer.SettingsService SettingsService portainer.SettingsService
LDAPService portainer.LDAPService LDAPService portainer.LDAPService
FileService portainer.FileService FileService portainer.FileService
JobScheduler portainer.JobScheduler JobScheduler portainer.JobScheduler
ScheduleService portainer.ScheduleService ScheduleService portainer.ScheduleService
RoleService portainer.RoleService
ExtensionService portainer.ExtensionService
AuthorizationService *portainer.AuthorizationService
} }
// NewHandler creates a handler to manage settings operations. // NewHandler creates a handler to manage settings operations.
@ -30,13 +33,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/settings", h.Handle("/settings",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
h.Handle("/settings", h.Handle("/settings",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut)
h.Handle("/settings/public", h.Handle("/settings/public",
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet) bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
h.Handle("/settings/authentication/checkLDAP", h.Handle("/settings/authentication/checkLDAP",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut)
return h return h
} }

View File

@ -14,6 +14,7 @@ type publicSettingsResponse struct {
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
ExternalTemplates bool `json:"ExternalTemplates"` ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"` OAuthLoginURI string `json:"OAuthLoginURI"`
@ -31,6 +32,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
AuthenticationMethod: settings.AuthenticationMethod, AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
ExternalTemplates: false, ExternalTemplates: false,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",

View File

@ -19,6 +19,7 @@ type settingsUpdatePayload struct {
OAuthSettings *portainer.OAuthSettings OAuthSettings *portainer.OAuthSettings
AllowBindMountsForRegularUsers *bool AllowBindMountsForRegularUsers *bool
AllowPrivilegedModeForRegularUsers *bool AllowPrivilegedModeForRegularUsers *bool
AllowVolumeBrowserForRegularUsers *bool
EnableHostManagementFeatures *bool EnableHostManagementFeatures *bool
SnapshotInterval *string SnapshotInterval *string
TemplatesURL *string TemplatesURL *string
@ -93,6 +94,12 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
} }
updateAuthorizations := false
if payload.AllowVolumeBrowserForRegularUsers != nil {
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
updateAuthorizations = true
}
if payload.EnableHostManagementFeatures != nil { if payload.EnableHostManagementFeatures != nil {
settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
} }
@ -118,9 +125,37 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err}
} }
if updateAuthorizations {
err := handler.updateVolumeBrowserSetting(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update RBAC authorizations", err}
}
}
return response.JSON(w, settings) return response.JSON(w, settings)
} }
func (handler *Handler) updateVolumeBrowserSetting(settings *portainer.Settings) error {
err := handler.AuthorizationService.UpdateVolumeBrowsingAuthorizations(settings.AllowVolumeBrowserForRegularUsers)
if err != nil {
return err
}
extension, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err != nil && err != portainer.ErrObjectNotFound {
return err
}
if extension != nil {
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return err
}
}
return nil
}
func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error { func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error {
settings.SnapshotInterval = snapshotInterval settings.SnapshotInterval = snapshotInterval

View File

@ -1,7 +1,9 @@
package stacks package stacks
import ( import (
"errors"
"net/http" "net/http"
"path"
"strconv" "strconv"
"strings" "strings"
@ -238,7 +240,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
} }
stackFolder := strconv.Itoa(int(stack.ID)) stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, payload.StackFileContent)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
} }
@ -271,6 +273,7 @@ type composeStackDeploymentConfig struct {
endpoint *portainer.Endpoint endpoint *portainer.Endpoint
dockerhub *portainer.DockerHub dockerhub *portainer.DockerHub
registries []portainer.Registry registries []portainer.Registry
isAdmin bool
} }
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) { func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
@ -295,6 +298,7 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
endpoint: endpoint, endpoint: endpoint,
dockerhub: dockerhub, dockerhub: dockerhub,
registries: filteredRegistries, registries: filteredRegistries,
isAdmin: securityContext.IsAdmin,
} }
return config, nil return config, nil
@ -306,12 +310,34 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
// clean it. Hence the use of the mutex. // clean it. Hence the use of the mutex.
// We should contribute to libcompose to support authentication without using the config.json file. // We should contribute to libcompose to support authentication without using the config.json file.
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error { func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
settings, err := handler.SettingsService.Settings()
if err != nil {
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
valid, err := handler.isValidStackFile(stackContent)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
}
}
handler.stackCreationMutex.Lock() handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock() defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint) handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
err := handler.ComposeStackManager.Up(config.stack, config.endpoint) err = handler.ComposeStackManager.Up(config.stack, config.endpoint)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,7 +1,9 @@
package stacks package stacks
import ( import (
"errors"
"net/http" "net/http"
"path"
"strconv" "strconv"
"strings" "strings"
@ -290,6 +292,7 @@ type swarmStackDeploymentConfig struct {
dockerhub *portainer.DockerHub dockerhub *portainer.DockerHub
registries []portainer.Registry registries []portainer.Registry
prune bool prune bool
isAdmin bool
} }
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) { func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
@ -315,18 +318,41 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
dockerhub: dockerhub, dockerhub: dockerhub,
registries: filteredRegistries, registries: filteredRegistries,
prune: prune, prune: prune,
isAdmin: securityContext.IsAdmin,
} }
return config, nil return config, nil
} }
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error { func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
settings, err := handler.SettingsService.Settings()
if err != nil {
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
valid, err := handler.isValidStackFile(stackContent)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
}
}
handler.stackCreationMutex.Lock() handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock() defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint) handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
err := handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint) err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
if err != nil { if err != nil {
return err return err
} }

View File

@ -25,6 +25,7 @@ type Handler struct {
DockerHubService portainer.DockerHubService DockerHubService portainer.DockerHubService
SwarmStackManager portainer.SwarmStackManager SwarmStackManager portainer.SwarmStackManager
ComposeStackManager portainer.ComposeStackManager ComposeStackManager portainer.ComposeStackManager
SettingsService portainer.SettingsService
} }
// NewHandler creates a handler to manage stack operations. // NewHandler creates a handler to manage stack operations.
@ -36,18 +37,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
requestBouncer: bouncer, requestBouncer: bouncer,
} }
h.Handle("/stacks", h.Handle("/stacks",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost)
h.Handle("/stacks", h.Handle("/stacks",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackList))).Methods(http.MethodGet) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackList))).Methods(http.MethodGet)
h.Handle("/stacks/{id}", h.Handle("/stacks/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackInspect))).Methods(http.MethodGet) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackInspect))).Methods(http.MethodGet)
h.Handle("/stacks/{id}", h.Handle("/stacks/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete)
h.Handle("/stacks/{id}", h.Handle("/stacks/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
h.Handle("/stacks/{id}/file", h.Handle("/stacks/{id}/file",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
h.Handle("/stacks/{id}/migrate", h.Handle("/stacks/{id}/migrate",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
return h return h
} }

View File

@ -5,6 +5,9 @@ import (
"log" "log"
"net/http" "net/http"
"github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/cli/compose/loader"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
@ -87,3 +90,38 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)} return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
} }
func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) {
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil {
return false, err
}
composeConfigFile := types.ConfigFile{
Config: composeConfigYAML,
}
composeConfigDetails := types.ConfigDetails{
ConfigFiles: []types.ConfigFile{composeConfigFile},
Environment: map[string]string{},
}
composeConfig, err := loader.Load(composeConfigDetails, func(options *loader.Options) {
options.SkipValidation = true
options.SkipInterpolation = true
})
if err != nil {
return false, err
}
for key := range composeConfig.Services {
service := composeConfig.Services[key]
for _, volume := range service.Volumes {
if volume.Type == "bind" {
return false, nil
}
}
}
return true, nil
}

View File

@ -23,6 +23,8 @@ func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status) *Han
} }
h.Handle("/status", h.Handle("/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet) bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
h.Handle("/status/version",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet)
return h return h
} }

View File

@ -0,0 +1,51 @@
package status
import (
"encoding/json"
"net/http"
"github.com/coreos/go-semver/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
)
type inspectVersionResponse struct {
UpdateAvailable bool `json:"UpdateAvailable"`
LatestVersion string `json:"LatestVersion"`
}
type githubData struct {
TagName string `json:"tag_name"`
}
// GET request on /api/status/version
func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) {
motd, err := client.Get(portainer.VersionCheckURL, 5)
if err != nil {
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
return
}
var data githubData
err = json.Unmarshal(motd, &data)
if err != nil {
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
return
}
resp := inspectVersionResponse{
UpdateAvailable: false,
}
currentVersion := semver.New(portainer.APIVersion)
latestVersion := semver.New(data.TagName)
if currentVersion.LessThan(*latestVersion) {
resp.UpdateAvailable = true
resp.LatestVersion = data.TagName
}
response.JSON(w, &resp)
}

View File

@ -0,0 +1,26 @@
package support
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/gorilla/mux"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle support operations.
type Handler struct {
*mux.Router
}
// NewHandler returns a new Handler
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/support",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.supportList))).Methods(http.MethodGet)
return h
}

View File

@ -0,0 +1,39 @@
package support
import (
"encoding/json"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"net/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
)
type supportProduct struct {
ID int `json:"Id"`
Name string `json:"Name"`
ShortDescription string `json:"ShortDescription"`
Price string `json:"Price"`
PriceDescription string `json:"PriceDescription"`
Description string `json:"Description"`
ProductID string `json:"ProductId"`
}
func (handler *Handler) supportList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
supportData, err := client.Get(portainer.SupportProductsURL, 30)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch support options", err}
}
var supportProducts []supportProduct
err = json.Unmarshal(supportData, &supportProducts)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch support options", err}
}
return response.JSON(w, supportProducts)
}

View File

@ -21,11 +21,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/tags", h.Handle("/tags",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
h.Handle("/tags", h.Handle("/tags",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet)
h.Handle("/tags/{id}", h.Handle("/tags/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)
return h return h
} }

View File

@ -13,8 +13,8 @@ import (
// Handler is the HTTP handler used to handle team membership operations. // Handler is the HTTP handler used to handle team membership operations.
type Handler struct { type Handler struct {
*mux.Router *mux.Router
TeamMembershipService portainer.TeamMembershipService TeamMembershipService portainer.TeamMembershipService
ResourceControlService portainer.ResourceControlService AuthorizationService *portainer.AuthorizationService
} }
// NewHandler creates a handler to manage team membership operations. // NewHandler creates a handler to manage team membership operations.
@ -23,13 +23,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/team_memberships", h.Handle("/team_memberships",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost)
h.Handle("/team_memberships", h.Handle("/team_memberships",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
h.Handle("/team_memberships/{id}", h.Handle("/team_memberships/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut)
h.Handle("/team_memberships/{id}", h.Handle("/team_memberships/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
return h return h
} }

View File

@ -70,5 +70,10 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team memberships inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team memberships inside the database", err}
} }
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
return response.JSON(w, membership) return response.JSON(w, membership)
} }

View File

@ -38,5 +38,10 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the team membership from the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the team membership from the database", err}
} }
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
return response.Empty(w) return response.Empty(w)
} }

View File

@ -12,9 +12,9 @@ import (
// Handler is the HTTP handler used to handle team operations. // Handler is the HTTP handler used to handle team operations.
type Handler struct { type Handler struct {
*mux.Router *mux.Router
TeamService portainer.TeamService TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService TeamMembershipService portainer.TeamMembershipService
ResourceControlService portainer.ResourceControlService AuthorizationService *portainer.AuthorizationService
} }
// NewHandler creates a handler to manage team operations. // NewHandler creates a handler to manage team operations.
@ -23,17 +23,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/teams", h.Handle("/teams",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
h.Handle("/teams", h.Handle("/teams",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet)
h.Handle("/teams/{id}", h.Handle("/teams/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
h.Handle("/teams/{id}", h.Handle("/teams/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut)
h.Handle("/teams/{id}", h.Handle("/teams/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
h.Handle("/teams/{id}/memberships", h.Handle("/teams/{id}/memberships",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet) bouncer.AdminAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet)
return h return h
} }

View File

@ -33,5 +33,10 @@ func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete associated team memberships from the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete associated team memberships from the database", err}
} }
err = handler.AuthorizationService.RemoveTeamAccessPolicies(portainer.TeamID(teamID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clean-up team access policies", err}
}
return response.Empty(w) return response.Empty(w)
} }

View File

@ -27,15 +27,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
} }
h.Handle("/templates", h.Handle("/templates",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
h.Handle("/templates", h.Handle("/templates",
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost) bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost)
h.Handle("/templates/{id}", h.Handle("/templates/{id}",
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet) bouncer.RestrictedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet)
h.Handle("/templates/{id}", h.Handle("/templates/{id}",
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut) bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut)
h.Handle("/templates/{id}", h.Handle("/templates/{id}",
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete) bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete)
return h return h
} }

View File

@ -22,6 +22,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}", h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.uploadTLS))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.uploadTLS))).Methods(http.MethodPost)
return h return h
} }

View File

@ -43,25 +43,9 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe
} }
user := &portainer.User{ user := &portainer.User{
Username: payload.Username, Username: payload.Username,
Role: portainer.AdministratorRole, Role: portainer.AdministratorRole,
PortainerAuthorizations: map[portainer.Authorization]bool{ PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
portainer.OperationPortainerDockerHubInspect: true,
portainer.OperationPortainerEndpointGroupList: true,
portainer.OperationPortainerEndpointList: true,
portainer.OperationPortainerEndpointInspect: true,
portainer.OperationPortainerEndpointExtensionAdd: true,
portainer.OperationPortainerEndpointExtensionRemove: true,
portainer.OperationPortainerExtensionList: true,
portainer.OperationPortainerMOTD: true,
portainer.OperationPortainerRegistryList: true,
portainer.OperationPortainerRegistryInspect: true,
portainer.OperationPortainerTeamList: true,
portainer.OperationPortainerTemplateList: true,
portainer.OperationPortainerTemplateInspect: true,
portainer.OperationPortainerUserList: true,
portainer.OperationPortainerUserMemberships: true,
},
} }
user.Password, err = handler.CryptoService.Hash(payload.Password) user.Password, err = handler.CryptoService.Hash(payload.Password)

View File

@ -23,6 +23,7 @@ type Handler struct {
ResourceControlService portainer.ResourceControlService ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService CryptoService portainer.CryptoService
SettingsService portainer.SettingsService SettingsService portainer.SettingsService
AuthorizationService *portainer.AuthorizationService
} }
// NewHandler creates a handler to manage user operations. // NewHandler creates a handler to manage user operations.
@ -31,19 +32,19 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/users", h.Handle("/users",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
h.Handle("/users", h.Handle("/users",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet)
h.Handle("/users/{id}", h.Handle("/users/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
h.Handle("/users/{id}", h.Handle("/users/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut)
h.Handle("/users/{id}", h.Handle("/users/{id}",
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
h.Handle("/users/{id}/memberships", h.Handle("/users/{id}/memberships",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet)
h.Handle("/users/{id}/passwd", h.Handle("/users/{id}/passwd",
rateLimiter.LimitAccess(bouncer.RestrictedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut) rateLimiter.LimitAccess(bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut)
h.Handle("/users/admin/check", h.Handle("/users/admin/check",
bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet) bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet)
h.Handle("/users/admin/init", h.Handle("/users/admin/init",

View File

@ -58,25 +58,9 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
} }
user = &portainer.User{ user = &portainer.User{
Username: payload.Username, Username: payload.Username,
Role: portainer.UserRole(payload.Role), Role: portainer.UserRole(payload.Role),
PortainerAuthorizations: map[portainer.Authorization]bool{ PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
portainer.OperationPortainerDockerHubInspect: true,
portainer.OperationPortainerEndpointGroupList: true,
portainer.OperationPortainerEndpointList: true,
portainer.OperationPortainerEndpointInspect: true,
portainer.OperationPortainerEndpointExtensionAdd: true,
portainer.OperationPortainerEndpointExtensionRemove: true,
portainer.OperationPortainerExtensionList: true,
portainer.OperationPortainerMOTD: true,
portainer.OperationPortainerRegistryList: true,
portainer.OperationPortainerRegistryInspect: true,
portainer.OperationPortainerTeamList: true,
portainer.OperationPortainerTemplateList: true,
portainer.OperationPortainerTemplateInspect: true,
portainer.OperationPortainerUserList: true,
portainer.OperationPortainerUserMemberships: true,
},
} }
settings, err := handler.SettingsService.Settings() settings, err := handler.SettingsService.Settings()

View File

@ -65,15 +65,20 @@ func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.U
} }
func (handler *Handler) deleteUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError { func (handler *Handler) deleteUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
err := handler.UserService.DeleteUser(portainer.UserID(user.ID)) err := handler.UserService.DeleteUser(user.ID)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user from the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user from the database", err}
} }
err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(portainer.UserID(user.ID)) err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(user.ID)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user memberships from the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user memberships from the database", err}
} }
err = handler.AuthorizationService.RemoveUserAccessPolicies(user.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clean-up user access policies", err}
}
return response.Empty(w) return response.Empty(w)
} }

View File

@ -3,6 +3,8 @@ package users
import ( import (
"net/http" "net/http"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
@ -16,6 +18,15 @@ func (handler *Handler) userInspect(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err} return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
} }
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if !securityContext.IsAdmin && securityContext.UserID != portainer.UserID(userID) {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied inspect user", portainer.ErrResourceAccessDenied}
}
user, err := handler.UserService.User(portainer.UserID(userID)) user, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrObjectNotFound { if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err}

View File

@ -24,11 +24,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/webhooks", h.Handle("/webhooks",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost)
h.Handle("/webhooks", h.Handle("/webhooks",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookList))).Methods(http.MethodGet) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookList))).Methods(http.MethodGet)
h.Handle("/webhooks/{id}", h.Handle("/webhooks/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete)
h.Handle("/webhooks/{token}", h.Handle("/webhooks/{token}",
bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost) bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost)
return h return h

View File

@ -26,8 +26,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
requestBouncer: bouncer, requestBouncer: bouncer,
} }
h.PathPrefix("/websocket/exec").Handler( h.PathPrefix("/websocket/exec").Handler(
bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketExec))) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))
h.PathPrefix("/websocket/attach").Handler( h.PathPrefix("/websocket/attach").Handler(
bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketAttach))) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketAttach)))
return h return h
} }

View File

@ -19,12 +19,14 @@ type (
dockerTransport *http.Transport dockerTransport *http.Transport
enableSignature bool enableSignature bool
ResourceControlService portainer.ResourceControlService ResourceControlService portainer.ResourceControlService
UserService portainer.UserService
TeamMembershipService portainer.TeamMembershipService TeamMembershipService portainer.TeamMembershipService
RegistryService portainer.RegistryService RegistryService portainer.RegistryService
DockerHubService portainer.DockerHubService DockerHubService portainer.DockerHubService
SettingsService portainer.SettingsService SettingsService portainer.SettingsService
SignatureService portainer.DigitalSignatureService SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService ReverseTunnelService portainer.ReverseTunnelService
ExtensionService portainer.ExtensionService
endpointIdentifier portainer.EndpointID endpointIdentifier portainer.EndpointID
endpointType portainer.EndpointType endpointType portainer.EndpointType
} }
@ -112,11 +114,29 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
return p.proxyBuildRequest(request) return p.proxyBuildRequest(request)
case strings.HasPrefix(path, "/images"): case strings.HasPrefix(path, "/images"):
return p.proxyImageRequest(request) return p.proxyImageRequest(request)
case strings.HasPrefix(path, "/v2"):
return p.proxyAgentRequest(request)
default: default:
return p.executeDockerRequest(request) return p.executeDockerRequest(request)
} }
} }
func (p *proxyTransport) proxyAgentRequest(r *http.Request) (*http.Response, error) {
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
switch {
case strings.HasPrefix(requestPath, "/browse"):
volumeIDParameter, found := r.URL.Query()["volumeID"]
if !found || len(volumeIDParameter) < 1 {
return p.administratorOperation(r)
}
return p.restrictedVolumeBrowserOperation(r, volumeIDParameter[0])
}
return p.executeDockerRequest(r)
}
func (p *proxyTransport) proxyConfigRequest(request *http.Request) (*http.Response, error) { func (p *proxyTransport) proxyConfigRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath { switch requestPath := request.URL.Path; requestPath {
case "/configs/create": case "/configs/create":
@ -360,7 +380,63 @@ func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID s
} }
resourceControl := getResourceControlByResourceID(resourceID, resourceControls) resourceControl := getResourceControlByResourceID(resourceID, resourceControls)
if resourceControl != nil && !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl) { if resourceControl == nil || !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
return writeAccessDeniedResponse()
}
}
return p.executeDockerRequest(request)
}
// restrictedVolumeBrowserOperation is similar to restrictedOperation but adds an extra check on a specific setting
func (p *proxyTransport) restrictedVolumeBrowserOperation(request *http.Request, resourceID string) (*http.Response, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
if tokenData.Role != portainer.AdministratorRole {
settings, err := p.SettingsService.Settings()
if err != nil {
return nil, err
}
_, err = p.ExtensionService.Extension(portainer.RBACExtension)
if err == portainer.ErrObjectNotFound && !settings.AllowVolumeBrowserForRegularUsers {
return writeAccessDeniedResponse()
} else if err != nil && err != portainer.ErrObjectNotFound {
return nil, err
}
user, err := p.UserService.User(tokenData.ID)
if err != nil {
return nil, err
}
endpointResourceAccess := false
_, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess]
if ok {
endpointResourceAccess = true
}
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
return nil, err
}
userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range teamMemberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
}
resourceControls, err := p.ResourceControlService.ResourceControls()
if err != nil {
return nil, err
}
resourceControl := getResourceControlByResourceID(resourceID, resourceControls)
if !endpointResourceAccess && (resourceControl == nil || !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl)) {
return writeAccessDeniedResponse() return writeAccessDeniedResponse()
} }
} }
@ -498,7 +574,12 @@ func (p *proxyTransport) createOperationContext(request *http.Request) (*restric
if tokenData.Role != portainer.AdministratorRole { if tokenData.Role != portainer.AdministratorRole {
operationContext.isAdmin = false operationContext.isAdmin = false
_, ok := tokenData.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess] user, err := p.UserService.User(operationContext.userID)
if err != nil {
return nil, err
}
_, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess]
if ok { if ok {
operationContext.endpointResourceAccess = true operationContext.endpointResourceAccess = true
} }

View File

@ -16,12 +16,14 @@ const AzureAPIBaseURL = "https://management.azure.com"
// proxyFactory is a factory to create reverse proxies to Docker endpoints // proxyFactory is a factory to create reverse proxies to Docker endpoints
type proxyFactory struct { type proxyFactory struct {
ResourceControlService portainer.ResourceControlService ResourceControlService portainer.ResourceControlService
UserService portainer.UserService
TeamMembershipService portainer.TeamMembershipService TeamMembershipService portainer.TeamMembershipService
SettingsService portainer.SettingsService SettingsService portainer.SettingsService
RegistryService portainer.RegistryService RegistryService portainer.RegistryService
DockerHubService portainer.DockerHubService DockerHubService portainer.DockerHubService
SignatureService portainer.DigitalSignatureService SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService ReverseTunnelService portainer.ReverseTunnelService
ExtensionService portainer.ExtensionService
} }
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
@ -70,11 +72,13 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, endpoint *port
transport := &proxyTransport{ transport := &proxyTransport{
enableSignature: enableSignature, enableSignature: enableSignature,
ResourceControlService: factory.ResourceControlService, ResourceControlService: factory.ResourceControlService,
UserService: factory.UserService,
TeamMembershipService: factory.TeamMembershipService, TeamMembershipService: factory.TeamMembershipService,
SettingsService: factory.SettingsService, SettingsService: factory.SettingsService,
RegistryService: factory.RegistryService, RegistryService: factory.RegistryService,
DockerHubService: factory.DockerHubService, DockerHubService: factory.DockerHubService,
ReverseTunnelService: factory.ReverseTunnelService, ReverseTunnelService: factory.ReverseTunnelService,
ExtensionService: factory.ExtensionService,
dockerTransport: &http.Transport{}, dockerTransport: &http.Transport{},
endpointIdentifier: endpoint.ID, endpointIdentifier: endpoint.ID,
endpointType: endpoint.Type, endpointType: endpoint.Type,

View File

@ -13,10 +13,12 @@ func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endp
transport := &proxyTransport{ transport := &proxyTransport{
enableSignature: false, enableSignature: false,
ResourceControlService: factory.ResourceControlService, ResourceControlService: factory.ResourceControlService,
UserService: factory.UserService,
TeamMembershipService: factory.TeamMembershipService, TeamMembershipService: factory.TeamMembershipService,
SettingsService: factory.SettingsService, SettingsService: factory.SettingsService,
RegistryService: factory.RegistryService, RegistryService: factory.RegistryService,
DockerHubService: factory.DockerHubService, DockerHubService: factory.DockerHubService,
ExtensionService: factory.ExtensionService,
dockerTransport: newSocketTransport(path), dockerTransport: newSocketTransport(path),
ReverseTunnelService: factory.ReverseTunnelService, ReverseTunnelService: factory.ReverseTunnelService,
endpointIdentifier: endpoint.ID, endpointIdentifier: endpoint.ID,

View File

@ -6,6 +6,8 @@ import (
"net" "net"
"net/http" "net/http"
"github.com/Microsoft/go-winio"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
) )
@ -14,11 +16,13 @@ func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endp
transport := &proxyTransport{ transport := &proxyTransport{
enableSignature: false, enableSignature: false,
ResourceControlService: factory.ResourceControlService, ResourceControlService: factory.ResourceControlService,
UserService: factory.UserService,
TeamMembershipService: factory.TeamMembershipService, TeamMembershipService: factory.TeamMembershipService,
SettingsService: factory.SettingsService, SettingsService: factory.SettingsService,
RegistryService: factory.RegistryService, RegistryService: factory.RegistryService,
DockerHubService: factory.DockerHubService, DockerHubService: factory.DockerHubService,
ReverseTunnelService: factory.ReverseTunnelService, ReverseTunnelService: factory.ReverseTunnelService,
ExtensionService: factory.ExtensionService,
dockerTransport: newNamedPipeTransport(path), dockerTransport: newNamedPipeTransport(path),
endpointIdentifier: endpoint.ID, endpointIdentifier: endpoint.ID,
endpointType: endpoint.Type, endpointType: endpoint.Type,

View File

@ -31,12 +31,14 @@ type (
// ManagerParams represents the required parameters to create a new Manager instance. // ManagerParams represents the required parameters to create a new Manager instance.
ManagerParams struct { ManagerParams struct {
ResourceControlService portainer.ResourceControlService ResourceControlService portainer.ResourceControlService
UserService portainer.UserService
TeamMembershipService portainer.TeamMembershipService TeamMembershipService portainer.TeamMembershipService
SettingsService portainer.SettingsService SettingsService portainer.SettingsService
RegistryService portainer.RegistryService RegistryService portainer.RegistryService
DockerHubService portainer.DockerHubService DockerHubService portainer.DockerHubService
SignatureService portainer.DigitalSignatureService SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService ReverseTunnelService portainer.ReverseTunnelService
ExtensionService portainer.ExtensionService
} }
) )
@ -48,12 +50,14 @@ func NewManager(parameters *ManagerParams) *Manager {
legacyExtensionProxies: cmap.New(), legacyExtensionProxies: cmap.New(),
proxyFactory: &proxyFactory{ proxyFactory: &proxyFactory{
ResourceControlService: parameters.ResourceControlService, ResourceControlService: parameters.ResourceControlService,
UserService: parameters.UserService,
TeamMembershipService: parameters.TeamMembershipService, TeamMembershipService: parameters.TeamMembershipService,
SettingsService: parameters.SettingsService, SettingsService: parameters.SettingsService,
RegistryService: parameters.RegistryService, RegistryService: parameters.RegistryService,
DockerHubService: parameters.DockerHubService, DockerHubService: parameters.DockerHubService,
SignatureService: parameters.SignatureService, SignatureService: parameters.SignatureService,
ReverseTunnelService: parameters.ReverseTunnelService, ReverseTunnelService: parameters.ReverseTunnelService,
ExtensionService: parameters.ExtensionService,
}, },
reverseTunnelService: parameters.ReverseTunnelService, reverseTunnelService: parameters.ReverseTunnelService,
} }

View File

@ -34,7 +34,7 @@ type (
} }
// RestrictedRequestContext is a data structure containing information // RestrictedRequestContext is a data structure containing information
// used in RestrictedAccess // used in AuthenticatedAccess
RestrictedRequestContext struct { RestrictedRequestContext struct {
IsAdmin bool IsAdmin bool
IsTeamLeader bool IsTeamLeader bool
@ -64,22 +64,40 @@ func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
return h return h
} }
// AuthorizedAccess defines a security check for API endpoints that require an authorization check. // AdminAccess defines a security check for API endpoints that require an authorization check.
// Authentication is required to access these endpoints. // Authentication is required to access these endpoints.
// If the RBAC extension is enabled, authorizations are required to use these endpoints. // If the RBAC extension is enabled, authorizations are required to use these endpoints.
// If the RBAC extension is not enabled, the administrator role is required to use these endpoints. // If the RBAC extension is not enabled, the administrator role is required to use these endpoints.
func (bouncer *RequestBouncer) AuthorizedAccess(h http.Handler) http.Handler { // The request context will be enhanced with a RestrictedRequestContext object
// that might be used later to inside the API operation for extra authorization validation
// and resource filtering.
func (bouncer *RequestBouncer) AdminAccess(h http.Handler) http.Handler {
h = bouncer.mwUpgradeToRestrictedRequest(h) h = bouncer.mwUpgradeToRestrictedRequest(h)
h = bouncer.mwCheckPortainerAuthorizations(h) h = bouncer.mwCheckPortainerAuthorizations(h, true)
h = bouncer.mwAuthenticatedUser(h) h = bouncer.mwAuthenticatedUser(h)
return h return h
} }
// RestrictedAccess defines a security check for restricted API endpoints. // RestrictedAccess defines a security check for restricted API endpoints.
// Authentication is required to access these endpoints. // Authentication is required to access these endpoints.
// If the RBAC extension is enabled, authorizations are required to use these endpoints.
// If the RBAC extension is not enabled, access is granted to any authenticated user.
// The request context will be enhanced with a RestrictedRequestContext object // The request context will be enhanced with a RestrictedRequestContext object
// that might be used later to authorize/filter access to resources inside an endpoint. // that might be used later to inside the API operation for extra authorization validation
// and resource filtering.
func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler { func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler {
h = bouncer.mwUpgradeToRestrictedRequest(h)
h = bouncer.mwCheckPortainerAuthorizations(h, false)
h = bouncer.mwAuthenticatedUser(h)
return h
}
// AuthenticatedAccess defines a security check for restricted API endpoints.
// Authentication is required to access these endpoints.
// The request context will be enhanced with a RestrictedRequestContext object
// that might be used later to inside the API operation for extra authorization validation
// and resource filtering.
func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler {
h = bouncer.mwUpgradeToRestrictedRequest(h) h = bouncer.mwUpgradeToRestrictedRequest(h)
h = bouncer.mwAuthenticatedUser(h) h = bouncer.mwAuthenticatedUser(h)
return h return h
@ -142,10 +160,15 @@ func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Reque
return err return err
} }
user, err := bouncer.userService.User(tokenData.ID)
if err != nil {
return err
}
apiOperation := &portainer.APIOperationAuthorizationRequest{ apiOperation := &portainer.APIOperationAuthorizationRequest{
Path: r.URL.String(), Path: r.URL.String(),
Method: r.Method, Method: r.Method,
Authorizations: tokenData.EndpointAuthorizations[endpoint.ID], Authorizations: user.EndpointAuthorizations[endpoint.ID],
} }
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey) bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
@ -186,11 +209,13 @@ func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler
// mwCheckPortainerAuthorizations will verify that the user has the required authorization to access // mwCheckPortainerAuthorizations will verify that the user has the required authorization to access
// a specific API endpoint. It will leverage the RBAC extension authorization validation if the extension // a specific API endpoint. It will leverage the RBAC extension authorization validation if the extension
// is enabled. // is enabled.
func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler) http.Handler { // If the administratorOnly flag is specified and the RBAC extension is not enabled, this will prevent non-admin
// users from accessing the endpoint.
func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, administratorOnly bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenData, err := RetrieveTokenData(r) tokenData, err := RetrieveTokenData(r)
if err != nil { if err != nil {
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied) httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrUnauthorized)
return return
} }
@ -201,6 +226,11 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler)
extension, err := bouncer.extensionService.Extension(portainer.RBACExtension) extension, err := bouncer.extensionService.Extension(portainer.RBACExtension)
if err == portainer.ErrObjectNotFound { if err == portainer.ErrObjectNotFound {
if administratorOnly {
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrUnauthorized)
return
}
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} else if err != nil { } else if err != nil {
@ -208,10 +238,19 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler)
return return
} }
user, err := bouncer.userService.User(tokenData.ID)
if err != nil && err == portainer.ErrObjectNotFound {
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized)
return
} else if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user details from the database", err)
return
}
apiOperation := &portainer.APIOperationAuthorizationRequest{ apiOperation := &portainer.APIOperationAuthorizationRequest{
Path: r.URL.String(), Path: r.URL.String(),
Method: r.Method, Method: r.Method,
Authorizations: tokenData.PortainerAuthorizations, Authorizations: user.PortainerAuthorizations,
} }
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey) bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
@ -281,7 +320,7 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized)
return return
} else if err != nil { } else if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve users from the database", err) httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user details from the database", err)
return return
} }
} else { } else {

View File

@ -3,6 +3,8 @@ package http
import ( import (
"time" "time"
"github.com/portainer/portainer/api/http/handler/support"
"github.com/portainer/portainer/api/http/handler/roles" "github.com/portainer/portainer/api/http/handler/roles"
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
@ -84,15 +86,27 @@ type Server struct {
func (server *Server) Start() error { func (server *Server) Start() error {
proxyManagerParameters := &proxy.ManagerParams{ proxyManagerParameters := &proxy.ManagerParams{
ResourceControlService: server.ResourceControlService, ResourceControlService: server.ResourceControlService,
UserService: server.UserService,
TeamMembershipService: server.TeamMembershipService, TeamMembershipService: server.TeamMembershipService,
SettingsService: server.SettingsService, SettingsService: server.SettingsService,
RegistryService: server.RegistryService, RegistryService: server.RegistryService,
DockerHubService: server.DockerHubService, DockerHubService: server.DockerHubService,
SignatureService: server.SignatureService, SignatureService: server.SignatureService,
ReverseTunnelService: server.ReverseTunnelService, ReverseTunnelService: server.ReverseTunnelService,
ExtensionService: server.ExtensionService,
} }
proxyManager := proxy.NewManager(proxyManagerParameters) proxyManager := proxy.NewManager(proxyManagerParameters)
authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
EndpointService: server.EndpointService,
EndpointGroupService: server.EndpointGroupService,
RegistryService: server.RegistryService,
RoleService: server.RoleService,
TeamMembershipService: server.TeamMembershipService,
UserService: server.UserService,
}
authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)
requestBouncerParameters := &security.RequestBouncerParams{ requestBouncerParameters := &security.RequestBouncerParams{
JWTService: server.JWTService, JWTService: server.JWTService,
UserService: server.UserService, UserService: server.UserService,
@ -136,10 +150,12 @@ func (server *Server) Start() error {
endpointHandler.JobService = server.JobService endpointHandler.JobService = server.JobService
endpointHandler.ReverseTunnelService = server.ReverseTunnelService endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.SettingsService = server.SettingsService endpointHandler.SettingsService = server.SettingsService
endpointHandler.AuthorizationService = authorizationService
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer) var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
endpointGroupHandler.EndpointService = server.EndpointService endpointGroupHandler.EndpointService = server.EndpointService
endpointGroupHandler.AuthorizationService = authorizationService
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer) var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
endpointProxyHandler.EndpointService = server.EndpointService endpointProxyHandler.EndpointService = server.EndpointService
@ -157,6 +173,7 @@ func (server *Server) Start() error {
extensionHandler.EndpointGroupService = server.EndpointGroupService extensionHandler.EndpointGroupService = server.EndpointGroupService
extensionHandler.EndpointService = server.EndpointService extensionHandler.EndpointService = server.EndpointService
extensionHandler.RegistryService = server.RegistryService extensionHandler.RegistryService = server.RegistryService
extensionHandler.AuthorizationService = authorizationService
var registryHandler = registries.NewHandler(requestBouncer) var registryHandler = registries.NewHandler(requestBouncer)
registryHandler.RegistryService = server.RegistryService registryHandler.RegistryService = server.RegistryService
@ -182,6 +199,9 @@ func (server *Server) Start() error {
settingsHandler.FileService = server.FileService settingsHandler.FileService = server.FileService
settingsHandler.JobScheduler = server.JobScheduler settingsHandler.JobScheduler = server.JobScheduler
settingsHandler.ScheduleService = server.ScheduleService settingsHandler.ScheduleService = server.ScheduleService
settingsHandler.RoleService = server.RoleService
settingsHandler.ExtensionService = server.ExtensionService
settingsHandler.AuthorizationService = authorizationService
var stackHandler = stacks.NewHandler(requestBouncer) var stackHandler = stacks.NewHandler(requestBouncer)
stackHandler.FileService = server.FileService stackHandler.FileService = server.FileService
@ -193,6 +213,7 @@ func (server *Server) Start() error {
stackHandler.GitService = server.GitService stackHandler.GitService = server.GitService
stackHandler.RegistryService = server.RegistryService stackHandler.RegistryService = server.RegistryService
stackHandler.DockerHubService = server.DockerHubService stackHandler.DockerHubService = server.DockerHubService
stackHandler.SettingsService = server.SettingsService
var tagHandler = tags.NewHandler(requestBouncer) var tagHandler = tags.NewHandler(requestBouncer)
tagHandler.TagService = server.TagService tagHandler.TagService = server.TagService
@ -200,11 +221,16 @@ func (server *Server) Start() error {
var teamHandler = teams.NewHandler(requestBouncer) var teamHandler = teams.NewHandler(requestBouncer)
teamHandler.TeamService = server.TeamService teamHandler.TeamService = server.TeamService
teamHandler.TeamMembershipService = server.TeamMembershipService teamHandler.TeamMembershipService = server.TeamMembershipService
teamHandler.AuthorizationService = authorizationService
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer) var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
teamMembershipHandler.AuthorizationService = authorizationService
var statusHandler = status.NewHandler(requestBouncer, server.Status) var statusHandler = status.NewHandler(requestBouncer, server.Status)
var supportHandler = support.NewHandler(requestBouncer)
var templatesHandler = templates.NewHandler(requestBouncer) var templatesHandler = templates.NewHandler(requestBouncer)
templatesHandler.TemplateService = server.TemplateService templatesHandler.TemplateService = server.TemplateService
templatesHandler.SettingsService = server.SettingsService templatesHandler.SettingsService = server.SettingsService
@ -219,6 +245,7 @@ func (server *Server) Start() error {
userHandler.CryptoService = server.CryptoService userHandler.CryptoService = server.CryptoService
userHandler.ResourceControlService = server.ResourceControlService userHandler.ResourceControlService = server.ResourceControlService
userHandler.SettingsService = server.SettingsService userHandler.SettingsService = server.SettingsService
userHandler.AuthorizationService = authorizationService
var websocketHandler = websocket.NewHandler(requestBouncer) var websocketHandler = websocket.NewHandler(requestBouncer)
websocketHandler.EndpointService = server.EndpointService websocketHandler.EndpointService = server.EndpointService
@ -245,6 +272,7 @@ func (server *Server) Start() error {
SettingsHandler: settingsHandler, SettingsHandler: settingsHandler,
StatusHandler: statusHandler, StatusHandler: statusHandler,
StackHandler: stackHandler, StackHandler: stackHandler,
SupportHandler: supportHandler,
TagHandler: tagHandler, TagHandler: tagHandler,
TeamHandler: teamHandler, TeamHandler: teamHandler,
TeamMembershipHandler: teamMembershipHandler, TeamMembershipHandler: teamMembershipHandler,

View File

@ -16,11 +16,9 @@ type Service struct {
} }
type claims struct { type claims struct {
UserID int `json:"id"` UserID int `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Role int `json:"role"` Role int `json:"role"`
EndpointAuthorizations portainer.EndpointAuthorizations `json:"endpointAuthorizations"`
PortainerAuthorizations portainer.Authorizations `json:"portainerAuthorizations"`
jwt.StandardClaims jwt.StandardClaims
} }
@ -40,12 +38,10 @@ func NewService() (*Service, error) {
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) { func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
expireToken := time.Now().Add(time.Hour * 8).Unix() expireToken := time.Now().Add(time.Hour * 8).Unix()
cl := claims{ cl := claims{
int(data.ID), UserID: int(data.ID),
data.Username, Username: data.Username,
int(data.Role), Role: int(data.Role),
data.EndpointAuthorizations, StandardClaims: jwt.StandardClaims{
data.PortainerAuthorizations,
jwt.StandardClaims{
ExpiresAt: expireToken, ExpiresAt: expireToken,
}, },
} }
@ -71,11 +67,9 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData,
if err == nil && parsedToken != nil { if err == nil && parsedToken != nil {
if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid { if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
tokenData := &portainer.TokenData{ tokenData := &portainer.TokenData{
ID: portainer.UserID(cl.UserID), ID: portainer.UserID(cl.UserID),
Username: cl.Username, Username: cl.Username,
Role: portainer.UserRole(cl.Role), Role: portainer.UserRole(cl.Role),
EndpointAuthorizations: cl.EndpointAuthorizations,
PortainerAuthorizations: cl.PortainerAuthorizations,
} }
return tokenData, nil return tokenData, nil
} }

View File

@ -106,6 +106,7 @@ type (
OAuthSettings OAuthSettings `json:"OAuthSettings"` OAuthSettings OAuthSettings `json:"OAuthSettings"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
SnapshotInterval string `json:"SnapshotInterval"` SnapshotInterval string `json:"SnapshotInterval"`
TemplatesURL string `json:"TemplatesURL"` TemplatesURL string `json:"TemplatesURL"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
@ -118,11 +119,12 @@ type (
// User represents a user account // User represents a user account
User struct { User struct {
ID UserID `json:"Id"` ID UserID `json:"Id"`
Username string `json:"Username"` Username string `json:"Username"`
Password string `json:"Password,omitempty"` Password string `json:"Password,omitempty"`
Role UserRole `json:"Role"` Role UserRole `json:"Role"`
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"` PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"`
} }
// UserID represents a user identifier // UserID represents a user identifier
@ -160,11 +162,9 @@ type (
// TokenData represents the data embedded in a JWT token // TokenData represents the data embedded in a JWT token
TokenData struct { TokenData struct {
ID UserID ID UserID
Username string Username string
Role UserRole Role UserRole
EndpointAuthorizations EndpointAuthorizations
PortainerAuthorizations Authorizations
} }
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier) // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
@ -638,7 +638,8 @@ type (
RoleService interface { RoleService interface {
Role(ID RoleID) (*Role, error) Role(ID RoleID) (*Role, error)
Roles() ([]Role, error) Roles() ([]Role, error)
CreateRole(set *Role) error CreateRole(role *Role) error
UpdateRole(ID RoleID, role *Role) error
} }
// TeamService represents a service for managing user data // TeamService represents a service for managing user data
@ -902,15 +903,19 @@ type (
const ( const (
// APIVersion is the version number of the Portainer API // APIVersion is the version number of the Portainer API
APIVersion = "1.22.0" APIVersion = "1.22.1"
// DBVersion is the version number of the Portainer database // DBVersion is the version number of the Portainer database
DBVersion = 19 DBVersion = 20
// AssetsServerURL represents the URL of the Portainer asset server // AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
MessageOfTheDayURL = AssetsServerURL + "/motd.json" MessageOfTheDayURL = AssetsServerURL + "/motd.json"
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved // ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.0.json" ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.1.json"
// SupportProductsURL represents the URL where Portainer support products can be retrieved
SupportProductsURL = AssetsServerURL + "/support.json"
// PortainerAgentHeader represents the name of the header available in any agent response // PortainerAgentHeader represents the name of the header available in any agent response
PortainerAgentHeader = "Portainer-Agent" PortainerAgentHeader = "Portainer-Agent"
// PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster // PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster

View File

@ -54,7 +54,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.22.0" version: "1.22.1"
title: "Portainer API" title: "Portainer API"
contact: contact:
email: "info@portainer.io" email: "info@portainer.io"
@ -254,7 +254,7 @@ paths:
- name: "EndpointType" - name: "EndpointType"
in: "formData" in: "formData"
type: "integer" type: "integer"
description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 3 (Azure environment)" description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge agent environment)"
required: true required: true
- name: "URL" - name: "URL"
in: "formData" in: "formData"
@ -3174,7 +3174,7 @@ definitions:
description: "Is analytics enabled" description: "Is analytics enabled"
Version: Version:
type: "string" type: "string"
example: "1.22.0" example: "1.22.1"
description: "Portainer API version" description: "Portainer API version"
PublicSettingsInspectResponse: PublicSettingsInspectResponse:
type: "object" type: "object"
@ -3920,6 +3920,10 @@ definitions:
type: "boolean" type: "boolean"
example: true example: true
description: "Whether non-administrator users should be able to use privileged mode when creating containers" description: "Whether non-administrator users should be able to use privileged mode when creating containers"
EdgeAgentCheckinInterval:
type: "integer"
example: "30"
description: "Polling interval for Edge agent (in seconds)"
EndpointGroupCreateRequest: EndpointGroupCreateRequest:
type: "object" type: "object"
required: required:

View File

@ -1,5 +1,5 @@
{ {
"packageName": "portainer", "packageName": "portainer",
"packageVersion": "1.22.0", "packageVersion": "1.22.1",
"projectName": "portainer" "projectName": "portainer"
} }

View File

@ -1,4 +1,5 @@
import '../assets/css/app.css'; import '../assets/css/app.css';
import './libraries/isteven-angular-multiselect/isteven-multi-select.css';
import angular from 'angular'; import angular from 'angular';
import './agent/_module'; import './agent/_module';

View File

@ -7,7 +7,7 @@
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">

View File

@ -17,7 +17,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-filters nowrap-cells"> <table class="table table-hover table-filters nowrap-cells">

View File

@ -11,6 +11,7 @@ angular.module('portainer')
.constant('API_ENDPOINT_SETTINGS', 'api/settings') .constant('API_ENDPOINT_SETTINGS', 'api/settings')
.constant('API_ENDPOINT_STACKS', 'api/stacks') .constant('API_ENDPOINT_STACKS', 'api/stacks')
.constant('API_ENDPOINT_STATUS', 'api/status') .constant('API_ENDPOINT_STATUS', 'api/status')
.constant('API_ENDPOINT_SUPPORT', 'api/support')
.constant('API_ENDPOINT_USERS', 'api/users') .constant('API_ENDPOINT_USERS', 'api/users')
.constant('API_ENDPOINT_TAGS', 'api/tags') .constant('API_ENDPOINT_TAGS', 'api/tags')
.constant('API_ENDPOINT_TEAMS', 'api/teams') .constant('API_ENDPOINT_TEAMS', 'api/teams')

View File

@ -55,7 +55,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -8,7 +8,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -136,7 +136,7 @@
></containers-datatable-actions> ></containers-datatable-actions>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-filters nowrap-cells"> <table class="table table-hover table-filters nowrap-cells">

View File

@ -172,6 +172,7 @@ function ($scope, $controller, DatatableService, EndpointProvider) {
this.setDefaults(); this.setDefaults();
this.prepareTableFromDataset(); this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey); var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) { if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse; this.state.reverseOrder = storedOrder.reverse;
@ -203,6 +204,5 @@ function ($scope, $controller, DatatableService, EndpointProvider) {
this.columnVisibility = storedColumnVisibility; this.columnVisibility = storedColumnVisibility;
this.columnVisibility.state.open = false; this.columnVisibility.state.open = false;
} }
this.state.orderBy = this.orderBy;
}; };
}]); }]);

View File

@ -8,7 +8,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -16,7 +16,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-filters nowrap-cells"> <table class="table table-hover table-filters nowrap-cells">

View File

@ -108,6 +108,7 @@ angular.module('portainer.docker')
this.setDefaults(); this.setDefaults();
this.prepareTableFromDataset(); this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey); var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) { if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse; this.state.reverseOrder = storedOrder.reverse;
@ -135,7 +136,6 @@ angular.module('portainer.docker')
this.settings.open = false; this.settings.open = false;
} }
this.onSettingsRepeaterChange(); this.onSettingsRepeaterChange();
this.state.orderBy = this.orderBy;
}; };
} }
]); ]);

View File

@ -75,7 +75,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-filters nowrap-cells"> <table class="table table-hover table-filters nowrap-cells">

View File

@ -39,6 +39,7 @@ function ($scope, $controller, DatatableService) {
this.setDefaults(); this.setDefaults();
this.prepareTableFromDataset(); this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey); var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) { if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse; this.state.reverseOrder = storedOrder.reverse;
@ -66,6 +67,5 @@ function ($scope, $controller, DatatableService) {
} }
this.onSettingsRepeaterChange(); this.onSettingsRepeaterChange();
this.state.orderBy = this.orderBy;
}; };
}]); }]);

View File

@ -8,7 +8,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -0,0 +1,27 @@
<td ng-if="allowCheckbox">
<span class="md-checkbox" ng-if="!parentCtrl.offlineMode">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="parentCtrl.selectItem(item)" ng-disabled="parentCtrl.disableRemove(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ng-if="parentCtrl.itemCanExpand(item)" ng-click="parentCtrl.expandItem(item, !item.Expanded)"><i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i></a>
</td>
<td ng-if="!allowCheckbox"></td>
<td>
<a ng-if="!parentCtrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
<span ng-if="parentCtrl.offlineMode">{{ item.Name | truncate:40 }}</span>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td>{{ item.Scope }}</td>
<td>{{ item.Driver }}</td>
<td>{{ item.Attachable }}</td>
<td>{{ item.Internal }}</td>
<td>{{ item.IPAM.Driver }}</td>
<td>{{ item.IPAM.Config[0].Subnet ? item.IPAM.Config[0].Subnet : '-' }}</td>
<td>{{ item.IPAM.Config[0].Gateway ? item.IPAM.Config[0].Gateway : '-' }}</td>
<td ng-if="parentCtrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
<td ng-if="parentCtrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>

View File

@ -0,0 +1,15 @@
angular.module('portainer.docker')
.directive('networkRowContent', [function networkRowContent() {
var directive = {
templateUrl: './networkRowContent.html',
restrict: 'A',
transclude: true,
scope: {
item: '<',
parentCtrl: '<',
allowCheckbox: '<',
allowExpand: '<'
}
};
return directive;
}]);

View File

@ -55,17 +55,22 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">
<thead> <thead>
<tr> <tr>
<th> <th style="width:55px;">
<span class="md-checkbox" ng-if="!$ctrl.offlineMode"> <span class="md-checkbox" ng-if="!$ctrl.offlineMode">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" /> <input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label> <label for="select_all"></label>
</span> </span>
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')"> <a ng-click="$ctrl.changeOrderBy('Name')">
Name Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i> <i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
@ -145,30 +150,11 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}"> <tr dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
<td> ng-class="{active: item.Checked}" network-row-content item="item" parent-ctrl="$ctrl" allow-checkbox="true">
<span class="md-checkbox" ng-if="!$ctrl.offlineMode"> </tr>
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.disableRemove(item)"/> <tr dir-paginate-end ng-show="item.Expanded" ng-repeat="it in item.Subs" style="background: #d5e8f3;"
<label for="select_{{ $index }}"></label> network-row-content item="it" parent-ctrl="$ctrl">
</span>
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
<span ng-if="$ctrl.offlineMode">{{ item.Name | truncate:40 }}</span>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td>{{ item.Scope }}</td>
<td>{{ item.Driver }}</td>
<td>{{ item.Attachable }}</td>
<td>{{ item.Internal }}</td>
<td>{{ item.IPAM.Driver }}</td>
<td>{{ item.IPAM.Config[0].Subnet ? item.IPAM.Config[0].Subnet : '-' }}</td>
<td>{{ item.IPAM.Config[0].Gateway ? item.IPAM.Config[0].Gateway : '-' }}</td>
<td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>
</tr> </tr>
<tr ng-if="!$ctrl.dataset"> <tr ng-if="!$ctrl.dataset">
<td colspan="9" class="text-center text-muted">Loading...</td> <td colspan="9" class="text-center text-muted">Loading...</td>

View File

@ -1,3 +1,5 @@
import _ from 'lodash-es';
angular.module('portainer.docker') angular.module('portainer.docker')
.controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService', .controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService',
function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) { function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) {
@ -8,6 +10,10 @@ angular.module('portainer.docker')
return PREDEFINED_NETWORKS.includes(item.Name); return PREDEFINED_NETWORKS.includes(item.Name);
}; };
this.state = Object.assign(this.state, {
expandedItems: []
})
/** /**
* Do not allow PREDEFINED_NETWORKS to be selected * Do not allow PREDEFINED_NETWORKS to be selected
*/ */
@ -19,6 +25,7 @@ angular.module('portainer.docker')
this.setDefaults(); this.setDefaults();
this.prepareTableFromDataset(); this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey); var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) { if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse; this.state.reverseOrder = storedOrder.reverse;
@ -45,7 +52,27 @@ angular.module('portainer.docker')
this.settings.open = false; this.settings.open = false;
} }
this.onSettingsRepeaterChange(); this.onSettingsRepeaterChange();
this.state.orderBy = this.orderBy; };
this.expandItem = function(item, expanded) {
item.Expanded = expanded;
};
this.itemCanExpand = function(item) {
return item.Subs.length > 0;
}
this.hasExpandableItems = function() {
return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length;
};
this.expandAll = function() {
this.state.expandAll = !this.state.expandAll;
_.forEach(this.state.filteredDataSet, (item) => {
if (this.itemCanExpand(item)) {
this.expandItem(item, this.state.expandAll);
}
});
}; };
} }
]); ]);

View File

@ -8,7 +8,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -46,7 +46,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -55,7 +55,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -61,6 +61,7 @@ angular.module('portainer.docker')
this.setDefaults(); this.setDefaults();
this.prepareTableFromDataset(); this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey); var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) { if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse; this.state.reverseOrder = storedOrder.reverse;
@ -87,7 +88,6 @@ angular.module('portainer.docker')
this.settings.open = false; this.settings.open = false;
} }
this.onSettingsRepeaterChange(); this.onSettingsRepeaterChange();
this.state.orderBy = this.orderBy;
}; };
} }
]); ]);

View File

@ -52,7 +52,7 @@
></services-datatable-actions> ></services-datatable-actions>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -68,6 +68,7 @@ function ($scope, $controller, DatatableService, EndpointProvider) {
this.setDefaults(); this.setDefaults();
this.prepareTableFromDataset(); this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey); var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) { if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse; this.state.reverseOrder = storedOrder.reverse;
@ -99,6 +100,5 @@ function ($scope, $controller, DatatableService, EndpointProvider) {
this.settings.open = false; this.settings.open = false;
} }
this.onSettingsRepeaterChange(); this.onSettingsRepeaterChange();
this.state.orderBy = this.orderBy;
}; };
}]); }]);

View File

@ -8,7 +8,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..."> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover nowrap-cells"> <table class="table table-hover nowrap-cells">

View File

@ -16,6 +16,7 @@ function ($scope, $controller, DatatableService) {
this.setDefaults(); this.setDefaults();
this.prepareTableFromDataset(); this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey); var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) { if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse; this.state.reverseOrder = storedOrder.reverse;
@ -42,6 +43,5 @@ function ($scope, $controller, DatatableService) {
this.settings.open = false; this.settings.open = false;
} }
this.onSettingsRepeaterChange(); this.onSettingsRepeaterChange();
this.state.orderBy = this.orderBy;
}; };
}]); }]);

View File

@ -55,7 +55,7 @@
</div> </div>
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> <input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }">
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-filters nowrap-cells"> <table class="table table-hover table-filters nowrap-cells">
@ -149,9 +149,11 @@
</span> </span>
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.volumes.volume({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a> <a ng-if="!$ctrl.offlineMode" ui-sref="docker.volumes.volume({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
<span ng-if="$ctrl.offlineMode">{{ item.Id | truncate:40 }}</span> <span ng-if="$ctrl.offlineMode">{{ item.Id | truncate:40 }}</span>
<a authorization="DockerAgentBrowseList" ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })" class="btn btn-xs btn-primary space-left" ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode"> <btn authorization="DockerAgentBrowseList" ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode">
<i class="fa fa-search"></i> browse</a> <a ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })" class="btn btn-xs btn-primary space-left">
</a> <i class="fa fa-search"></i> browse
</a>
</btn>
<span style="margin-left: 10px;" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span> <span style="margin-left: 10px;" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span>
</td> </td>
<td>{{ item.StackName ? item.StackName : '-' }}</td> <td>{{ item.StackName ? item.StackName : '-' }}</td>

Some files were not shown because too many files have changed in this diff Show More