diff --git a/cli/cmd/check.go b/cli/cmd/check.go index b3c658a40..d5187ef8c 100644 --- a/cli/cmd/check.go +++ b/cli/cmd/check.go @@ -1,8 +1,11 @@ package cmd import ( + "github.com/creasty/defaults" "github.com/spf13/cobra" + "github.com/up9inc/mizu/cli/config/configStructs" "github.com/up9inc/mizu/cli/telemetry" + "github.com/up9inc/mizu/shared/logger" ) var checkCmd = &cobra.Command{ @@ -17,4 +20,11 @@ var checkCmd = &cobra.Command{ func init() { rootCmd.AddCommand(checkCmd) + + defaultCheckConfig := configStructs.CheckConfig{} + if err := defaults.Set(&defaultCheckConfig); err != nil { + logger.Log.Debug(err) + } + + checkCmd.Flags().Bool(configStructs.PreTapCheckName, defaultCheckConfig.PreTap, "Check pre-tap Mizu installation for potential problems") } diff --git a/cli/cmd/checkRunner.go b/cli/cmd/checkRunner.go index 7bbb0d8d0..24d140acd 100644 --- a/cli/cmd/checkRunner.go +++ b/cli/cmd/checkRunner.go @@ -3,6 +3,10 @@ package cmd import ( "context" "fmt" + "github.com/up9inc/mizu/shared" + rbac "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" "regexp" "github.com/up9inc/mizu/cli/apiserver" @@ -14,7 +18,7 @@ import ( ) func runMizuCheck() { - logger.Log.Infof("Mizu install checks\n===================") + logger.Log.Infof("Mizu checks\n===================") ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel will be called when this function exits @@ -25,17 +29,23 @@ func runMizuCheck() { checkPassed = checkKubernetesVersion(kubernetesVersion) } - var isInstallCommand bool - if checkPassed { - checkPassed, isInstallCommand = checkMizuMode(ctx, kubernetesProvider) - } + if config.Config.Check.PreTap { + if checkPassed { + checkPassed = checkK8sTapPermissions(ctx, kubernetesProvider) + } + } else { + var isInstallCommand bool + if checkPassed { + checkPassed, isInstallCommand = checkMizuMode(ctx, kubernetesProvider) + } - if checkPassed { - checkPassed = checkK8sResources(ctx, kubernetesProvider, isInstallCommand) - } + if checkPassed { + checkPassed = checkK8sResources(ctx, kubernetesProvider, isInstallCommand) + } - if checkPassed { - checkPassed = checkServerConnection(kubernetesProvider) + if checkPassed { + checkPassed = checkServerConnection(kubernetesProvider) + } } if checkPassed { @@ -273,9 +283,81 @@ func checkResourceExist(resourceName string, resourceType string, exist bool, er } else if !exist { logger.Log.Errorf("%v '%v' %v doesn't exist", fmt.Sprintf(uiUtils.Red, "✗"), resourceName, resourceType) return false - } else { - logger.Log.Infof("%v '%v' %v exists", fmt.Sprintf(uiUtils.Green, "√"), resourceName, resourceType) } + logger.Log.Infof("%v '%v' %v exists", fmt.Sprintf(uiUtils.Green, "√"), resourceName, resourceType) + return true +} + +func checkK8sTapPermissions(ctx context.Context, kubernetesProvider *kubernetes.Provider) bool { + logger.Log.Infof("\nkubernetes-permissions\n--------------------") + + var filePath string + if config.Config.IsNsRestrictedMode() { + filePath = "./examples/roles/permissions-ns-tap.yaml" + } else { + filePath = "./examples/roles/permissions-all-namespaces-tap.yaml" + } + + data, err := shared.ReadFromFile(filePath) + if err != nil { + logger.Log.Errorf("%v error while checking kubernetes permissions, err: %v", fmt.Sprintf(uiUtils.Red, "✗"), err) + return false + } + + obj, err := getDecodedObject(data) + if err != nil { + logger.Log.Errorf("%v error while checking kubernetes permissions, err: %v", fmt.Sprintf(uiUtils.Red, "✗"), err) + return false + } + + var rules []rbac.PolicyRule + if config.Config.IsNsRestrictedMode() { + rules = obj.(*rbac.Role).Rules + } else { + rules = obj.(*rbac.ClusterRole).Rules + } + + return checkPermissions(ctx, kubernetesProvider, rules) +} + +func getDecodedObject(data []byte) (runtime.Object, error) { + decode := scheme.Codecs.UniversalDeserializer().Decode + + obj, _, err := decode(data, nil, nil) + if err != nil { + return nil, err + } + + return obj, nil +} + +func checkPermissions(ctx context.Context, kubernetesProvider *kubernetes.Provider, rules []rbac.PolicyRule) bool { + permissionsExist := true + + for _, rule := range rules { + for _, group := range rule.APIGroups { + for _, resource := range rule.Resources { + for _, verb := range rule.Verbs { + exist, err := kubernetesProvider.CanI(ctx, config.Config.MizuResourcesNamespace, resource, verb, group) + permissionsExist = checkPermissionExist(group, resource, verb, exist, err) && permissionsExist + } + } + } + } + + return permissionsExist +} + +func checkPermissionExist(group string, resource string, verb string, exist bool, err error) bool { + if err != nil { + logger.Log.Errorf("%v error checking permission for %v %v in group '%v', err: %v", fmt.Sprintf(uiUtils.Red, "✗"), verb, resource, group, err) + return false + } else if !exist { + logger.Log.Errorf("%v can't %v %v in group '%v'", fmt.Sprintf(uiUtils.Red, "✗"), verb, resource, group) + return false + } + + logger.Log.Infof("%v can %v %v in group '%v'", fmt.Sprintf(uiUtils.Green, "√"), verb, resource, group) return true } diff --git a/cli/config/configStruct.go b/cli/config/configStruct.go index a993e1616..c64c795a0 100644 --- a/cli/config/configStruct.go +++ b/cli/config/configStruct.go @@ -22,6 +22,7 @@ const ( type ConfigStruct struct { Tap configStructs.TapConfig `yaml:"tap"` + Check configStructs.CheckConfig `yaml:"check"` Version configStructs.VersionConfig `yaml:"version"` View configStructs.ViewConfig `yaml:"view"` Logs configStructs.LogsConfig `yaml:"logs"` diff --git a/cli/config/configStructs/checkConfig.go b/cli/config/configStructs/checkConfig.go new file mode 100644 index 000000000..699fe9d23 --- /dev/null +++ b/cli/config/configStructs/checkConfig.go @@ -0,0 +1,9 @@ +package configStructs + +const ( + PreTapCheckName = "pre-tap" +) + +type CheckConfig struct { + PreTap bool `yaml:"pre-tap"` +} diff --git a/shared/fileUtils.go b/shared/fileUtils.go new file mode 100644 index 000000000..976ff6c2d --- /dev/null +++ b/shared/fileUtils.go @@ -0,0 +1,20 @@ +package shared + +import ( + "io/ioutil" + "os" +) + +func ReadFromFile(path string) ([]byte, error) { + reader, err := os.Open(path) + if err != nil { + return nil, err + } + + data, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/shared/kubernetes/provider.go b/shared/kubernetes/provider.go index 18301cb05..0b16cb2a8 100644 --- a/shared/kubernetes/provider.go +++ b/shared/kubernetes/provider.go @@ -17,6 +17,7 @@ import ( "github.com/up9inc/mizu/shared/semver" "github.com/up9inc/mizu/tap/api" v1 "k8s.io/api/apps/v1" + auth "k8s.io/api/authorization/v1" core "k8s.io/api/core/v1" rbac "k8s.io/api/rbac/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -443,6 +444,26 @@ func (provider *Provider) CreateService(ctx context.Context, namespace string, s return provider.clientSet.CoreV1().Services(namespace).Create(ctx, &service, metav1.CreateOptions{}) } +func (provider *Provider) CanI(ctx context.Context, namespace string, resource string, verb string, group string) (bool, error) { + selfSubjectAccessReview := &auth.SelfSubjectAccessReview{ + Spec: auth.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &auth.ResourceAttributes{ + Namespace: namespace, + Resource: resource, + Verb: verb, + Group: group, + }, + }, + } + + response, err := provider.clientSet.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, selfSubjectAccessReview, metav1.CreateOptions{}) + if err != nil { + return false, err + } + + return response.Status.Allowed, nil +} + func (provider *Provider) DoesNamespaceExist(ctx context.Context, name string) (bool, error) { namespaceResource, err := provider.clientSet.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) return provider.doesResourceExist(namespaceResource, err)