feat(k8s): Introduce the ability to restrict access to default namespace (EE-745) (#5337)

pull/5352/head
dbuduev 2021-07-23 17:10:46 +12:00 committed by GitHub
parent c26af1449c
commit 7d6b1edd48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 56 additions and 60 deletions

View File

@ -151,11 +151,17 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
} }
} }
updateAuthorizations := false
if payload.Kubernetes != nil { if payload.Kubernetes != nil {
if payload.Kubernetes.Configuration.RestrictDefaultNamespace !=
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace {
updateAuthorizations = true
}
endpoint.Kubernetes = *payload.Kubernetes endpoint.Kubernetes = *payload.Kubernetes
} }
updateAuthorizations := false
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) { if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
updateAuthorizations = true updateAuthorizations = true
endpoint.UserAccessPolicies = payload.UserAccessPolicies endpoint.UserAccessPolicies = payload.UserAccessPolicies

View File

@ -34,7 +34,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
// RoundTrip is the implementation of the the http.RoundTripper interface // RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) { func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token, err := getRoundTripToken(request, transport.tokenManager) token, err := transport.getRoundTripToken(request, transport.tokenManager)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -31,7 +31,7 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo
// RoundTrip is the implementation of the the http.RoundTripper interface // RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) { func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token, err := getRoundTripToken(request, transport.tokenManager) token, err := transport.getRoundTripToken(request, transport.tokenManager)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -45,7 +45,7 @@ func (manager *tokenManager) getAdminServiceAccountToken() string {
return manager.adminToken return manager.adminToken
} }
func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) { func (manager *tokenManager) getUserServiceAccountToken(userID int, endpointID portainer.EndpointID) (string, error) {
manager.mutex.Lock() manager.mutex.Lock()
defer manager.mutex.Unlock() defer manager.mutex.Unlock()
@ -61,7 +61,13 @@ func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, err
teamIds = append(teamIds, int(membership.TeamID)) teamIds = append(teamIds, int(membership.TeamID))
} }
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds) endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return "", err
}
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds, restrictDefaultNamespace)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -87,7 +87,7 @@ func (transport *baseTransport) executeKubernetesRequest(request *http.Request)
// #region ROUND TRIP // #region ROUND TRIP
func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string, error) { func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string, error) {
token, err := getRoundTripToken(request, transport.tokenManager) token, err := transport.getRoundTripToken(request, transport.tokenManager)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -102,7 +102,7 @@ func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response
return transport.proxyKubernetesRequest(request) return transport.proxyKubernetesRequest(request)
} }
func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) { func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) {
tokenData, err := security.RetrieveTokenData(request) tokenData, err := security.RetrieveTokenData(request)
if err != nil { if err != nil {
return "", err return "", err
@ -112,7 +112,7 @@ func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (strin
if tokenData.Role == portainer.AdministratorRole { if tokenData.Role == portainer.AdministratorRole {
token = tokenManager.getAdminServiceAccountToken() token = tokenManager.getAdminServiceAccountToken()
} else { } else {
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID)) token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID), transport.endpoint.ID)
if err != nil { if err != nil {
log.Printf("Failed retrieving service account token: %v", err) log.Printf("Failed retrieving service account token: %v", err)
return "", err return "", err

View File

@ -9,10 +9,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
type (
namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy
)
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace // NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error { func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
kcl.lock.Lock() kcl.lock.Lock()
@ -48,18 +44,8 @@ func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNam
return policies, nil return policies, nil
} }
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error { func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string, restrictDefaultNamespace bool) error {
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{}) accessPolicies, err := kcl.GetNamespaceAccessPolicies()
if k8serrors.IsNotFound(err) {
return nil
} else if err != nil {
return err
}
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
var accessPolicies namespaceAccessPolicies
err = json.Unmarshal([]byte(accessData), &accessPolicies)
if err != nil { if err != nil {
return err return err
} }
@ -70,20 +56,16 @@ func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, service
} }
for _, namespace := range namespaces.Items { for _, namespace := range namespaces.Items {
if namespace.Name == defaultNamespace { if namespace.Name == defaultNamespace && !restrictDefaultNamespace {
continue err = kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace)
}
policies, ok := accessPolicies[namespace.Name]
if !ok {
err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name)
if err != nil { if err != nil {
return err return err
} }
continue continue
} }
if !hasUserAccessToNamespace(userID, teamIDs, policies) { policies, ok := accessPolicies[namespace.Name]
if !ok || !hasUserAccessToNamespace(userID, teamIDs, policies) {
err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name)
if err != nil { if err != nil {
return err return err

View File

@ -17,7 +17,7 @@ func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error)
// SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes // SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes
// cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user. // cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user.
//It will also create required default RoleBinding and ClusterRoleBinding rules. //It will also create required default RoleBinding and ClusterRoleBinding rules.
func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error { func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error {
serviceAccountName := userServiceAccountName(userID, kcl.instanceID) serviceAccountName := userServiceAccountName(userID, kcl.instanceID)
err := kcl.ensureRequiredResourcesExist() err := kcl.ensureRequiredResourcesExist()
@ -25,20 +25,7 @@ func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error
return err return err
} }
err = kcl.ensureServiceAccountForUserExists(serviceAccountName) err = kcl.createUserServiceAccount(portainerNamespace, serviceAccountName)
if err != nil {
return err
}
return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName)
}
func (kcl *KubeClient) ensureRequiredResourcesExist() error {
return kcl.createPortainerUserClusterRole()
}
func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName string) error {
err := kcl.createUserServiceAccount(portainerNamespace, serviceAccountName)
if err != nil { if err != nil {
return err return err
} }
@ -53,7 +40,11 @@ func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName stri
return err return err
} }
return kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace) return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName, restrictDefaultNamespace)
}
func (kcl *KubeClient) ensureRequiredResourcesExist() error {
return kcl.createPortainerUserClusterRole()
} }
func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error { func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error {

View File

@ -414,10 +414,11 @@ type (
// KubernetesConfiguration represents the configuration of a Kubernetes endpoint // KubernetesConfiguration represents the configuration of a Kubernetes endpoint
KubernetesConfiguration struct { KubernetesConfiguration struct {
UseLoadBalancer bool `json:"UseLoadBalancer"` UseLoadBalancer bool `json:"UseLoadBalancer"`
UseServerMetrics bool `json:"UseServerMetrics"` UseServerMetrics bool `json:"UseServerMetrics"`
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"` IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"`
} }
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
@ -1170,7 +1171,7 @@ type (
// KubeClient represents a service used to query a Kubernetes environment // KubeClient represents a service used to query a Kubernetes environment
KubeClient interface { KubeClient interface {
SetupUserServiceAccount(userID int, teamIDs []int) error SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
GetServiceAccountBearerToken(userID int) (string, error) GetServiceAccountBearerToken(userID int) (string, error)
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
NamespaceAccessPoliciesDeleteNamespace(namespace string) error NamespaceAccessPoliciesDeleteNamespace(namespace string) error

View File

@ -1003,6 +1003,8 @@ definitions:
type: boolean type: boolean
UseServerMetrics: UseServerMetrics:
type: boolean type: boolean
RestrictDefaultNamespace:
type: boolean
type: object type: object
portainer.KubernetesData: portainer.KubernetesData:
properties: properties:

View File

@ -10,5 +10,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatab
reverseOrder: '<', reverseOrder: '<',
removeAction: '<', removeAction: '<',
refreshCallback: '<', refreshCallback: '<',
endpoint: '<',
}, },
}); });

View File

@ -18,7 +18,11 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
}; };
this.canManageAccess = function (item) { this.canManageAccess = function (item) {
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item); if (!this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace) {
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item);
} else {
return !this.isSystemNamespace(item);
}
}; };
this.disableRemove = function (item) { this.disableRemove = function (item) {

View File

@ -145,11 +145,7 @@
<label class="control-label text-left"> <label class="control-label text-left">
Restrict access to the default namespace Restrict access to the default namespace
</label> </label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label> <label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.RestrictDefaultNamespace" /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-setup-default" target="_blank"> Portainer Business Edition</a>.
</span>
</div> </div>
</div> </div>

View File

@ -107,6 +107,7 @@ class KubernetesConfigureController {
endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses; endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses;
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace;
} }
transformFormValues() { transformFormValues() {
@ -259,6 +260,7 @@ class KubernetesConfigureController {
UseLoadBalancer: false, UseLoadBalancer: false,
UseServerMetrics: false, UseServerMetrics: false,
IngressClasses: [], IngressClasses: [],
RestrictDefaultNamespace: false,
}; };
try { try {
@ -281,6 +283,7 @@ class KubernetesConfigureController {
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer; this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
this.formValues.RestrictDefaultNamespace = this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace;
this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => { this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => {
ic.IsNew = false; ic.IsNew = false;
ic.NeedsDeletion = false; ic.NeedsDeletion = false;

View File

@ -13,6 +13,7 @@
order-by="Namespace.Name" order-by="Namespace.Name"
remove-action="ctrl.removeAction" remove-action="ctrl.removeAction"
refresh-callback="ctrl.getResourcePools" refresh-callback="ctrl.getResourcePools"
endpoint="ctrl.endpoint"
></kubernetes-resource-pools-datatable> ></kubernetes-resource-pools-datatable>
</div> </div>
</div> </div>

View File

@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView',
templateUrl: './resourcePools.html', templateUrl: './resourcePools.html',
controller: 'KubernetesResourcePoolsController', controller: 'KubernetesResourcePoolsController',
controllerAs: 'ctrl', controllerAs: 'ctrl',
bindings: {
endpoint: '<',
},
}); });

View File

@ -32,7 +32,7 @@
"dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js", "dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js",
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client", "dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client",
"start:toolkit": "grunt start:toolkit", "start:toolkit": "grunt start:toolkit",
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer", "build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
"clean:all": "grunt clean:all", "clean:all": "grunt clean:all",
"format": "prettier --loglevel warn --write \"**/*.{js,css,html}\"", "format": "prettier --loglevel warn --write \"**/*.{js,css,html}\"",
"lint": "yarn lint:client; yarn lint:server", "lint": "yarn lint:client; yarn lint:server",