mirror of https://github.com/portainer/portainer
commit
80ad5079f7
|
@ -15,6 +15,7 @@ issues:
|
|||
- kind/feature
|
||||
- kind/question
|
||||
- kind/style
|
||||
- kind/workaround
|
||||
- bug/need-confirmation
|
||||
- bug/confirmed
|
||||
- status/discuss
|
||||
|
|
|
@ -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 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
|
||||
|
||||
|
@ -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 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
|
||||
|
||||
**_Portainer_** has full support for the following Docker versions:
|
||||
|
|
|
@ -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, ®istry)
|
||||
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, ®istry)
|
||||
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
|
||||
}
|
|
@ -124,8 +124,11 @@ func (store *Store) MigrateData() error {
|
|||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
TemplateService: store.TemplateService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
|
|
|
@ -218,8 +218,6 @@ func (store *Store) Init() error {
|
|||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentBrowseGet: true,
|
||||
portainer.OperationDockerAgentBrowseList: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
|
@ -342,11 +340,6 @@ func (store *Store) Init() error {
|
|||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentBrowseDelete: true,
|
||||
portainer.OperationDockerAgentBrowseGet: true,
|
||||
portainer.OperationDockerAgentBrowseList: true,
|
||||
portainer.OperationDockerAgentBrowsePut: true,
|
||||
portainer.OperationDockerAgentBrowseRename: true,
|
||||
portainer.OperationDockerAgentUndefined: true,
|
||||
portainer.OperationPortainerResourceControlCreate: true,
|
||||
portainer.OperationPortainerResourceControlUpdate: true,
|
||||
|
@ -413,8 +406,6 @@ func (store *Store) Init() error {
|
|||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentBrowseGet: true,
|
||||
portainer.OperationDockerAgentBrowseList: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -8,8 +8,11 @@ import (
|
|||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"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/stack"
|
||||
"github.com/portainer/portainer/api/bolt/teammembership"
|
||||
"github.com/portainer/portainer/api/bolt/template"
|
||||
"github.com/portainer/portainer/api/bolt/user"
|
||||
"github.com/portainer/portainer/api/bolt/version"
|
||||
|
@ -25,8 +28,11 @@ type (
|
|||
extensionService *extension.Service
|
||||
registryService *registry.Service
|
||||
resourceControlService *resourcecontrol.Service
|
||||
roleService *role.Service
|
||||
scheduleService *schedule.Service
|
||||
settingsService *settings.Service
|
||||
stackService *stack.Service
|
||||
teamMembershipService *teammembership.Service
|
||||
templateService *template.Service
|
||||
userService *user.Service
|
||||
versionService *version.Service
|
||||
|
@ -42,8 +48,11 @@ type (
|
|||
ExtensionService *extension.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
StackService *stack.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TemplateService *template.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
|
@ -61,7 +70,10 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
|||
extensionService: parameters.ExtensionService,
|
||||
registryService: parameters.RegistryService,
|
||||
resourceControlService: parameters.ResourceControlService,
|
||||
roleService: parameters.RoleService,
|
||||
scheduleService: parameters.ScheduleService,
|
||||
settingsService: parameters.SettingsService,
|
||||
teamMembershipService: parameters.TeamMembershipService,
|
||||
templateService: parameters.TemplateService,
|
||||
stackService: parameters.StackService,
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -66,18 +66,24 @@ func (service *Service) Roles() ([]portainer.Role, error) {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ func initLDAPService() portainer.LDAPService {
|
|||
}
|
||||
|
||||
func initGitService() portainer.GitService {
|
||||
return &git.Service{}
|
||||
return git.NewService()
|
||||
}
|
||||
|
||||
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{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
SnapshotInterval: *flags.SnapshotInterval,
|
||||
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)
|
||||
extension.Enabled = false
|
||||
extension.License.Valid = false
|
||||
extensionService.Persist(&extension)
|
||||
}
|
||||
|
||||
err = extensionService.Persist(&extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return extensionManager, nil
|
||||
|
@ -618,7 +624,7 @@ func main() {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
adminPasswordHash, err = cryptoService.Hash(string(content))
|
||||
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -638,23 +644,7 @@ func main() {
|
|||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||
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,
|
||||
},
|
||||
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
err := store.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
|
|
|
@ -19,7 +19,8 @@ func NewJobScheduler() *JobScheduler {
|
|||
|
||||
// ScheduleJob schedules the execution of a job via a runner
|
||||
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
|
||||
|
@ -35,7 +36,7 @@ func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType
|
|||
|
||||
for _, entry := range cronEntries {
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType {
|
||||
err := newCron.AddJob(newCronExpression, entry.Job)
|
||||
_, err := newCron.AddJob(newCronExpression, entry.Job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -69,7 +70,7 @@ func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) err
|
|||
jobRunner = entry.Job
|
||||
}
|
||||
|
||||
err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
|
||||
_, err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package docker
|
|||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
|
@ -10,7 +11,7 @@ import (
|
|||
"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())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -22,44 +23,44 @@ func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
|
|||
|
||||
err = snapshotInfo(snapshot, cli)
|
||||
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 {
|
||||
err = snapshotSwarmServices(snapshot, cli)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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()
|
||||
|
|
|
@ -24,5 +24,5 @@ func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*p
|
|||
}
|
||||
defer cli.Close()
|
||||
|
||||
return snapshot(cli)
|
||||
return snapshot(cli, endpoint)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
|
@ -193,6 +194,7 @@ func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
|||
|
||||
err := licenseCheckProcess.Run()
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,37 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
"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.
|
||||
type Service struct{}
|
||||
type Service struct {
|
||||
httpsCli *http.Client
|
||||
}
|
||||
|
||||
// NewService initializes a new service.
|
||||
func NewService(dataStorePath string) (*Service, error) {
|
||||
service := &Service{}
|
||||
func NewService() *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
|
||||
|
@ -32,7 +48,7 @@ func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, refer
|
|||
return cloneRepository(repositoryURL, referenceName, destination)
|
||||
}
|
||||
|
||||
func cloneRepository(repositoryURL, referenceName string, destination string) error {
|
||||
func cloneRepository(repositoryURL, referenceName, destination string) error {
|
||||
options := &git.CloneOptions{
|
||||
URL: repositoryURL,
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
|
@ -100,23 +100,7 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
|||
user := &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||
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,
|
||||
},
|
||||
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
|
@ -137,56 +121,11 @@ func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User)
|
|||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
|
|
|
@ -113,23 +113,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
|||
user = &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||
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,
|
||||
},
|
||||
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -25,9 +25,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
|
||||
updateAuthorizations = true
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package endpointgroups
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
@ -53,12 +54,15 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
|||
endpointGroup.Tags = payload.Tags
|
||||
}
|
||||
|
||||
if payload.UserAccessPolicies != nil {
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.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
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ type Handler struct {
|
|||
*mux.Router
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
|
@ -22,18 +23,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
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",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||
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}",
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -25,10 +25,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
requestBouncer: bouncer,
|
||||
}
|
||||
h.PathPrefix("/{id}/azure").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
||||
h.PathPrefix("/{id}/docker").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||
h.PathPrefix("/{id}/storidge").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
|
|
|
@ -2,12 +2,12 @@ package endpoints
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
@ -192,9 +192,9 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
|||
Snapshots: []portainer.Snapshot{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
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
|
||||
|
@ -238,9 +238,9 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
|||
EdgeKey: edgeKey,
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
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
|
||||
|
@ -344,17 +344,37 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint)
|
|||
snapshot, err := handler.Snapshotter.CreateSnapshot(endpoint)
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
if err != nil {
|
||||
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
if strings.Contains(err.Error(), "Invalid request signature") {
|
||||
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 {
|
||||
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
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
|
||||
|
|
|
@ -43,5 +43,12 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
|||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package endpoints
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -76,12 +77,15 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
endpoint.Tags = payload.Tags
|
||||
}
|
||||
|
||||
if payload.UserAccessPolicies != nil {
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.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
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ type Handler struct {
|
|||
JobService portainer.JobService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
SettingsService portainer.SettingsService
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
|
@ -48,25 +49,25 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
|||
}
|
||||
|
||||
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",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
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",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ type Handler struct {
|
|||
EndpointGroupService portainer.EndpointGroupService
|
||||
EndpointService portainer.EndpointService
|
||||
RegistryService portainer.RegistryService
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage extension operations.
|
||||
|
@ -26,15 +27,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
}
|
||||
|
||||
h.Handle("/extensions",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
||||
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",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package extensions
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||
tmp := policies[key]
|
||||
|
@ -33,7 +35,6 @@ func (handler *Handler) upgradeRBACData() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
|
@ -55,5 +56,6 @@ func (handler *Handler) upgradeRBACData() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/support"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/schedules"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/roles"
|
||||
|
@ -48,6 +50,7 @@ type Handler struct {
|
|||
SettingsHandler *settings.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
SupportHandler *support.Handler
|
||||
TagHandler *tags.Handler
|
||||
TeamMembershipHandler *teammemberships.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)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/status"):
|
||||
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"):
|
||||
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
|
|
|
@ -18,7 +18,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/motd",
|
||||
bouncer.AuthorizedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -33,19 +33,19 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
}
|
||||
|
||||
h.Handle("/registries",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
||||
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",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost)
|
||||
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(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package registries
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
|
@ -11,19 +10,16 @@ import (
|
|||
)
|
||||
|
||||
type registryUpdatePayload struct {
|
||||
Name string
|
||||
URL string
|
||||
Authentication bool
|
||||
Username string
|
||||
Password string
|
||||
Name *string
|
||||
URL *string
|
||||
Authentication *bool
|
||||
Username *string
|
||||
Password *string
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -47,33 +43,42 @@ 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}
|
||||
}
|
||||
|
||||
if payload.Name != nil {
|
||||
registry.Name = *payload.Name
|
||||
}
|
||||
|
||||
if payload.URL != nil {
|
||||
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 {
|
||||
if r.URL == *payload.URL && r.ID != registry.ID {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
registry.Name = payload.Name
|
||||
registry.URL = *payload.URL
|
||||
}
|
||||
|
||||
if payload.URL != "" {
|
||||
registry.URL = payload.URL
|
||||
}
|
||||
|
||||
if payload.Authentication {
|
||||
if payload.Authentication != nil {
|
||||
if *payload.Authentication {
|
||||
registry.Authentication = true
|
||||
registry.Username = payload.Username
|
||||
registry.Password = payload.Password
|
||||
|
||||
if payload.Username != nil {
|
||||
registry.Username = *payload.Username
|
||||
}
|
||||
|
||||
if payload.Password != nil {
|
||||
registry.Password = *payload.Password
|
||||
}
|
||||
|
||||
} else {
|
||||
registry.Authentication = false
|
||||
registry.Username = ""
|
||||
registry.Password = ""
|
||||
}
|
||||
}
|
||||
|
||||
if payload.UserAccessPolicies != nil {
|
||||
registry.UserAccessPolicies = payload.UserAccessPolicies
|
||||
|
|
|
@ -21,11 +21,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
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}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/roles",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.roleList))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.roleList))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -28,18 +28,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
}
|
||||
|
||||
h.Handle("/schedules",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
||||
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",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@ type Handler struct {
|
|||
FileService portainer.FileService
|
||||
JobScheduler portainer.JobScheduler
|
||||
ScheduleService portainer.ScheduleService
|
||||
RoleService portainer.RoleService
|
||||
ExtensionService portainer.ExtensionService
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
|
@ -30,13 +33,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
||||
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",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ type publicSettingsResponse struct {
|
|||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
ExternalTemplates bool `json:"ExternalTemplates"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
|
@ -31,6 +32,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
|||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
|
||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||
ExternalTemplates: false,
|
||||
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||
|
|
|
@ -19,6 +19,7 @@ type settingsUpdatePayload struct {
|
|||
OAuthSettings *portainer.OAuthSettings
|
||||
AllowBindMountsForRegularUsers *bool
|
||||
AllowPrivilegedModeForRegularUsers *bool
|
||||
AllowVolumeBrowserForRegularUsers *bool
|
||||
EnableHostManagementFeatures *bool
|
||||
SnapshotInterval *string
|
||||
TemplatesURL *string
|
||||
|
@ -93,6 +94,12 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
||||
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.EnableHostManagementFeatures != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
settings.SnapshotInterval = snapshotInterval
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -238,7 +240,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
|||
}
|
||||
|
||||
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 {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
|
||||
}
|
||||
|
@ -271,6 +273,7 @@ type composeStackDeploymentConfig struct {
|
|||
endpoint *portainer.Endpoint
|
||||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
isAdmin bool
|
||||
}
|
||||
|
||||
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,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
|
@ -306,12 +310,34 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
|
|||
// clean it. Hence the use of the mutex.
|
||||
// We should contribute to libcompose to support authentication without using the config.json file.
|
||||
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()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -290,6 +292,7 @@ type swarmStackDeploymentConfig struct {
|
|||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
prune bool
|
||||
isAdmin bool
|
||||
}
|
||||
|
||||
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,
|
||||
registries: filteredRegistries,
|
||||
prune: prune,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
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()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ type Handler struct {
|
|||
DockerHubService portainer.DockerHubService
|
||||
SwarmStackManager portainer.SwarmStackManager
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage stack operations.
|
||||
|
@ -36,18 +37,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
requestBouncer: bouncer,
|
||||
}
|
||||
h.Handle("/stacks",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost)
|
||||
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}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackInspect))).Methods(http.MethodGet)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackInspect))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
|
||||
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",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/cli/cli/compose/types"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"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)}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status) *Han
|
|||
}
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/status/version",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -21,11 +21,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/tags",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
type Handler struct {
|
||||
*mux.Router
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team membership operations.
|
||||
|
@ -23,13 +23,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
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",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, membership)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ type Handler struct {
|
|||
*mux.Router
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team operations.
|
||||
|
@ -23,17 +23,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/teams",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -27,15 +27,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
}
|
||||
|
||||
h.Handle("/templates",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
|
||||
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}",
|
||||
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}",
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -45,23 +45,7 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe
|
|||
user := &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||
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,
|
||||
},
|
||||
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||
|
|
|
@ -23,6 +23,7 @@ type Handler struct {
|
|||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
SettingsService portainer.SettingsService
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage user operations.
|
||||
|
@ -31,19 +32,19 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/users",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
|
||||
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}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/memberships",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet)
|
||||
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",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet)
|
||||
h.Handle("/users/admin/init",
|
||||
|
|
|
@ -60,23 +60,7 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
|||
user = &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.UserRole(payload.Role),
|
||||
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||
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,
|
||||
},
|
||||
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
|
|
|
@ -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 {
|
||||
err := handler.UserService.DeleteUser(portainer.UserID(user.ID))
|
||||
err := handler.UserService.DeleteUser(user.ID)
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package users
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"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}
|
||||
}
|
||||
|
||||
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))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err}
|
||||
|
|
|
@ -24,11 +24,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/webhooks",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost)
|
||||
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}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/webhooks/{token}",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost)
|
||||
return h
|
||||
|
|
|
@ -26,8 +26,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
requestBouncer: bouncer,
|
||||
}
|
||||
h.PathPrefix("/websocket/exec").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketExec)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))
|
||||
h.PathPrefix("/websocket/attach").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketAttach)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketAttach)))
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -19,12 +19,14 @@ type (
|
|||
dockerTransport *http.Transport
|
||||
enableSignature bool
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
UserService portainer.UserService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SettingsService portainer.SettingsService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionService portainer.ExtensionService
|
||||
endpointIdentifier portainer.EndpointID
|
||||
endpointType portainer.EndpointType
|
||||
}
|
||||
|
@ -112,11 +114,29 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
|
|||
return p.proxyBuildRequest(request)
|
||||
case strings.HasPrefix(path, "/images"):
|
||||
return p.proxyImageRequest(request)
|
||||
case strings.HasPrefix(path, "/v2"):
|
||||
return p.proxyAgentRequest(request)
|
||||
default:
|
||||
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) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/configs/create":
|
||||
|
@ -360,7 +380,63 @@ func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID s
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -498,7 +574,12 @@ func (p *proxyTransport) createOperationContext(request *http.Request) (*restric
|
|||
if tokenData.Role != portainer.AdministratorRole {
|
||||
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 {
|
||||
operationContext.endpointResourceAccess = true
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@ const AzureAPIBaseURL = "https://management.azure.com"
|
|||
// proxyFactory is a factory to create reverse proxies to Docker endpoints
|
||||
type proxyFactory struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
UserService portainer.UserService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionService portainer.ExtensionService
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||
|
@ -70,11 +72,13 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, endpoint *port
|
|||
transport := &proxyTransport{
|
||||
enableSignature: enableSignature,
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
UserService: factory.UserService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
RegistryService: factory.RegistryService,
|
||||
DockerHubService: factory.DockerHubService,
|
||||
ReverseTunnelService: factory.ReverseTunnelService,
|
||||
ExtensionService: factory.ExtensionService,
|
||||
dockerTransport: &http.Transport{},
|
||||
endpointIdentifier: endpoint.ID,
|
||||
endpointType: endpoint.Type,
|
||||
|
|
|
@ -13,10 +13,12 @@ func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endp
|
|||
transport := &proxyTransport{
|
||||
enableSignature: false,
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
UserService: factory.UserService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
RegistryService: factory.RegistryService,
|
||||
DockerHubService: factory.DockerHubService,
|
||||
ExtensionService: factory.ExtensionService,
|
||||
dockerTransport: newSocketTransport(path),
|
||||
ReverseTunnelService: factory.ReverseTunnelService,
|
||||
endpointIdentifier: endpoint.ID,
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
|
@ -14,11 +16,13 @@ func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endp
|
|||
transport := &proxyTransport{
|
||||
enableSignature: false,
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
UserService: factory.UserService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
RegistryService: factory.RegistryService,
|
||||
DockerHubService: factory.DockerHubService,
|
||||
ReverseTunnelService: factory.ReverseTunnelService,
|
||||
ExtensionService: factory.ExtensionService,
|
||||
dockerTransport: newNamedPipeTransport(path),
|
||||
endpointIdentifier: endpoint.ID,
|
||||
endpointType: endpoint.Type,
|
||||
|
|
|
@ -31,12 +31,14 @@ type (
|
|||
// ManagerParams represents the required parameters to create a new Manager instance.
|
||||
ManagerParams struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
UserService portainer.UserService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionService portainer.ExtensionService
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -48,12 +50,14 @@ func NewManager(parameters *ManagerParams) *Manager {
|
|||
legacyExtensionProxies: cmap.New(),
|
||||
proxyFactory: &proxyFactory{
|
||||
ResourceControlService: parameters.ResourceControlService,
|
||||
UserService: parameters.UserService,
|
||||
TeamMembershipService: parameters.TeamMembershipService,
|
||||
SettingsService: parameters.SettingsService,
|
||||
RegistryService: parameters.RegistryService,
|
||||
DockerHubService: parameters.DockerHubService,
|
||||
SignatureService: parameters.SignatureService,
|
||||
ReverseTunnelService: parameters.ReverseTunnelService,
|
||||
ExtensionService: parameters.ExtensionService,
|
||||
},
|
||||
reverseTunnelService: parameters.ReverseTunnelService,
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ type (
|
|||
}
|
||||
|
||||
// RestrictedRequestContext is a data structure containing information
|
||||
// used in RestrictedAccess
|
||||
// used in AuthenticatedAccess
|
||||
RestrictedRequestContext struct {
|
||||
IsAdmin bool
|
||||
IsTeamLeader bool
|
||||
|
@ -64,22 +64,40 @@ func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
|||
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.
|
||||
// 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.
|
||||
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.mwCheckPortainerAuthorizations(h)
|
||||
h = bouncer.mwCheckPortainerAuthorizations(h, true)
|
||||
h = bouncer.mwAuthenticatedUser(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// RestrictedAccess defines a security check for restricted API 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
|
||||
// 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 {
|
||||
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.mwAuthenticatedUser(h)
|
||||
return h
|
||||
|
@ -142,10 +160,15 @@ func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Reque
|
|||
return err
|
||||
}
|
||||
|
||||
user, err := bouncer.userService.User(tokenData.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
||||
Path: r.URL.String(),
|
||||
Method: r.Method,
|
||||
Authorizations: tokenData.EndpointAuthorizations[endpoint.ID],
|
||||
Authorizations: user.EndpointAuthorizations[endpoint.ID],
|
||||
}
|
||||
|
||||
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
|
||||
// a specific API endpoint. It will leverage the RBAC extension authorization validation if the extension
|
||||
// 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) {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied)
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -201,6 +226,11 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler)
|
|||
|
||||
extension, err := bouncer.extensionService.Extension(portainer.RBACExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
if administratorOnly {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
} else if err != nil {
|
||||
|
@ -208,10 +238,19 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler)
|
|||
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{
|
||||
Path: r.URL.String(),
|
||||
Method: r.Method,
|
||||
Authorizations: tokenData.PortainerAuthorizations,
|
||||
Authorizations: user.PortainerAuthorizations,
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
} 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
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -3,6 +3,8 @@ package http
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/support"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/roles"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
|
@ -84,15 +86,27 @@ type Server struct {
|
|||
func (server *Server) Start() error {
|
||||
proxyManagerParameters := &proxy.ManagerParams{
|
||||
ResourceControlService: server.ResourceControlService,
|
||||
UserService: server.UserService,
|
||||
TeamMembershipService: server.TeamMembershipService,
|
||||
SettingsService: server.SettingsService,
|
||||
RegistryService: server.RegistryService,
|
||||
DockerHubService: server.DockerHubService,
|
||||
SignatureService: server.SignatureService,
|
||||
ReverseTunnelService: server.ReverseTunnelService,
|
||||
ExtensionService: server.ExtensionService,
|
||||
}
|
||||
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{
|
||||
JWTService: server.JWTService,
|
||||
UserService: server.UserService,
|
||||
|
@ -136,10 +150,12 @@ func (server *Server) Start() error {
|
|||
endpointHandler.JobService = server.JobService
|
||||
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
endpointHandler.SettingsService = server.SettingsService
|
||||
endpointHandler.AuthorizationService = authorizationService
|
||||
|
||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
||||
endpointGroupHandler.EndpointService = server.EndpointService
|
||||
endpointGroupHandler.AuthorizationService = authorizationService
|
||||
|
||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||
endpointProxyHandler.EndpointService = server.EndpointService
|
||||
|
@ -157,6 +173,7 @@ func (server *Server) Start() error {
|
|||
extensionHandler.EndpointGroupService = server.EndpointGroupService
|
||||
extensionHandler.EndpointService = server.EndpointService
|
||||
extensionHandler.RegistryService = server.RegistryService
|
||||
extensionHandler.AuthorizationService = authorizationService
|
||||
|
||||
var registryHandler = registries.NewHandler(requestBouncer)
|
||||
registryHandler.RegistryService = server.RegistryService
|
||||
|
@ -182,6 +199,9 @@ func (server *Server) Start() error {
|
|||
settingsHandler.FileService = server.FileService
|
||||
settingsHandler.JobScheduler = server.JobScheduler
|
||||
settingsHandler.ScheduleService = server.ScheduleService
|
||||
settingsHandler.RoleService = server.RoleService
|
||||
settingsHandler.ExtensionService = server.ExtensionService
|
||||
settingsHandler.AuthorizationService = authorizationService
|
||||
|
||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||
stackHandler.FileService = server.FileService
|
||||
|
@ -193,6 +213,7 @@ func (server *Server) Start() error {
|
|||
stackHandler.GitService = server.GitService
|
||||
stackHandler.RegistryService = server.RegistryService
|
||||
stackHandler.DockerHubService = server.DockerHubService
|
||||
stackHandler.SettingsService = server.SettingsService
|
||||
|
||||
var tagHandler = tags.NewHandler(requestBouncer)
|
||||
tagHandler.TagService = server.TagService
|
||||
|
@ -200,11 +221,16 @@ func (server *Server) Start() error {
|
|||
var teamHandler = teams.NewHandler(requestBouncer)
|
||||
teamHandler.TeamService = server.TeamService
|
||||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||
teamHandler.AuthorizationService = authorizationService
|
||||
|
||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
|
||||
teamMembershipHandler.AuthorizationService = authorizationService
|
||||
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
|
||||
var supportHandler = support.NewHandler(requestBouncer)
|
||||
|
||||
var templatesHandler = templates.NewHandler(requestBouncer)
|
||||
templatesHandler.TemplateService = server.TemplateService
|
||||
templatesHandler.SettingsService = server.SettingsService
|
||||
|
@ -219,6 +245,7 @@ func (server *Server) Start() error {
|
|||
userHandler.CryptoService = server.CryptoService
|
||||
userHandler.ResourceControlService = server.ResourceControlService
|
||||
userHandler.SettingsService = server.SettingsService
|
||||
userHandler.AuthorizationService = authorizationService
|
||||
|
||||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||
websocketHandler.EndpointService = server.EndpointService
|
||||
|
@ -245,6 +272,7 @@ func (server *Server) Start() error {
|
|||
SettingsHandler: settingsHandler,
|
||||
StatusHandler: statusHandler,
|
||||
StackHandler: stackHandler,
|
||||
SupportHandler: supportHandler,
|
||||
TagHandler: tagHandler,
|
||||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
|
|
|
@ -19,8 +19,6 @@ type claims struct {
|
|||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
EndpointAuthorizations portainer.EndpointAuthorizations `json:"endpointAuthorizations"`
|
||||
PortainerAuthorizations portainer.Authorizations `json:"portainerAuthorizations"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
|
@ -40,12 +38,10 @@ func NewService() (*Service, error) {
|
|||
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
|
||||
expireToken := time.Now().Add(time.Hour * 8).Unix()
|
||||
cl := claims{
|
||||
int(data.ID),
|
||||
data.Username,
|
||||
int(data.Role),
|
||||
data.EndpointAuthorizations,
|
||||
data.PortainerAuthorizations,
|
||||
jwt.StandardClaims{
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
Role: int(data.Role),
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expireToken,
|
||||
},
|
||||
}
|
||||
|
@ -74,8 +70,6 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData,
|
|||
ID: portainer.UserID(cl.UserID),
|
||||
Username: cl.Username,
|
||||
Role: portainer.UserRole(cl.Role),
|
||||
EndpointAuthorizations: cl.EndpointAuthorizations,
|
||||
PortainerAuthorizations: cl.PortainerAuthorizations,
|
||||
}
|
||||
return tokenData, nil
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@ type (
|
|||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
|
@ -123,6 +124,7 @@ type (
|
|||
Password string `json:"Password,omitempty"`
|
||||
Role UserRole `json:"Role"`
|
||||
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
|
||||
EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"`
|
||||
}
|
||||
|
||||
// UserID represents a user identifier
|
||||
|
@ -163,8 +165,6 @@ type (
|
|||
ID UserID
|
||||
Username string
|
||||
Role UserRole
|
||||
EndpointAuthorizations EndpointAuthorizations
|
||||
PortainerAuthorizations Authorizations
|
||||
}
|
||||
|
||||
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
|
||||
|
@ -638,7 +638,8 @@ type (
|
|||
RoleService interface {
|
||||
Role(ID RoleID) (*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
|
||||
|
@ -902,15 +903,19 @@ type (
|
|||
|
||||
const (
|
||||
// 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 = 19
|
||||
DBVersion = 20
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||
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 = 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 = "Portainer-Agent"
|
||||
// PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster
|
||||
|
|
|
@ -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).
|
||||
|
||||
version: "1.22.0"
|
||||
version: "1.22.1"
|
||||
title: "Portainer API"
|
||||
contact:
|
||||
email: "info@portainer.io"
|
||||
|
@ -254,7 +254,7 @@ paths:
|
|||
- name: "EndpointType"
|
||||
in: "formData"
|
||||
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
|
||||
- name: "URL"
|
||||
in: "formData"
|
||||
|
@ -3174,7 +3174,7 @@ definitions:
|
|||
description: "Is analytics enabled"
|
||||
Version:
|
||||
type: "string"
|
||||
example: "1.22.0"
|
||||
example: "1.22.1"
|
||||
description: "Portainer API version"
|
||||
PublicSettingsInspectResponse:
|
||||
type: "object"
|
||||
|
@ -3920,6 +3920,10 @@ definitions:
|
|||
type: "boolean"
|
||||
example: true
|
||||
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:
|
||||
type: "object"
|
||||
required:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"packageName": "portainer",
|
||||
"packageVersion": "1.22.0",
|
||||
"packageVersion": "1.22.1",
|
||||
"projectName": "portainer"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import '../assets/css/app.css';
|
||||
import './libraries/isteven-angular-multiselect/isteven-multi-select.css';
|
||||
import angular from 'angular';
|
||||
|
||||
import './agent/_module';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<rd-widget-body classes="no-padding">
|
||||
<div class="searchBar">
|
||||
<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>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
|
|
|
@ -11,6 +11,7 @@ angular.module('portainer')
|
|||
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
|
||||
.constant('API_ENDPOINT_STACKS', 'api/stacks')
|
||||
.constant('API_ENDPOINT_STATUS', 'api/status')
|
||||
.constant('API_ENDPOINT_SUPPORT', 'api/support')
|
||||
.constant('API_ENDPOINT_USERS', 'api/users')
|
||||
.constant('API_ENDPOINT_TAGS', 'api/tags')
|
||||
.constant('API_ENDPOINT_TEAMS', 'api/teams')
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -136,7 +136,7 @@
|
|||
></containers-datatable-actions>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
|
|
|
@ -172,6 +172,7 @@ function ($scope, $controller, DatatableService, EndpointProvider) {
|
|||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
|
@ -203,6 +204,5 @@ function ($scope, $controller, DatatableService, EndpointProvider) {
|
|||
this.columnVisibility = storedColumnVisibility;
|
||||
this.columnVisibility.state.open = false;
|
||||
}
|
||||
this.state.orderBy = this.orderBy;
|
||||
};
|
||||
}]);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
|
|
|
@ -108,6 +108,7 @@ angular.module('portainer.docker')
|
|||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
|
@ -135,7 +136,6 @@ angular.module('portainer.docker')
|
|||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
this.state.orderBy = this.orderBy;
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
|
|
|
@ -39,6 +39,7 @@ function ($scope, $controller, DatatableService) {
|
|||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
|
@ -66,6 +67,5 @@ function ($scope, $controller, DatatableService) {
|
|||
}
|
||||
|
||||
this.onSettingsRepeaterChange();
|
||||
this.state.orderBy = this.orderBy;
|
||||
};
|
||||
}]);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}]);
|
|
@ -55,17 +55,22 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<th style="width:55px;">
|
||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.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')">
|
||||
Name
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td>
|
||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.disableRemove(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</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 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))"
|
||||
ng-class="{active: item.Checked}" network-row-content item="item" parent-ctrl="$ctrl" allow-checkbox="true">
|
||||
</tr>
|
||||
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="it in item.Subs" style="background: #d5e8f3;"
|
||||
network-row-content item="it" parent-ctrl="$ctrl">
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="9" class="text-center text-muted">Loading...</td>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('NetworksDatatableController', ['$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);
|
||||
};
|
||||
|
||||
this.state = Object.assign(this.state, {
|
||||
expandedItems: []
|
||||
})
|
||||
|
||||
/**
|
||||
* Do not allow PREDEFINED_NETWORKS to be selected
|
||||
*/
|
||||
|
@ -19,6 +25,7 @@ angular.module('portainer.docker')
|
|||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
|
@ -45,7 +52,27 @@ angular.module('portainer.docker')
|
|||
this.settings.open = false;
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
]);
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -61,6 +61,7 @@ angular.module('portainer.docker')
|
|||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
|
@ -87,7 +88,6 @@ angular.module('portainer.docker')
|
|||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
this.state.orderBy = this.orderBy;
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
></services-datatable-actions>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -68,6 +68,7 @@ function ($scope, $controller, DatatableService, EndpointProvider) {
|
|||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
|
@ -99,6 +100,5 @@ function ($scope, $controller, DatatableService, EndpointProvider) {
|
|||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
this.state.orderBy = this.orderBy;
|
||||
};
|
||||
}]);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
|
|
@ -16,6 +16,7 @@ function ($scope, $controller, DatatableService) {
|
|||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
|
@ -42,6 +43,5 @@ function ($scope, $controller, DatatableService) {
|
|||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
this.state.orderBy = this.orderBy;
|
||||
};
|
||||
}]);
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
|
@ -149,9 +149,11 @@
|
|||
</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>
|
||||
<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">
|
||||
<i class="fa fa-search"></i> browse</a>
|
||||
<btn authorization="DockerAgentBrowseList" ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode">
|
||||
<a ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })" class="btn btn-xs btn-primary space-left">
|
||||
<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>
|
||||
</td>
|
||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue