From 4ee9cf1a96d69b29736e2e8662a10636b233414f Mon Sep 17 00:00:00 2001 From: Spyros Synodinos <138458697+ssyno@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:49:23 +0300 Subject: [PATCH] Implement multiple roles per token (#161) * add app roles * update tests * fix tests * useCombinedTokens addition * CHANGELOG * update for roles set on configmap * update everything for roles flag * go mod tidy * clear DOCKERFILE + fix deployment * values schema json update * updates for comments * CHANGELOG UPDATE --- CHANGELOG.md | 1 + go.mod | 2 +- .../templates/deployment.yaml | 5 +- helm/teleport-operator/values.schema.json | 13 +++ helm/teleport-operator/values.yaml | 7 +- internal/controller/cluster_controller.go | 33 +++---- .../controller/cluster_controller_test.go | 30 ++++--- internal/pkg/key/key.go | 47 +++++++++- internal/pkg/teleport/configmap.go | 12 +-- internal/pkg/teleport/configmap_test.go | 32 +++---- internal/pkg/teleport/token.go | 26 ++---- internal/pkg/teleport/token_test.go | 90 +++++++++++++------ internal/pkg/test/resources.go | 25 ++++-- internal/pkg/test/teleport_client_test.go | 24 ++--- main.go | 19 +++- 15 files changed, 234 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074c3bf4..639541d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Implemented option to generate combined tokens with multiple roles - Change ownership to Team Shield ### Fixed diff --git a/go.mod b/go.mod index d141ac71..455fdfd2 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/onsi/ginkgo/v2 v2.20.0 github.com/onsi/gomega v1.34.1 github.com/pkg/errors v0.9.1 - go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.25.0 k8s.io/apimachinery v0.25.0 @@ -92,6 +91,7 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.28.0 // indirect diff --git a/helm/teleport-operator/templates/deployment.yaml b/helm/teleport-operator/templates/deployment.yaml index 82e19d84..2061f726 100644 --- a/helm/teleport-operator/templates/deployment.yaml +++ b/helm/teleport-operator/templates/deployment.yaml @@ -21,7 +21,7 @@ spec: labels: {{- include "labels.selector" . | nindent 8 }} spec: - serviceAccountName: {{ include "resource.default.name" . }} + serviceAccountName: {{ include "resource.default.name" . }} securityContext: runAsUser: {{ .Values.pod.user.id }} runAsGroup: {{ .Values.pod.group.id }} @@ -35,7 +35,8 @@ spec: value: "{{ .Values.teleport.proxyAddr }}=yes" image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Chart.Version }}" args: - - "--namespace={{ include "resource.default.namespace" . }}" + - "--namespace={{ include "resource.default.namespace" . }}" + - "--token-roles={{ .Values.teleportOperator.roles | join "," }}" {{- if .Values.tbot.enabled }} - "--tbot" {{- end }} diff --git a/helm/teleport-operator/values.schema.json b/helm/teleport-operator/values.schema.json index 00e67450..67551da4 100644 --- a/helm/teleport-operator/values.schema.json +++ b/helm/teleport-operator/values.schema.json @@ -63,6 +63,19 @@ } } }, + "teleportOperator": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of roles for the Teleport operator. Example: ['kube', 'app']" + } + }, + "required": ["roles"] + }, "pod": { "type": "object", "properties": { diff --git a/helm/teleport-operator/values.yaml b/helm/teleport-operator/values.yaml index 4332a193..c78e0452 100644 --- a/helm/teleport-operator/values.yaml +++ b/helm/teleport-operator/values.yaml @@ -17,7 +17,12 @@ teleport: managementClusterName: "" proxyAddr: test.teleport.giantswarm.io:443 teleportClusterName: test.teleport.giantswarm.io - teleportVersion: 15.1.7 + teleportVersion: 16.1.7 + +teleportOperator: + roles: + - kube + - app pod: user: diff --git a/internal/controller/cluster_controller.go b/internal/controller/cluster_controller.go index 450ffd8a..dfbe4b31 100644 --- a/internal/controller/cluster_controller.go +++ b/internal/controller/cluster_controller.go @@ -45,6 +45,7 @@ type ClusterReconciler struct { Teleport *teleport.Teleport IsBotEnabled bool Namespace string + TokenRoles []string } //+kubebuilder:rbac:groups=cluster.x-k8s.io.giantswarm.io,resources=clusters,verbs=get;list;watch;create;update;patch;delete @@ -157,7 +158,7 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, microerror.Mask(err) } if secret == nil { - token, err := r.Teleport.GenerateToken(ctx, registerName, "node") + token, err := r.Teleport.GenerateToken(ctx, registerName, []string{key.RoleNode}) if err != nil { return ctrl.Result{}, microerror.Mask(err) } @@ -169,12 +170,12 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err != nil { return ctrl.Result{}, microerror.Mask(err) } - tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, "node") + tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, key.RoleNode) if err != nil { return ctrl.Result{}, microerror.Mask(err) } if !tokenValid { - token, err := r.Teleport.GenerateToken(ctx, registerName, "node") + token, err := r.Teleport.GenerateToken(ctx, registerName, []string{key.RoleNode}) if err != nil { return ctrl.Result{}, microerror.Mask(err) } @@ -186,41 +187,41 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } } - // Check if the confimap exists in the cluster, if not, generate teleport token and create the config map + // Check if the configmap exists in the cluster, if not, generate teleport token and create the config map // if it is, check teleport token validity, and update the configmap if teleport token has expired configMap, err := r.Teleport.GetConfigMap(ctx, log, r.Client, cluster.Name, cluster.Namespace) if err != nil { return ctrl.Result{}, microerror.Mask(err) } - if configMap != nil { + if configMap == nil { + token, err := r.Teleport.GenerateToken(ctx, registerName, r.TokenRoles) + if err != nil { + return ctrl.Result{}, microerror.Mask(err) + } + if err := r.Teleport.CreateConfigMap(ctx, log, r.Client, cluster.Name, cluster.Namespace, registerName, token, r.TokenRoles); err != nil { + return ctrl.Result{}, microerror.Mask(err) + } + } else { token, err := r.Teleport.GetTokenFromConfigMap(ctx, configMap) if err != nil { return ctrl.Result{}, microerror.Mask(err) } - tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, "kube") + tokenValid, err := r.Teleport.IsTokenValid(ctx, registerName, token, key.RolesToString(r.TokenRoles)) if err != nil { return ctrl.Result{}, microerror.Mask(err) } if !tokenValid { - token, err := r.Teleport.GenerateToken(ctx, registerName, "kube") + token, err := r.Teleport.GenerateToken(ctx, registerName, r.TokenRoles) if err != nil { return ctrl.Result{}, microerror.Mask(err) } - if err := r.Teleport.UpdateConfigMap(ctx, log, r.Client, configMap, token); err != nil { + if err := r.Teleport.UpdateConfigMap(ctx, log, r.Client, configMap, token, r.TokenRoles); err != nil { return ctrl.Result{}, microerror.Mask(err) } } else { log.Info("ConfigMap has valid teleport kube join token", "configMapName", configMap.GetName()) } - } else { - token, err := r.Teleport.GenerateToken(ctx, registerName, "kube") - if err != nil { - return ctrl.Result{}, microerror.Mask(err) - } - if err := r.Teleport.CreateConfigMap(ctx, log, r.Client, cluster.Name, cluster.Namespace, registerName, token); err != nil { - return ctrl.Result{}, microerror.Mask(err) - } } if r.IsBotEnabled { diff --git a/internal/controller/cluster_controller_test.go b/internal/controller/cluster_controller_test.go index 82eb5f5f..80187c15 100644 --- a/internal/controller/cluster_controller_test.go +++ b/internal/controller/cluster_controller_test.go @@ -49,7 +49,7 @@ func Test_ClusterController(t *testing.T) { cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), }, { name: "case 1: Register cluster and update Secret, ConfigMap and App resources in case they exist", @@ -59,10 +59,10 @@ func Test_ClusterController(t *testing.T) { identity: newIdentity(test.LastReadValue), cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), }, { name: "case 2: Update Secret and ConfigMap resources in case join token changes", @@ -72,10 +72,10 @@ func Test_ClusterController(t *testing.T) { identity: newIdentity(test.LastReadValue), cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.NewTokenName), - expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName), + expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{"kube", "app"}), }, { name: "case 3: Deregister cluster and delete resources in case the cluster is deleted", @@ -85,7 +85,7 @@ func Test_ClusterController(t *testing.T) { identity: newIdentity(test.LastReadValue), cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Now()), secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), }, { name: "case 4: Reconnect to Teleport when credentials are rotated", @@ -96,13 +96,13 @@ func Test_ClusterController(t *testing.T) { cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), identitySecret: test.NewIdentitySecret(test.NamespaceName, test.IdentityFileValue), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), newTeleportClient: func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) { return test.NewTeleportClient(test.FakeTeleportClientConfig{Tokens: nil}), nil }, expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.NewTokenName), - expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName), + expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{"kube", "app"}), }, { name: "case 5: Return an error in case reconnection to Teleport fails after the credentials are rotated", @@ -112,7 +112,7 @@ func Test_ClusterController(t *testing.T) { identity: newIdentity(time.Now().Add(-identityExpirationPeriod - time.Second)), cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), newTeleportClient: func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) { return nil, errors.New("simulated error") }, @@ -153,11 +153,13 @@ func Test_ClusterController(t *testing.T) { log := ctrl.Log.WithName("test") controller := &ClusterReconciler{ - Client: ctrlClient, - Log: log, - Scheme: scheme.Scheme, - Namespace: tc.namespace, - Teleport: teleport.New(tc.namespace, tc.config, test.NewMockTokenGenerator(tc.token)), + Client: ctrlClient, + Log: log, + Scheme: scheme.Scheme, + Namespace: tc.namespace, + Teleport: teleport.New(tc.namespace, tc.config, test.NewMockTokenGenerator(tc.token)), + IsBotEnabled: false, + TokenRoles: []string{"kube", "app"}, } controller.Teleport.TeleportClient = test.NewTeleportClient(test.FakeTeleportClientConfig{ Tokens: tc.tokens, diff --git a/internal/pkg/key/key.go b/internal/pkg/key/key.go index 9b18ff8f..0174fd6a 100644 --- a/internal/pkg/key/key.go +++ b/internal/pkg/key/key.go @@ -2,7 +2,10 @@ package key import ( "fmt" + "strings" "time" + + "github.com/gravitational/teleport/api/types" ) const ( @@ -15,6 +18,7 @@ const ( TeleportBotSecretName = "identity-output" TeleportBotNamespace = "giantswarm" TeleportBotAppName = "teleport-tbot" + TeleportAppTokenValidity = 720 * time.Hour TeleportKubeTokenValidity = 720 * time.Hour TeleportNodeTokenValidity = 720 * time.Hour @@ -26,8 +30,45 @@ const ( ManagementClusterName = "managementClusterName" ProxyAddr = "proxyAddr" TeleportVersion = "teleportVersion" + RoleKube = "kube" + RoleApp = "app" + RoleNode = "node" ) +func ParseRoles(s string) ([]string, error) { + parts := strings.Split(s, ",") + roles := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + switch part { + case RoleKube, RoleApp, RoleNode: + roles = append(roles, part) + default: + return nil, fmt.Errorf("invalid role: %s", part) + } + } + return roles, nil +} + +func RolesToString(roles []string) string { + return strings.Join(roles, ",") +} + +func RolesToSystemRoles(roles []string) []types.SystemRole { + systemRoles := make([]types.SystemRole, 0, len(roles)) + for _, role := range roles { + switch role { + case RoleKube: + systemRoles = append(systemRoles, types.RoleKube) + case RoleApp: + systemRoles = append(systemRoles, types.RoleApp) + case RoleNode: + systemRoles = append(systemRoles, types.RoleNode) + } + } + return systemRoles +} + func GetConfigmapName(clusterName string, appName string) string { return fmt.Sprintf("%s-%s-config", clusterName, appName) } @@ -56,8 +97,8 @@ func GetAppName(clusterName string, appName string) string { return fmt.Sprintf("%s-%s", clusterName, appName) } -func GetConfigmapDataFromTemplate(authToken string, proxyAddr string, kubeClusterName string, teleportVersion string) string { - dataTpl := `roles: "kube" +func GetConfigmapDataFromTemplate(authToken string, proxyAddr string, kubeClusterName string, teleportVersion string, roles []string) string { + dataTpl := `roles: "%s" authToken: "%s" proxyAddr: "%s" kubeClusterName: "%s" @@ -67,7 +108,7 @@ kubeClusterName: "%s" dataTpl = fmt.Sprintf("%steleportVersionOverride: %q", dataTpl, teleportVersion) } - return fmt.Sprintf(dataTpl, authToken, proxyAddr, kubeClusterName) + return fmt.Sprintf(dataTpl, RolesToString(roles), authToken, proxyAddr, kubeClusterName) } func GetTbotConfigmapDataFromTemplate(kubeClusterName string, clusterName string) string { diff --git a/internal/pkg/teleport/configmap.go b/internal/pkg/teleport/configmap.go index b67a6c5b..44271395 100644 --- a/internal/pkg/teleport/configmap.go +++ b/internal/pkg/teleport/configmap.go @@ -68,11 +68,11 @@ func (t *Teleport) GetTokenFromConfigMap(ctx context.Context, configMap *corev1. return token, nil } -func (t *Teleport) CreateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string, clusterNamespace string, registerName string, token string) error { +func (t *Teleport) CreateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string, clusterNamespace string, registerName string, token string, roles []string) error { configMapName := key.GetConfigmapName(clusterName, t.Config.AppName) configMapData := map[string]string{ - "values": t.getConfigMapData(registerName, token), + "values": t.getConfigMapData(registerName, token, roles), } cm := corev1.ConfigMap{} @@ -137,7 +137,7 @@ func (t *Teleport) EnsureTbotConfigMap(ctx context.Context, log logr.Logger, ctr return nil } -func (t *Teleport) UpdateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, configMap *corev1.ConfigMap, token string) error { +func (t *Teleport) UpdateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, configMap *corev1.ConfigMap, token string, roles []string) error { valuesBytes, ok := configMap.Data["values"] if !ok { return microerror.Mask(fmt.Errorf("malformed ConfigMap: key `values` not found")) @@ -148,8 +148,8 @@ func (t *Teleport) UpdateConfigMap(ctx context.Context, log logr.Logger, ctrlCli return microerror.Mask(fmt.Errorf("failed to parse YAML: %w", err)) } - // Modify the authToken value valuesYaml["authToken"] = token + valuesYaml["roles"] = key.RolesToString(roles) updatedValuesYaml, err := yaml.Marshal(valuesYaml) if err != nil { @@ -205,7 +205,7 @@ func (t *Teleport) DeleteTbotConfigMap(ctx context.Context, log logr.Logger, ctr return nil } -func (t *Teleport) getConfigMapData(registerName string, token string) string { +func (t *Teleport) getConfigMapData(registerName string, token string, roles []string) string { var ( authToken = token proxyAddr = t.Config.ProxyAddr @@ -213,7 +213,7 @@ func (t *Teleport) getConfigMapData(registerName string, token string) string { teleportVersionOverride = t.Config.TeleportVersion ) - return key.GetConfigmapDataFromTemplate(authToken, proxyAddr, kubeClusterName, teleportVersionOverride) + return key.GetConfigmapDataFromTemplate(authToken, proxyAddr, kubeClusterName, teleportVersionOverride, roles) } func (t *Teleport) getTbotConfigMapData(registerName string, clusterName string) string { diff --git a/internal/pkg/teleport/configmap_test.go b/internal/pkg/teleport/configmap_test.go index 97162c9e..63124f39 100644 --- a/internal/pkg/teleport/configmap_test.go +++ b/internal/pkg/teleport/configmap_test.go @@ -38,7 +38,7 @@ func Test_ConfigMapCRUD(t *testing.T) { namespace: test.NamespaceName, clusterName: test.ClusterName, registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - configMapToCreate: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMapToCreate: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), config: &config.Config{ AppName: test.AppName, ProxyAddr: test.ProxyAddr, @@ -50,8 +50,8 @@ func Test_ConfigMapCRUD(t *testing.T) { namespace: test.NamespaceName, clusterName: test.ClusterName, registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), - configMapToCreate: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), + configMapToCreate: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), expectError: false, config: &config.Config{ AppName: test.AppName, @@ -64,8 +64,8 @@ func Test_ConfigMapCRUD(t *testing.T) { namespace: test.NamespaceName, clusterName: test.ClusterName, registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), - configMapToRead: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), + configMapToRead: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), config: &config.Config{ AppName: test.AppName, ProxyAddr: test.ProxyAddr, @@ -77,7 +77,7 @@ func Test_ConfigMapCRUD(t *testing.T) { namespace: test.NamespaceName, clusterName: test.ClusterName, registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - configMapToRead: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMapToRead: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), expectEmpty: true, config: &config.Config{ AppName: test.AppName, @@ -91,8 +91,8 @@ func Test_ConfigMapCRUD(t *testing.T) { clusterName: test.ClusterName, registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), token: test.TokenName, - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), - configMapToRead: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), + configMapToRead: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), config: &config.Config{ AppName: test.AppName, ProxyAddr: test.ProxyAddr, @@ -111,7 +111,7 @@ func Test_ConfigMapCRUD(t *testing.T) { Namespace: test.NamespaceName, }, }, - configMapToRead: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMapToRead: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), expectError: true, config: &config.Config{ AppName: test.AppName, @@ -125,8 +125,8 @@ func Test_ConfigMapCRUD(t *testing.T) { clusterName: test.ClusterName, registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), token: test.NewTokenName, - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), - configMapToUpdate: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), + configMapToUpdate: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{"kube", "app"}), config: &config.Config{ AppName: test.AppName, ProxyAddr: test.ProxyAddr, @@ -138,8 +138,8 @@ func Test_ConfigMapCRUD(t *testing.T) { namespace: test.NamespaceName, clusterName: test.ClusterName, registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), - configMapToDelete: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), + configMapToDelete: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), expectError: false, config: &config.Config{ AppName: test.AppName, @@ -152,7 +152,7 @@ func Test_ConfigMapCRUD(t *testing.T) { namespace: test.NamespaceName, clusterName: test.ClusterName, registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - configMapToDelete: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName), + configMapToDelete: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{"kube", "app"}), expectError: false, config: &config.Config{ AppName: test.AppName, @@ -201,7 +201,7 @@ func Test_ConfigMapCRUD(t *testing.T) { } if tc.configMapToCreate != nil { - err = teleport.CreateConfigMap(ctx, log, ctrlClient, tc.clusterName, tc.namespace, tc.registerName, tc.token) + err = teleport.CreateConfigMap(ctx, log, ctrlClient, tc.clusterName, tc.namespace, tc.registerName, tc.token, []string{"kube", "app"}) test.CheckError(t, tc.expectError, err) if err != nil { actualConfigMap, err = loadConfigMap(ctx, ctrlClient, tc.configMapToCreate) @@ -213,7 +213,7 @@ func Test_ConfigMapCRUD(t *testing.T) { } if tc.configMapToUpdate != nil { - err = teleport.UpdateConfigMap(ctx, log, ctrlClient, tc.configMap, tc.token) + err = teleport.UpdateConfigMap(ctx, log, ctrlClient, tc.configMap, tc.token, []string{"kube", "app"}) test.CheckError(t, tc.expectError, err) if err != nil { actualConfigMap, err = loadConfigMap(ctx, ctrlClient, tc.configMapToUpdate) diff --git a/internal/pkg/teleport/token.go b/internal/pkg/teleport/token.go index a5ae6c43..de9295bc 100644 --- a/internal/pkg/teleport/token.go +++ b/internal/pkg/teleport/token.go @@ -2,11 +2,10 @@ package teleport import ( "context" - "fmt" "time" "github.com/go-logr/logr" - tt "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types" "github.com/giantswarm/microerror" @@ -29,24 +28,11 @@ func (t *Teleport) IsTokenValid(ctx context.Context, registerName string, oldTok return false, nil } -func (t *Teleport) GenerateToken(ctx context.Context, registerName string, tokenType string) (string, error) { - var ( - tokenValidity time.Time - tokenRole tt.SystemRole - ) +func (t *Teleport) GenerateToken(ctx context.Context, registerName string, roles []string) (string, error) { + tokenValidity := time.Now().Add(key.TeleportKubeTokenValidity) + tokenRoles := key.RolesToSystemRoles(roles) - switch tokenType { - case "kube": - tokenValidity = time.Now().Add(key.TeleportKubeTokenValidity) - tokenRole = tt.RoleKube - case "node": - tokenValidity = time.Now().Add(key.TeleportNodeTokenValidity) - tokenRole = tt.RoleNode - default: - return "", microerror.Mask(fmt.Errorf("token type %s is not supported", tokenType)) - } - - token, err := tt.NewProvisionToken(t.TokenGenerator.Generate(), []tt.SystemRole{tokenRole}, tokenValidity) + token, err := types.NewProvisionToken(t.TokenGenerator.Generate(), tokenRoles, tokenValidity) if err != nil { return "", microerror.Mask(err) } @@ -55,7 +41,7 @@ func (t *Teleport) GenerateToken(ctx context.Context, registerName string, token m := token.GetMetadata() m.Labels = map[string]string{ "cluster": registerName, - "type": tokenType, + "roles": key.RolesToString(roles), } token.SetMetadata(m) if err := t.TeleportClient.UpsertToken(ctx, token); err != nil { diff --git a/internal/pkg/teleport/token_test.go b/internal/pkg/teleport/token_test.go index 14260f49..919de766 100644 --- a/internal/pkg/teleport/token_test.go +++ b/internal/pkg/teleport/token_test.go @@ -2,7 +2,9 @@ package teleport import ( "context" + "strings" "testing" + "time" "github.com/gravitational/teleport/api/types" ctrl "sigs.k8s.io/controller-runtime" @@ -15,28 +17,39 @@ import ( func Test_GenerateToken(t *testing.T) { testCases := []struct { - name string - registerName string - tokenType string - failsList bool - failsDelete bool - failsUpsert bool - expectError bool - expectedToken types.ProvisionToken + name string + registerName string + tokenType []string + failsList bool + failsDelete bool + failsUpsert bool + expectError bool + expectedRoles []string + expectedExpiry time.Duration }{ { - name: "case 0: Generate a new kube token", - registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - tokenType: test.TokenTypeKube, - expectError: false, - expectedToken: test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeKube), + name: "case 0: Generate a new kube token", + registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), + tokenType: []string{"kube"}, + expectError: false, + expectedRoles: []string{"kube"}, + expectedExpiry: 720 * time.Hour, }, { - name: "case 1: Generate a new node token", - registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - tokenType: test.TokenTypeNode, - expectError: false, - expectedToken: test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeNode), + name: "case 1: Generate a new node token", + registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), + tokenType: []string{"node"}, + expectError: false, + expectedRoles: []string{"node"}, + expectedExpiry: 720 * time.Hour, + }, + { + name: "case 2: Generate a new kube and app token", + registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), + tokenType: []string{"kube", "app"}, + expectError: false, + expectedRoles: []string{"kube", "app"}, + expectedExpiry: 720 * time.Hour, }, { name: "case 3: Fail in case new token cannot be upserted", @@ -66,7 +79,28 @@ func Test_GenerateToken(t *testing.T) { generatedToken, err := teleport.TeleportClient.GetToken(ctx, tokenName) test.CheckError(t, false, err) if err == nil { - test.CheckToken(t, tc.expectedToken, generatedToken) + expectedExpiryTime := time.Now().Add(tc.expectedExpiry) + + actualToken := generatedToken.(*types.ProvisionTokenV2) + if !actualToken.GetMetadata().Expires.After(expectedExpiryTime.Add(-time.Minute)) || + !actualToken.GetMetadata().Expires.Before(expectedExpiryTime.Add(time.Minute)) { + t.Fatalf("Expected token expiry to be close to %v, but got %v", expectedExpiryTime, actualToken.GetMetadata().Expires) + } + + actualRoles := actualToken.GetRoles() + actualRoleStrings := make([]string, len(actualRoles)) + for i, role := range actualRoles { + actualRoleStrings[i] = strings.ToLower(role.String()) + } + + if len(tc.expectedRoles) != len(actualRoleStrings) { + t.Fatalf("Expected roles %v, but got %v", tc.expectedRoles, actualRoleStrings) + } + for i := range tc.expectedRoles { + if tc.expectedRoles[i] != actualRoleStrings[i] { + t.Fatalf("Expected roles %v, but got %v", tc.expectedRoles, actualRoleStrings) + } + } } }) } @@ -88,7 +122,7 @@ func Test_IsTokenValid(t *testing.T) { registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), tokenName: test.TokenName, tokenType: test.TokenTypeKube, - tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeKube)}, + tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"})}, expectedResult: true, }, { @@ -104,7 +138,7 @@ func Test_IsTokenValid(t *testing.T) { registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), tokenName: test.TokenName, tokenType: test.TokenTypeNode, - tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeKube)}, + tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"})}, expectedResult: false, }, { @@ -149,28 +183,28 @@ func Test_DeleteToken(t *testing.T) { { name: "case 0: Delete token from Teleport", registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - token: test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeKube), - tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeKube)}, + token: test.NewToken(test.TokenName, test.ClusterName, []string{"kube"}), + tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"})}, expectDeleted: true, }, { name: "case 1: Do not delete token in case cluster label does not match", registerName: test.ManagementClusterName, - token: test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeKube), - tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeKube)}, + token: test.NewToken(test.TokenName, test.ClusterName, []string{"kube"}), + tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"})}, expectDeleted: false, }, { name: "case 2: Succeed in case token does not exist", registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - token: test.NewToken(test.TokenName, test.ManagementClusterName, test.TokenTypeKube), + token: test.NewToken(test.TokenName, test.ManagementClusterName, []string{"kube"}), expectDeleted: true, }, { name: "case 3: Fail in case teleport client is unable to delete the token", registerName: key.GetRegisterName(test.ManagementClusterName, test.ClusterName), - token: test.NewToken(test.TokenName, test.ManagementClusterName, test.TokenTypeKube), - tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, test.TokenTypeKube)}, + token: test.NewToken(test.TokenName, test.ManagementClusterName, []string{"kube"}), + tokens: []types.ProvisionToken{test.NewToken(test.TokenName, test.ClusterName, []string{"kube"})}, failsDelete: true, expectError: true, }, diff --git a/internal/pkg/test/resources.go b/internal/pkg/test/resources.go index cbdc593b..aca4d926 100644 --- a/internal/pkg/test/resources.go +++ b/internal/pkg/test/resources.go @@ -2,6 +2,7 @@ package test import ( "fmt" + "strings" "time" appv1alpha1 "github.com/giantswarm/apiextensions-application/api/v1alpha1" @@ -30,6 +31,7 @@ const ( TokenTypeKube = "kube" TokenTypeNode = "node" + TokenTypeApp = "app" AppCatalog = "app-catalog" AppVersion = "appVersion" @@ -38,7 +40,7 @@ const ( IdentityFileValue = "identity-file-value" TeleportVersion = "1.0.0" - ConfigMapValuesFormat = "authToken: %s\nproxyAddr: %s\nroles: kube\nkubeClusterName: %s\nteleportVersionOverride: %s" + ConfigMapValuesFormat = "authToken: %s\nproxyAddr: %s\nroles: %s\nkubeClusterName: %s\nteleportVersionOverride: %s" ) var LastReadValue = time.Now() @@ -80,7 +82,7 @@ func NewIdentitySecret(namespaceName, identityFile string) *corev1.Secret { } } -func NewConfigMap(clusterName, appName, namespaceName, tokenName string) *corev1.ConfigMap { +func NewConfigMap(clusterName, appName, namespaceName, tokenName string, roles []string) *corev1.ConfigMap { registerName := key.GetRegisterName(ManagementClusterName, clusterName) return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -88,28 +90,33 @@ func NewConfigMap(clusterName, appName, namespaceName, tokenName string) *corev1 Namespace: namespaceName, }, Data: map[string]string{ - "values": fmt.Sprintf(ConfigMapValuesFormat, tokenName, ProxyAddr, registerName, TeleportVersion), + "values": fmt.Sprintf(ConfigMapValuesFormat, tokenName, ProxyAddr, strings.Join(roles, ","), registerName, TeleportVersion), }, } } -func NewToken(tokenName, clusterName, tokenType string) teleportTypes.ProvisionToken { +func NewToken(tokenName, clusterName string, roles []string) teleportTypes.ProvisionToken { newToken := &teleportTypes.ProvisionTokenV2{ Metadata: teleportTypes.Metadata{ Name: tokenName, Labels: map[string]string{ ClusterKey: key.GetRegisterName(ManagementClusterName, clusterName), - TokenTypeKey: tokenType, + TokenTypeKey: strings.Join(roles, ","), }, }, Spec: teleportTypes.ProvisionTokenSpecV2{ Roles: []teleportTypes.SystemRole{}, }, } - if tokenType == TokenTypeKube { - newToken.Spec.Roles = append(newToken.Spec.Roles, teleportTypes.RoleKube) - } else if tokenType == TokenTypeNode { - newToken.Spec.Roles = append(newToken.Spec.Roles, teleportTypes.RoleNode) + for _, role := range roles { + switch role { + case key.RoleKube: + newToken.Spec.Roles = append(newToken.Spec.Roles, teleportTypes.RoleKube) + case key.RoleApp: + newToken.Spec.Roles = append(newToken.Spec.Roles, teleportTypes.RoleApp) + case key.RoleNode: + newToken.Spec.Roles = append(newToken.Spec.Roles, teleportTypes.RoleNode) + } } return newToken } diff --git a/internal/pkg/test/teleport_client_test.go b/internal/pkg/test/teleport_client_test.go index 30090ab9..2789b4ba 100644 --- a/internal/pkg/test/teleport_client_test.go +++ b/internal/pkg/test/teleport_client_test.go @@ -41,13 +41,13 @@ func Test_FakeTeleportClient(t *testing.T) { name: "case 2: Return expected list of tokens", config: FakeTeleportClientConfig{ Tokens: []types.ProvisionToken{ - NewToken(TokenName, ClusterName, TokenTypeKube), - NewToken(NewTokenName, ClusterName, TokenTypeKube), + NewToken(TokenName, ClusterName, []string{"kube"}), + NewToken(NewTokenName, ClusterName, []string{"kube"}), }, }, expectedTokens: []types.ProvisionToken{ - NewToken(TokenName, ClusterName, TokenTypeKube), - NewToken(NewTokenName, ClusterName, TokenTypeKube), + NewToken(TokenName, ClusterName, []string{"kube"}), + NewToken(NewTokenName, ClusterName, []string{"kube"}), }, expectClientErrors: false, }, @@ -55,8 +55,8 @@ func Test_FakeTeleportClient(t *testing.T) { name: "case 3, Fail to return list of tokens", config: FakeTeleportClientConfig{ Tokens: []types.ProvisionToken{ - NewToken(TokenName, ClusterName, TokenTypeKube), - NewToken(NewTokenName, ClusterName, TokenTypeKube), + NewToken(TokenName, ClusterName, []string{"kube"}), + NewToken(NewTokenName, ClusterName, []string{"kube"}), }, }, expectClientErrors: false, @@ -66,18 +66,18 @@ func Test_FakeTeleportClient(t *testing.T) { name: "case 4: Return expected token", config: FakeTeleportClientConfig{ Tokens: []types.ProvisionToken{ - NewToken(TokenName, ClusterName, TokenTypeKube), + NewToken(TokenName, ClusterName, []string{"kube"}), }, }, tokenName: TokenName, - expectedToken: NewToken(TokenName, ClusterName, TokenTypeKube), + expectedToken: NewToken(TokenName, ClusterName, []string{"kube"}), expectClientErrors: false, }, { name: "case 5: Fail to return expected token", config: FakeTeleportClientConfig{ Tokens: []types.ProvisionToken{ - NewToken(TokenName, ClusterName, TokenTypeKube), + NewToken(TokenName, ClusterName, []string{"kube"}), }, }, tokenName: NewTokenName, @@ -87,14 +87,14 @@ func Test_FakeTeleportClient(t *testing.T) { { name: "case 6: Store token", config: FakeTeleportClientConfig{}, - storedToken: NewToken(TokenName, ClusterName, TokenTypeKube), + storedToken: NewToken(TokenName, ClusterName, []string{"kube"}), expectClientErrors: false, expectTokensError: false, }, { name: "case 7: Upsert token", config: FakeTeleportClientConfig{}, - upsertedToken: NewToken(TokenName, ClusterName, TokenTypeNode), + upsertedToken: NewToken(TokenName, ClusterName, []string{"node"}), expectClientErrors: false, expectTokensError: false, }, @@ -128,7 +128,7 @@ func Test_FakeTeleportClient(t *testing.T) { } } - token = NewToken(uuid.NewString(), ClusterName, TokenTypeKube) + token = NewToken(uuid.NewString(), ClusterName, []string{"kube"}) err = fakeClient.CreateToken(ctx, token) CheckError(t, tc.expectClientErrors, err) diff --git a/main.go b/main.go index 43e83b8a..48f50c49 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,6 @@ import ( // to ensure that exec-entrypoint and run can make use of them. appv1alpha1 "github.com/giantswarm/apiextensions-application/api/v1alpha1" - "go.uber.org/zap/zapcore" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -40,6 +39,7 @@ import ( "github.com/giantswarm/teleport-operator/internal/controller" "github.com/giantswarm/teleport-operator/internal/pkg/config" + "github.com/giantswarm/teleport-operator/internal/pkg/key" "github.com/giantswarm/teleport-operator/internal/pkg/teleport" "github.com/giantswarm/teleport-operator/internal/pkg/token" //+kubebuilder:scaffold:imports @@ -58,13 +58,14 @@ func init() { //+kubebuilder:scaffold:scheme } - func main() { var metricsAddr string var enableLeaderElection bool var enableTeleportBot bool var probeAddr string var namespace string + var tokenRolesStr string + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -74,15 +75,24 @@ func main() { "Enable teleport bot for teleport-operator. "+ "Enabling this will ensure teleport bot configmap is created and app.spec.extraConfig is updated.") flag.StringVar(&namespace, "namespace", "", "Namespace where operator is deployed") + flag.StringVar(&tokenRolesStr, "token-roles", "kube", "Comma-separated list of roles for the token (kube, app, node)") + opts := zap.Options{ - Development: false, - TimeEncoder: zapcore.ISO8601TimeEncoder, + Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + tokenRoles, err := key.ParseRoles(tokenRolesStr) + if err != nil { + setupLog.Error(err, "Failed to parse token roles") + os.Exit(1) + } + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + restConfig := ctrl.GetConfigOrDie() mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ Scheme: scheme, @@ -129,6 +139,7 @@ func main() { Teleport: tele, IsBotEnabled: enableTeleportBot, Namespace: namespace, + TokenRoles: tokenRoles, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Cluster") os.Exit(1)