diff --git a/charts/fission-all/templates/_fission-kubernetes-roles.tpl b/charts/fission-all/templates/_fission-kubernetes-roles.tpl index 1c943cc68c..d946146d7e 100644 --- a/charts/fission-all/templates/_fission-kubernetes-roles.tpl +++ b/charts/fission-all/templates/_fission-kubernetes-roles.tpl @@ -117,6 +117,28 @@ rules: - get - list - watch +{{- if .Values.executor.serviceAccountCheck.enabled }} +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - get +- apiGroups: + - authorization.k8s.io + resources: + - localsubjectaccessreviews + verbs: + - create +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + - roles + verbs: + - create +{{- end }} - apiGroups: - apps resources: diff --git a/charts/fission-all/templates/executor/deployment.yaml b/charts/fission-all/templates/executor/deployment.yaml index 26cd0b532d..e626fa968a 100644 --- a/charts/fission-all/templates/executor/deployment.yaml +++ b/charts/fission-all/templates/executor/deployment.yaml @@ -77,6 +77,12 @@ spec: - name: CONTAINER_OBJECT_REAPER_INTERVAL value: {{ .Values.executor.container.objectReaperInterval | quote }} {{- end}} + {{- if .Values.executor.serviceAccountCheck.enabled }} + - name: SERVICEACCOUNT_CHECK_ENABLED + value: {{ .Values.executor.serviceAccountCheck.enabled | quote }} + - name: SERVICEACCOUNT_CHECK_INTERVAL + value: {{ .Values.executor.serviceAccountCheck.interval | quote }} + {{- end}} {{- include "fission-resource-namespace.envs" . | indent 8 }} - name: HELM_RELEASE_NAME value: {{ .Release.Name | quote }} diff --git a/charts/fission-all/values.yaml b/charts/fission-all/values.yaml index f88a6ba4c4..d51039c5be 100644 --- a/charts/fission-all/values.yaml +++ b/charts/fission-all/values.yaml @@ -193,6 +193,13 @@ executor: ## ## objectReaperInterval: 5 + serviceAccountCheck: + ## enables fission to create service account, roles and rolebinding for missing permission for builder and fetcher. + enabled: true + ## indicates the time interval in minutes, after that fission will create service account, roles and rolebinding for builder and fetcher. + ## interval will be applicable only if enable value is set to true. + ## default timing will be 30 minutes. + interval: 30 ## router is responsible for routing function calls to the appropriate function. ## router: diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go index 2e913de70d..bcea0ea7b6 100644 --- a/pkg/executor/executor.go +++ b/pkg/executor/executor.go @@ -386,6 +386,8 @@ func StartExecutor(ctx context.Context, logger *zap.Logger, port int) error { return err } + utils.CreateMissingPermissionForSA(ctx, kubernetesClient, logger) + go metrics.ServeMetrics(ctx, logger) go api.Serve(ctx, port) diff --git a/pkg/utils/serviceaccount.go b/pkg/utils/serviceaccount.go new file mode 100644 index 0000000000..09d14d684d --- /dev/null +++ b/pkg/utils/serviceaccount.go @@ -0,0 +1,306 @@ +package utils + +import ( + "context" + "fmt" + "os" + "strconv" + "time" + + uuid "github.com/satori/go.uuid" + + "go.uber.org/zap" + authorizationv1 "k8s.io/api/authorization/v1" + v1 "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" +) + +const ( + FetcherSAName string = "fission-fetcher" + BuilderSAName string = "fission-builder" + ENV_CREATE_SA string = "SERVICEACCOUNT_CHECK_ENABLED" + ENV_SA_INTERVAL string = "SERVICEACCOUNT_CHECK_INTERVAL" +) + +type ( + ServiceAccount struct { + kubernetesClient kubernetes.Interface + logger *zap.Logger + nsResolver *NamespaceResolver + permissions []*ServiceAccountPermissions + } + + ServiceAccountPermissions struct { + saName string + permissions []*PermissionCheck + } + PermissionCheck struct { + gvr *schema.GroupVersionResource + verb string + exists bool + } +) + +var ( + fetcherCheck = &ServiceAccountPermissions{ + saName: FetcherSAName, + permissions: []*PermissionCheck{ + { + gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, + verb: "list", + }, + { + gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}, + verb: "get", + }, + { + gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}, + verb: "get", + }, + { + gvr: &schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "packages"}, + verb: "get", + }, + { + gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "events"}, + verb: "create", + }, + }, + } + builderCheck = &ServiceAccountPermissions{ + saName: BuilderSAName, + permissions: []*PermissionCheck{ + { + gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}, + verb: "get", + }, + { + gvr: &schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}, + verb: "get", + }, + { + gvr: &schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "packages"}, + verb: "get", + }, + }, + } +) + +func CreateMissingPermissionForSA(ctx context.Context, kubernetesClient kubernetes.Interface, logger *zap.Logger) { + enableSA := createServiceAccount() + if enableSA { + interval := getSAInterval() + logger.Debug("interval value", zap.Any("interval", interval)) + sa := getSAObj(ctx, kubernetesClient, logger) + go sa.doSACheck(ctx, interval) + } +} + +func getSAObj(ctx context.Context, kubernetesClient kubernetes.Interface, logger *zap.Logger) *ServiceAccount { + saObj := &ServiceAccount{ + kubernetesClient: kubernetesClient, + logger: logger, + nsResolver: DefaultNSResolver(), + } + saObj.permissions = append(saObj.permissions, fetcherCheck) + saObj.permissions = append(saObj.permissions, builderCheck) + return saObj +} + +func (sa *ServiceAccount) doSACheck(ctx context.Context, interval time.Duration) { + wait.UntilWithContext(ctx, sa.runSACheck, interval) +} + +func (sa *ServiceAccount) runSACheck(ctx context.Context) { + for _, ns := range sa.nsResolver.FissionResourceNS { + for _, permission := range sa.permissions { + if permission.saName == BuilderSAName { + ns = sa.nsResolver.GetBuilderNS(ns) + } else { + ns = sa.nsResolver.GetFunctionNS(ns) + } + setupSAAndRoleBindings(ctx, sa.kubernetesClient, sa.logger, ns, permission) + } + } +} + +func setupSAAndRoleBindings(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, namespace string, ps *ServiceAccountPermissions) { + SAObj, err := createGetSA(ctx, client, ps.saName, namespace) + if err != nil { + logger.Error("error while creating or getting service account", + zap.String("SA_name", ps.saName), + zap.String("namespace", namespace), + zap.Error(err)) + return + } + + var rules []rbac.PolicyRule + + for _, permission := range ps.permissions { + permission.exists, err = checkPermission(ctx, client, SAObj, permission.gvr, permission.verb) + if err != nil { + // some error occurred while checking permission + // now assume permission not exists and will add this permission in rules, insted of return + logger.Error("error while checking permission", zap.Error(err)) + } + if !permission.exists { + rules = append(rules, rbac.PolicyRule{ + APIGroups: []string{permission.gvr.Group}, + Resources: []string{permission.gvr.Resource}, + Verbs: []string{permission.verb}, + }) + } + } + + if len(rules) > 0 { + suffix, err := generateSuffix() + if err != nil { + logger.Error("error while generating random suffix", zap.Error(err)) + } + // permission not exists, setup roles for the same + role, err := setupRoles(ctx, client, logger, SAObj, rules, suffix) + if err != nil { + logger.Error("error while creating roles", zap.Error(err)) + return + } + _, err = setupRoleBinding(ctx, client, logger, SAObj, role, suffix) + if err != nil { + logger.Error("error while creating role bindings", zap.Error(err)) + return + } + } +} + +func setupRoles(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, sa *v1.ServiceAccount, rules []rbac.PolicyRule, suffix string) (*rbac.Role, error) { + logger.Debug("creating role", + zap.String("role_name", fmt.Sprintf("%s-role-%s", sa.Name, suffix)), + zap.String("SA_Name", sa.Name), + zap.String("namespace", sa.Namespace)) + + roleObj := &rbac.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-role-%s", sa.Name, suffix), + Namespace: sa.Namespace, + }, + Rules: rules, + } + role, err := client.RbacV1().Roles(sa.Namespace).Create(ctx, roleObj, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("error while creating role for sa %s in namespace %s error: %s", sa.Name, sa.Namespace, err.Error()) + } + logger.Debug("role created successfully", + zap.String("role_name", role.Name), + zap.String("namespace", role.Namespace), + zap.String("SA_Name", sa.Name)) + return role, nil +} + +func setupRoleBinding(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, sa *v1.ServiceAccount, role *rbac.Role, suffix string) (*rbac.RoleBinding, error) { + logger.Debug("creating role binding", + zap.String("rolebinding_name", fmt.Sprintf("%s-rolebinding-%s", sa.Name, suffix)), + zap.String("SA_Name", sa.Name), + zap.String("namespace", sa.Namespace)) + + roleBindingObj := &rbac.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-rolebinding-%s", sa.Name, suffix), + Namespace: sa.Namespace, + }, + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + Name: sa.Name, + Namespace: sa.Namespace, + }, + }, + RoleRef: rbac.RoleRef{ + Kind: "Role", + Name: role.Name, + }, + } + roleBinding, err := client.RbacV1().RoleBindings(sa.Namespace).Create(ctx, roleBindingObj, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("error while creating rolebinding for sa %s in namespace %s error: %s", sa.Name, sa.Namespace, err.Error()) + } + logger.Debug("role binding created successfully", + zap.String("rolebinding_name", roleBinding.Name), + zap.String("namespace", roleBinding.Namespace), + zap.String("SA_Name", sa.Name)) + return roleBinding, nil +} + +func checkPermission(ctx context.Context, client kubernetes.Interface, sa *v1.ServiceAccount, gvr *schema.GroupVersionResource, verb string) (bool, error) { + user := fmt.Sprintf("system:serviceaccount:%s:%s", sa.Namespace, sa.Name) + sar := authorizationv1.LocalSubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: sa.Namespace, + Group: gvr.Group, + Version: gvr.Version, + Resource: gvr.Resource, + Verb: verb, + }, + User: fmt.Sprintf("system:serviceaccount:%s:%s", sa.Namespace, sa.Name), + }, + } + r, err := client.AuthorizationV1().LocalSubjectAccessReviews(sa.Namespace).Create(ctx, &sar, metav1.CreateOptions{}) + if err != nil { + return false, fmt.Errorf("error occurred while checking permission for sa %s error: %s", user, err.Error()) + } + + if !r.Status.Allowed { + return false, fmt.Errorf("permission %s/%s/%s-%s denied for sa %s", gvr.Group, gvr.Version, gvr.Resource, verb, user) + } + return true, nil +} + +// CreateGetSA => create service account if not exists else get it. +func createGetSA(ctx context.Context, k8sClient kubernetes.Interface, SAName, ns string) (*v1.ServiceAccount, error) { + saObj, err := k8sClient.CoreV1().ServiceAccounts(ns).Get(ctx, SAName, metav1.GetOptions{}) + if err != nil && k8serrors.IsNotFound(err) { + saObj = &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: SAName, + }, + } + saObj, err = k8sClient.CoreV1().ServiceAccounts(ns).Create(ctx, saObj, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + } + if err != nil { + return nil, err + } + return saObj, nil +} + +// generateSuffix generates a random string of 6 characters +func generateSuffix() (string, error) { + id, err := uuid.NewV4() + if err != nil { + return "", nil + } + return id.String()[:6], nil +} + +func createServiceAccount() bool { + createSA, err := strconv.ParseBool(os.Getenv(ENV_CREATE_SA)) + if err != nil { + return false + } + return createSA +} + +func getSAInterval() time.Duration { + SAInterval, err := GetUIntValueFromEnv(ENV_SA_INTERVAL) + if err != nil { + return time.Duration(30) * time.Minute + } + return time.Duration(SAInterval) * time.Minute +}