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.Configuration.RestrictDefaultNamespace !=
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace {
updateAuthorizations = true
}
endpoint.Kubernetes = *payload.Kubernetes
}
updateAuthorizations := false
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
updateAuthorizations = true
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
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 {
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
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 {
return nil, err
}

View File

@ -45,7 +45,7 @@ func (manager *tokenManager) getAdminServiceAccountToken() string {
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()
defer manager.mutex.Unlock()
@ -61,7 +61,13 @@ func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, err
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 {
return "", err
}

View File

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

View File

@ -9,10 +9,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type (
namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy
)
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
kcl.lock.Lock()
@ -48,18 +44,8 @@ func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNam
return policies, nil
}
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error {
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
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)
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string, restrictDefaultNamespace bool) error {
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
if err != nil {
return err
}
@ -70,20 +56,16 @@ func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, service
}
for _, namespace := range namespaces.Items {
if namespace.Name == defaultNamespace {
continue
}
policies, ok := accessPolicies[namespace.Name]
if !ok {
err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name)
if namespace.Name == defaultNamespace && !restrictDefaultNamespace {
err = kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace)
if err != nil {
return err
}
continue
}
if !hasUserAccessToNamespace(userID, teamIDs, policies) {
policies, ok := accessPolicies[namespace.Name]
if !ok || !hasUserAccessToNamespace(userID, teamIDs, policies) {
err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name)
if err != nil {
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
// cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user.
//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)
err := kcl.ensureRequiredResourcesExist()
@ -25,20 +25,7 @@ func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error
return err
}
err = kcl.ensureServiceAccountForUserExists(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)
err = kcl.createUserServiceAccount(portainerNamespace, serviceAccountName)
if err != nil {
return err
}
@ -53,7 +40,11 @@ func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName stri
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 {

View File

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

View File

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

View File

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

View File

@ -18,7 +18,11 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
};
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) {

View File

@ -145,11 +145,7 @@
<label class="control-label text-left">
Restrict access to the default namespace
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><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>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.RestrictDefaultNamespace" /><i></i> </label>
</div>
</div>

View File

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

View File

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

View File

@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView',
templateUrl: './resourcePools.html',
controller: 'KubernetesResourcePoolsController',
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:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client",
"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",
"format": "prettier --loglevel warn --write \"**/*.{js,css,html}\"",
"lint": "yarn lint:client; yarn lint:server",