mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(extensions): introduce RBAC extension (#2900)
This commit is contained in:
parent
27a0188949
commit
8057aa45c4
196 changed files with 3321 additions and 1316 deletions
|
@ -14,7 +14,10 @@ type (
|
|||
jwtService portainer.JWTService
|
||||
userService portainer.UserService
|
||||
teamMembershipService portainer.TeamMembershipService
|
||||
endpointService portainer.EndpointService
|
||||
endpointGroupService portainer.EndpointGroupService
|
||||
extensionService portainer.ExtensionService
|
||||
rbacExtensionClient *rbacExtensionClient
|
||||
authDisabled bool
|
||||
}
|
||||
|
||||
|
@ -23,7 +26,10 @@ type (
|
|||
JWTService portainer.JWTService
|
||||
UserService portainer.UserService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
ExtensionService portainer.ExtensionService
|
||||
RBACExtensionURL string
|
||||
AuthDisabled bool
|
||||
}
|
||||
|
||||
|
@ -43,48 +49,49 @@ func NewRequestBouncer(parameters *RequestBouncerParams) *RequestBouncer {
|
|||
jwtService: parameters.JWTService,
|
||||
userService: parameters.UserService,
|
||||
teamMembershipService: parameters.TeamMembershipService,
|
||||
endpointService: parameters.EndpointService,
|
||||
endpointGroupService: parameters.EndpointGroupService,
|
||||
extensionService: parameters.ExtensionService,
|
||||
rbacExtensionClient: newRBACExtensionClient(parameters.RBACExtensionURL),
|
||||
authDisabled: parameters.AuthDisabled,
|
||||
}
|
||||
}
|
||||
|
||||
// PublicAccess defines a security check for public endpoints.
|
||||
// PublicAccess defines a security check for public API endpoints.
|
||||
// No authentication is required to access these endpoints.
|
||||
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// AuthenticatedAccess defines a security check for private endpoints.
|
||||
// AuthorizedAccess defines a security check for API endpoints that require an authorization check.
|
||||
// Authentication is required to access these endpoints.
|
||||
func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler {
|
||||
h = bouncer.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
// 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 {
|
||||
h = bouncer.mwUpgradeToRestrictedRequest(h)
|
||||
h = bouncer.mwCheckPortainerAuthorizations(h)
|
||||
h = bouncer.mwAuthenticatedUser(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// RestrictedAccess defines a security check for restricted endpoints.
|
||||
// RestrictedAccess 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 authorize/filter access to resources.
|
||||
// that might be used later to authorize/filter access to resources inside an endpoint.
|
||||
func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler {
|
||||
h = bouncer.mwUpgradeToRestrictedRequest(h)
|
||||
h = bouncer.AuthenticatedAccess(h)
|
||||
h = bouncer.mwAuthenticatedUser(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// AdministratorAccess defines a chain of middleware for restricted endpoints.
|
||||
// Authentication as well as administrator role are required to access these endpoints.
|
||||
func (bouncer *RequestBouncer) AdministratorAccess(h http.Handler) http.Handler {
|
||||
h = mwCheckAdministratorRole(h)
|
||||
h = bouncer.AuthenticatedAccess(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// EndpointAccess retrieves the JWT token from the request context and verifies
|
||||
// AuthorizedEndpointOperation retrieves the JWT token from the request context and verifies
|
||||
// that the user can access the specified endpoint.
|
||||
// An error is returned when access is denied.
|
||||
func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||
// If the RBAC extension is enabled and the authorizationCheck flag is set,
|
||||
// it will also validate that the user can execute the specified operation.
|
||||
// An error is returned when access to the endpoint is denied or if the user do not have the required
|
||||
// authorization to execute the operation.
|
||||
func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint, authorizationCheck bool) error {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -108,9 +115,43 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
|
|||
return portainer.ErrEndpointAccessDenied
|
||||
}
|
||||
|
||||
if authorizationCheck {
|
||||
err = bouncer.checkEndpointOperationAuthorization(r, endpoint)
|
||||
if err != nil {
|
||||
return portainer.ErrAuthorizationRequired
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return nil
|
||||
}
|
||||
|
||||
extension, err := bouncer.extensionService.Extension(portainer.RBACExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
||||
Path: r.URL.String(),
|
||||
Method: r.Method,
|
||||
Authorizations: tokenData.EndpointAuthorizations[endpoint.ID],
|
||||
}
|
||||
|
||||
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
|
||||
return bouncer.rbacExtensionClient.checkAuthorization(apiOperation)
|
||||
}
|
||||
|
||||
// RegistryAccess retrieves the JWT token from the request context and verifies
|
||||
// that the user can access the specified registry.
|
||||
// An error is returned when access is denied.
|
||||
|
@ -136,11 +177,50 @@ func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portain
|
|||
return nil
|
||||
}
|
||||
|
||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||
func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler {
|
||||
h = bouncer.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
extension, err := bouncer.extensionService.Extension(portainer.RBACExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err)
|
||||
return
|
||||
}
|
||||
|
||||
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
||||
Path: r.URL.String(),
|
||||
Method: r.Method,
|
||||
Authorizations: tokenData.PortainerAuthorizations,
|
||||
}
|
||||
|
||||
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
|
||||
err = bouncer.rbacExtensionClient.checkAuthorization(apiOperation)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrAuthorizationRequired)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
@ -166,19 +246,6 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
|
|||
})
|
||||
}
|
||||
|
||||
// mwCheckAdministratorRole check the role of the user associated to the request
|
||||
func mwCheckAdministratorRole(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil || tokenData.Role != portainer.AdministratorRole {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// mwCheckAuthentication provides Authentication middleware for handlers
|
||||
func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -229,6 +296,15 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
|
|||
})
|
||||
}
|
||||
|
||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
|
||||
requestContext := &RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue