THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

Browse Source

feat(api): relocate authorizations outside of JWT (#3079)

* feat(api): relocate authorizations outside of JWT

* fix(api): update user authorization after enabling the RBAC extension

* feat(api): add PortainerEndpointList operation in the default portainer authorizations

* feat(auth): retrieve authorization from API instead of JWT

* refactor(auth): move permissions retrieval to function

* refactor(api): document authorizations methods
tags/1.22.1^2
Anthony Lapenna GitHub 5 months ago
parent
commit
7d76bc89e7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 472 additions and 303 deletions
  1. +266
    -0
      api/authorizations.go
  2. +2
    -0
      api/bolt/datastore.go
  3. +29
    -0
      api/bolt/migrator/migrate_dbversion19.go
  4. +16
    -0
      api/bolt/migrator/migrator.go
  5. +4
    -20
      api/cmd/portainer/main.go
  6. +6
    -67
      api/http/handler/auth/authenticate.go
  7. +3
    -19
      api/http/handler/auth/authenticate_oauth.go
  8. +0
    -122
      api/http/handler/auth/authorization.go
  9. +10
    -0
      api/http/handler/endpointgroups/endpointgroup_update.go
  10. +1
    -0
      api/http/handler/endpointgroups/handler.go
  11. +10
    -0
      api/http/handler/endpoints/endpoint_update.go
  12. +1
    -0
      api/http/handler/endpoints/handler.go
  13. +1
    -0
      api/http/handler/extensions/handler.go
  14. +12
    -1
      api/http/handler/extensions/upgrade.go
  15. +3
    -19
      api/http/handler/users/admin_init.go
  16. +3
    -19
      api/http/handler/users/user_create.go
  17. +11
    -0
      api/http/handler/users/user_inspect.go
  18. +7
    -1
      api/http/proxy/docker_transport.go
  19. +2
    -0
      api/http/proxy/factory.go
  20. +1
    -0
      api/http/proxy/factory_local.go
  21. +3
    -1
      api/http/proxy/factory_local_windows.go
  22. +2
    -0
      api/http/proxy/manager.go
  23. +17
    -3
      api/http/security/bouncer.go
  24. +13
    -0
      api/http/server.go
  25. +10
    -16
      api/jwt/jwt.go
  26. +10
    -11
      api/portainer.go
  27. +2
    -0
      app/portainer/models/user.js
  28. +11
    -4
      app/portainer/services/authentication.js
  29. +16
    -0
      app/portainer/views/auth/authController.js

+ 266
- 0
api/authorizations.go View File

@@ -0,0 +1,266 @@
package portainer

// AuthorizationService represents a service used to
// update authorizations associated to a user or team.
type AuthorizationService struct {
endpointService EndpointService
endpointGroupService EndpointGroupService
roleService RoleService
teamMembershipService TeamMembershipService
userService UserService
}

// AuthorizationServiceParameters are the required parameters
// used to create a new AuthorizationService.
type AuthorizationServiceParameters struct {
EndpointService EndpointService
EndpointGroupService EndpointGroupService
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,
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,
}
}

// UpdateUserAuthorizationsFromPolicies will update users authorizations based on the specified access policies.
func (service *AuthorizationService) UpdateUserAuthorizationsFromPolicies(userPolicies *UserAccessPolicies, teamPolicies *TeamAccessPolicies) error {

for userID, policy := range *userPolicies {
if policy.RoleID == 0 {
continue
}

err := service.UpdateUserAuthorizations(userID)
if err != nil {
return err
}
}

for teamID, policy := range *teamPolicies {
if policy.RoleID == 0 {
continue
}

err := service.updateUserAuthorizationsInTeam(teamID)
if err != nil {
return err
}
}

return nil
}

func (service *AuthorizationService) updateUserAuthorizationsInTeam(teamID TeamID) error {

memberships, err := service.teamMembershipService.TeamMembershipsByTeamID(teamID)
if err != nil {
return err
}

for _, membership := range memberships {
err := service.UpdateUserAuthorizations(membership.UserID)
if err != nil {
return err
}
}

return nil
}

// UpdateUserAuthorizations will trigger an update of the authorizations for the specified user.
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
}

endpointAuthorizations[endpoint.ID] = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
}

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
}

+ 2
- 0
api/bolt/datastore.go View File

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


+ 29
- 0
api/bolt/migrator/migrate_dbversion19.go View File

@@ -0,0 +1,29 @@
package migrator

import portainer "github.com/portainer/portainer/api"

func (m *Migrator) updateUsersToDBVersion20() error {
legacyUsers, err := m.userService.Users()
if err != nil {
return err
}

authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
EndpointService: m.endpointService,
EndpointGroupService: m.endpointGroupService,
RoleService: m.roleService,
TeamMembershipService: m.teamMembershipService,
UserService: m.userService,
}

authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)

for _, user := range legacyUsers {
err := authorizationService.UpdateUserAuthorizations(user.ID)
if err != nil {
return err
}
}

return nil
}

+ 16
- 0
api/bolt/migrator/migrator.go View File

@@ -8,8 +8,10 @@ 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/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 +27,10 @@ type (
extensionService *extension.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
settingsService *settings.Service
stackService *stack.Service
teamMembershipService *teammembership.Service
templateService *template.Service
userService *user.Service
versionService *version.Service
@@ -42,8 +46,10 @@ type (
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
SettingsService *settings.Service
StackService *stack.Service
TeamMembershipService *teammembership.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
@@ -61,7 +67,9 @@ func NewMigrator(parameters *Parameters) *Migrator {
extensionService: parameters.ExtensionService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,
settingsService: parameters.SettingsService,
teamMembershipService: parameters.TeamMembershipService,
templateService: parameters.TemplateService,
stackService: parameters.StackService,
userService: parameters.UserService,
@@ -257,5 +265,13 @@ func (m *Migrator) Migrate() error {
}
}

// Portainer 1.22.x
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return err
}
}

return m.versionService.StoreDBVersion(portainer.DBVersion)
}

+ 4
- 20
api/cmd/portainer/main.go View File

@@ -635,26 +635,10 @@ func main() {
if len(users) == 0 {
log.Printf("Creating admin user with password hash %s", adminPasswordHash)
user := &portainer.User{
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,
},
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
}
err := store.UserService.CreateUser(user)
if err != nil {


+ 6
- 67
api/http/handler/auth/authenticate.go View File

@@ -98,25 +98,9 @@ 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,
},
Username: username,
Role: portainer.StandardUserRole,
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
}

err = handler.UserService.CreateUser(user)
@@ -134,59 +118,14 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use

func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
tokenData := &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
PortainerAuthorizations: user.PortainerAuthorizations,
ID: user.ID,
Username: user.Username,
Role: user.Role,
}

_, 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 {


+ 3
- 19
api/http/handler/auth/authenticate_oauth.go View File

@@ -111,25 +111,9 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h

if user == nil {
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,
},
Username: username,
Role: portainer.StandardUserRole,
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
}

err = handler.UserService.CreateUser(user)


+ 0
- 122
api/http/handler/auth/authorization.go View File

@@ -1,122 +0,0 @@
package auth

import portainer "github.com/portainer/portainer/api"

func getUserEndpointAuthorizations(user *portainer.User, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, roles []portainer.Role, userMemberships []portainer.TeamMembership) portainer.EndpointAuthorizations {
endpointAuthorizations := make(portainer.EndpointAuthorizations)

groupUserAccessPolicies := map[portainer.EndpointGroupID]portainer.UserAccessPolicies{}
groupTeamAccessPolicies := map[portainer.EndpointGroupID]portainer.TeamAccessPolicies{}
for _, endpointGroup := range endpointGroups {
groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies
groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies
}

for _, endpoint := range endpoints {
authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}

authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}

authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}

endpointAuthorizations[endpoint.ID] = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
}

return endpointAuthorizations
}

func getAuthorizationsFromUserEndpointPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
policyRoles := make([]portainer.RoleID, 0)

policy, ok := endpoint.UserAccessPolicies[user.ID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}

return getAuthorizationsFromRoles(policyRoles, roles)
}

func getAuthorizationsFromUserEndpointGroupPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.UserAccessPolicies) portainer.Authorizations {
policyRoles := make([]portainer.RoleID, 0)

policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}

return getAuthorizationsFromRoles(policyRoles, roles)
}

func getAuthorizationsFromTeamEndpointPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
policyRoles := make([]portainer.RoleID, 0)

for _, membership := range memberships {
policy, ok := endpoint.TeamAccessPolicies[membership.TeamID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
}

return getAuthorizationsFromRoles(policyRoles, roles)
}

func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.TeamAccessPolicies) portainer.Authorizations {
policyRoles := make([]portainer.RoleID, 0)

for _, membership := range memberships {
policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
}

return getAuthorizationsFromRoles(policyRoles, roles)
}

func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []portainer.Role) portainer.Authorizations {
var roleAuthorizations []portainer.Authorizations
for _, id := range roleIdentifiers {
for _, role := range roles {
if role.ID == id {
roleAuthorizations = append(roleAuthorizations, role.Authorizations)
break
}
}
}

processedAuthorizations := make(portainer.Authorizations)
if len(roleAuthorizations) > 0 {
processedAuthorizations = roleAuthorizations[0]
for idx, authorizations := range roleAuthorizations {
if idx == 0 {
continue
}
processedAuthorizations = mergeAuthorizations(processedAuthorizations, authorizations)
}
}

return processedAuthorizations
}

func mergeAuthorizations(a, b portainer.Authorizations) portainer.Authorizations {
c := make(map[portainer.Authorization]bool)

for k := range b {
if _, ok := a[k]; ok {
c[k] = true
}
}
return c
}

+ 10
- 0
api/http/handler/endpointgroups/endpointgroup_update.go View File

@@ -53,12 +53,15 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
endpointGroup.Tags = payload.Tags
}

updateAuthorizations := false
if payload.UserAccessPolicies != nil {
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
updateAuthorizations = true
}

if payload.TeamAccessPolicies != nil {
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
updateAuthorizations = true
}

err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
@@ -66,5 +69,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.UpdateUserAuthorizationsFromPolicies(&payload.UserAccessPolicies, &payload.TeamAccessPolicies)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}

return response.JSON(w, endpointGroup)
}

+ 1
- 0
api/http/handler/endpointgroups/handler.go View File

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


+ 10
- 0
api/http/handler/endpoints/endpoint_update.go View File

@@ -76,12 +76,15 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.Tags = payload.Tags
}

updateAuthorizations := false
if payload.UserAccessPolicies != nil {
endpoint.UserAccessPolicies = payload.UserAccessPolicies
updateAuthorizations = true
}

if payload.TeamAccessPolicies != nil {
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
updateAuthorizations = true
}

if payload.Status != nil {
@@ -173,5 +176,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.UpdateUserAuthorizationsFromPolicies(&payload.UserAccessPolicies, &payload.TeamAccessPolicies)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}

return response.JSON(w, endpoint)
}

+ 1
- 0
api/http/handler/endpoints/handler.go View File

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


+ 1
- 0
api/http/handler/extensions/handler.go View File

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


+ 12
- 1
api/http/handler/extensions/upgrade.go View File

@@ -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]
@@ -34,6 +36,10 @@ func (handler *Handler) upgradeRBACData() error {
return err
}

err = handler.AuthorizationService.UpdateUserAuthorizationsFromPolicies(&endpointGroup.UserAccessPolicies, &endpointGroup.TeamAccessPolicies)
if err != nil {
return err
}
}

endpoints, err := handler.EndpointService.Endpoints()
@@ -54,6 +60,11 @@ func (handler *Handler) upgradeRBACData() error {
if err != nil {
return err
}

err = handler.AuthorizationService.UpdateUserAuthorizationsFromPolicies(&endpoint.UserAccessPolicies, &endpoint.TeamAccessPolicies)
if err != nil {
return err
}
}
return nil
}

+ 3
- 19
api/http/handler/users/admin_init.go View File

@@ -43,25 +43,9 @@ 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,
},
Username: payload.Username,
Role: portainer.AdministratorRole,
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
}

user.Password, err = handler.CryptoService.Hash(payload.Password)


+ 3
- 19
api/http/handler/users/user_create.go View File

@@ -58,25 +58,9 @@ 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,
},
Username: payload.Username,
Role: portainer.UserRole(payload.Role),
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
}

settings, err := handler.SettingsService.Settings()


+ 11
- 0
api/http/handler/users/user_inspect.go View File

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


+ 7
- 1
api/http/proxy/docker_transport.go View File

@@ -19,6 +19,7 @@ type (
dockerTransport *http.Transport
enableSignature bool
ResourceControlService portainer.ResourceControlService
UserService portainer.UserService
TeamMembershipService portainer.TeamMembershipService
RegistryService portainer.RegistryService
DockerHubService portainer.DockerHubService
@@ -498,7 +499,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
}


+ 2
- 0
api/http/proxy/factory.go View File

@@ -16,6 +16,7 @@ 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
@@ -70,6 +71,7 @@ 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,


+ 1
- 0
api/http/proxy/factory_local.go View File

@@ -13,6 +13,7 @@ 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,


+ 3
- 1
api/http/proxy/factory_local_windows.go View File

@@ -3,10 +3,11 @@
package proxy

import (
"github.com/Microsoft/go-winio"
"net"
"net/http"

"github.com/Microsoft/go-winio"

portainer "github.com/portainer/portainer/api"
)

@@ -15,6 +16,7 @@ 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,


+ 2
- 0
api/http/proxy/manager.go View File

@@ -31,6 +31,7 @@ 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
@@ -48,6 +49,7 @@ 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,


+ 17
- 3
api/http/security/bouncer.go View File

@@ -142,10 +142,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)
@@ -208,10 +213,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 +295,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 {


+ 13
- 0
api/http/server.go View File

@@ -84,6 +84,7 @@ 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,
@@ -93,6 +94,15 @@ func (server *Server) Start() error {
}
proxyManager := proxy.NewManager(proxyManagerParameters)

authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
EndpointService: server.EndpointService,
EndpointGroupService: server.EndpointGroupService,
RoleService: server.RoleService,
TeamMembershipService: server.TeamMembershipService,
UserService: server.UserService,
}
authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)

requestBouncerParameters := &security.RequestBouncerParams{
JWTService: server.JWTService,
UserService: server.UserService,
@@ -136,10 +146,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 +169,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


+ 10
- 16
api/jwt/jwt.go View File

@@ -16,11 +16,9 @@ type Service struct {
}

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"`
UserID int `json:"id"`
Username string `json:"username"`
Role int `json:"role"`
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,
},
}
@@ -71,11 +67,9 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData,
if err == nil && parsedToken != nil {
if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
tokenData := &portainer.TokenData{
ID: portainer.UserID(cl.UserID),
Username: cl.Username,
Role: portainer.UserRole(cl.Role),
EndpointAuthorizations: cl.EndpointAuthorizations,
PortainerAuthorizations: cl.PortainerAuthorizations,
ID: portainer.UserID(cl.UserID),
Username: cl.Username,
Role: portainer.UserRole(cl.Role),
}
return tokenData, nil
}


+ 10
- 11
api/portainer.go View File

@@ -118,11 +118,12 @@ type (

// User represents a user account
User struct {
ID UserID `json:"Id"`
Username string `json:"Username"`
Password string `json:"Password,omitempty"`
Role UserRole `json:"Role"`
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
ID UserID `json:"Id"`
Username string `json:"Username"`
Password string `json:"Password,omitempty"`
Role UserRole `json:"Role"`
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"`
}

// UserID represents a user identifier
@@ -160,11 +161,9 @@ type (

// TokenData represents the data embedded in a JWT token
TokenData struct {
ID UserID
Username string
Role UserRole
EndpointAuthorizations EndpointAuthorizations
PortainerAuthorizations Authorizations
ID UserID
Username string
Role UserRole
}

// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
@@ -904,7 +903,7 @@ const (
// APIVersion is the version number of the Portainer API
APIVersion = "1.22.0"
// 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


+ 2
- 0
app/portainer/models/user.js View File

@@ -2,6 +2,8 @@ export function UserViewModel(data) {
this.Id = data.Id;
this.Username = data.Username;
this.Role = data.Role;
this.EndpointAuthorizations = data.EndpointAuthorizations;
this.PortainerAuthorizations = data.PortainerAuthorizations;
if (data.Role === 1) {
this.RoleName = 'administrator';
} else {


+ 11
- 4
app/portainer/services/authentication.js View File

@@ -1,7 +1,7 @@
angular.module('portainer.app')
.factory('Authentication', [
'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider',
function AuthenticationFactory(Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider) {
'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', 'UserService',
function AuthenticationFactory(Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider, UserService) {
'use strict';

var service = {};
@@ -15,6 +15,7 @@ function AuthenticationFactory(Auth, OAuth, jwtHelper, LocalStorage, StateManage
service.getUserDetails = getUserDetails;
service.isAdmin = isAdmin;
service.hasAuthorizations = hasAuthorizations;
service.retrievePermissions = retrievePermissions;

function init() {
var jwt = LocalStorage.getJWT();
@@ -53,14 +54,20 @@ function AuthenticationFactory(Auth, OAuth, jwtHelper, LocalStorage, StateManage
return user;
}

function retrievePermissions() {
return UserService.user(user.ID)
.then((data) => {
user.endpointAuthorizations = data.EndpointAuthorizations;
user.portainerAuthorizations = data.PortainerAuthorizations;
});
}

function setUser(jwt) {
LocalStorage.storeJWT(jwt);
var tokenPayload = jwtHelper.decodeToken(jwt);
user.username = tokenPayload.username;
user.ID = tokenPayload.id;
user.role = tokenPayload.role;
user.endpointAuthorizations = tokenPayload.endpointAuthorizations;
user.portainerAuthorizations = tokenPayload.portainerAuthorizations;
}

function isAdmin() {


+ 16
- 0
app/portainer/views/auth/authController.js View File

@@ -29,12 +29,21 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us
}
}

function permissionsError() {
$scope.state.permissionsError = true;
Authentication.logout();
$scope.state.AuthenticationError = 'Unable to retrieve permissions.'
$scope.state.loginInProgress = false;
return Promise.reject();
}

$scope.authenticateUser = function() {
var username = $scope.formValues.Username;
var password = $scope.formValues.Password;
$scope.state.loginInProgress = true;

Authentication.login(username, password)
.then(() => Authentication.retrievePermissions().catch(permissionsError))
.then(function success() {
return retrieveAndSaveEnabledExtensions();
})
@@ -42,6 +51,9 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us
checkForEndpoints();
})
.catch(function error() {
if ($scope.state.permissionsError) {
return;
}
SettingsService.publicSettings()
.then(function success(settings) {
if (settings.AuthenticationMethod === 1) {
@@ -166,6 +178,7 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us

function oAuthLogin(code) {
return Authentication.OAuthLogin(code)
.then(() => Authentication.retrievePermissions().catch(permissionsError))
.then(function success() {
return retrieveAndSaveEnabledExtensions();
})
@@ -173,6 +186,9 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us
URLHelper.cleanParameters();
})
.catch(function error() {
if ($scope.state.permissionsError) {
return;
}
$scope.state.AuthenticationError = 'Unable to login via OAuth';
$scope.state.isInOAuthProcess = false;
});


Loading…
Cancel
Save