/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package validate

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"os/exec"
	"sync"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/sirupsen/logrus"
	internalapi "k8s.io/cri-api/pkg/apis"
	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"

	"sigs.k8s.io/cri-tools/pkg/framework"
)

const (
	apparmorProfileNamePrefix = "localhost/"
	testProfiles              = `
#include <tunables/global>
profile cri-validate-apparmor-test-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>
  file,
  # Deny all file writes.
  deny /** w,
}
profile cri-validate-apparmor-test-audit-write flags=(attach_disconnected) {
  #include <abstractions/base>
  file,
  # Only audit file writes.
  audit /** w,
}
`
)

// The AppArmor profile to the CRI via the deprecated apparmor_profile field
// in favor of the newer structured apparmor field.
// CRI provides the AppArmor profile via both fields to maintain backwards compatibility.
var _ = framework.KubeDescribe("AppArmor", func() {
	f := framework.NewDefaultCRIFramework()

	var (
		rc                internalapi.RuntimeService
		ic                internalapi.ImageManagerService
		checkAppArmorOnce sync.Once
		appArmorEnabled   bool
	)

	appArmorIsEnabled := func() bool {
		checkAppArmorOnce.Do(func() {
			buf, err := os.ReadFile("/sys/module/apparmor/parameters/enabled")
			appArmorEnabled = err == nil && len(buf) > 1 && buf[0] == 'Y'
		})

		return appArmorEnabled
	}

	BeforeEach(func(ctx SpecContext) {
		if !appArmorIsEnabled() {
			Skip("AppArmor is disabled on this host")
		}

		rc = f.CRIClient.CRIRuntimeClient
		ic = f.CRIClient.CRIImageClient

		Expect(loadTestProfiles(ctx)).NotTo(HaveOccurred())
	})

	Context("runtime should support deprecated apparmor_profile field", func() {
		var (
			sandboxID     string
			sandboxConfig *runtimeapi.PodSandboxConfig
		)

		BeforeEach(func(ctx SpecContext) {
			sandboxID, sandboxConfig = framework.CreatePodSandboxForContainer(ctx, rc)
		})

		AfterEach(func(ctx SpecContext) {
			framework.CleanupPodSandbox(ctx, rc, sandboxID)
		})

		It("should fail with an unloaded apparmor_profile", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				ApparmorProfile: apparmorProfileNamePrefix + "non-existent-profile",
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, false)
			Expect(containerID).To(BeEmpty())
		})

		It("should enforce a apparmor_profile blocking writes", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				ApparmorProfile: apparmorProfileNamePrefix + "cri-validate-apparmor-test-deny-write",
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, true)
			checkContainerApparmor(ctx, rc, containerID, false)
		})

		It("should enforce a permissive deprecated profile", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				ApparmorProfile: apparmorProfileNamePrefix + "cri-validate-apparmor-test-audit-write",
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, true)
			checkContainerApparmor(ctx, rc, containerID, true)
		})
	})

	Context("runtime should support apparmor field", func() {
		var (
			sandboxID     string
			sandboxConfig *runtimeapi.PodSandboxConfig
		)

		BeforeEach(func(ctx SpecContext) {
			sandboxID, sandboxConfig = framework.CreatePodSandboxForContainer(ctx, rc)
		})

		AfterEach(func(ctx SpecContext) {
			framework.CleanupPodSandbox(ctx, rc, sandboxID)
		})

		It("should fail with an unloaded apparmor_profile", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				Apparmor: &runtimeapi.SecurityProfile{
					ProfileType:  runtimeapi.SecurityProfile_Localhost,
					LocalhostRef: apparmorProfileNamePrefix + "non-existent-profile",
				},
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, false)
			Expect(containerID).To(BeEmpty())
		})

		It("should enforce a apparmor_profile blocking writes", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				Apparmor: &runtimeapi.SecurityProfile{
					ProfileType:  runtimeapi.SecurityProfile_Localhost,
					LocalhostRef: apparmorProfileNamePrefix + "cri-validate-apparmor-test-deny-write",
				},
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, true)
			checkContainerApparmor(ctx, rc, containerID, false)
		})

		It("should enforce a permissive deprecated profile", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				Apparmor: &runtimeapi.SecurityProfile{
					ProfileType:  runtimeapi.SecurityProfile_Localhost,
					LocalhostRef: apparmorProfileNamePrefix + "cri-validate-apparmor-test-audit-write",
				},
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, true)
			checkContainerApparmor(ctx, rc, containerID, true)
		})
	})

	Context("runtime should prefer new apparmor field", func() {
		var (
			sandboxID     string
			sandboxConfig *runtimeapi.PodSandboxConfig
		)

		BeforeEach(func(ctx SpecContext) {
			sandboxID, sandboxConfig = framework.CreatePodSandboxForContainer(ctx, rc)
		})

		AfterEach(func(ctx SpecContext) {
			framework.CleanupPodSandbox(ctx, rc, sandboxID)
		})

		It("should fail with an unloaded apparmor_profile", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				ApparmorProfile: apparmorProfileNamePrefix + "non-existent-profile",
				Apparmor: &runtimeapi.SecurityProfile{
					ProfileType:  runtimeapi.SecurityProfile_Localhost,
					LocalhostRef: apparmorProfileNamePrefix + "non-existent-profile",
				},
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, false)
			Expect(containerID).To(BeEmpty())
		})

		It("should enforce a apparmor_profile blocking writes", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				ApparmorProfile: apparmorProfileNamePrefix + "non-existent-profile",
				Apparmor: &runtimeapi.SecurityProfile{
					ProfileType:  runtimeapi.SecurityProfile_Localhost,
					LocalhostRef: apparmorProfileNamePrefix + "cri-validate-apparmor-test-deny-write",
				},
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, true)
			checkContainerApparmor(ctx, rc, containerID, false)
		})

		It("should work with apparmor profile", func(ctx SpecContext) {
			profile := &runtimeapi.LinuxContainerSecurityContext{
				ApparmorProfile: apparmorProfileNamePrefix + "non-existent-profile",
				Apparmor: &runtimeapi.SecurityProfile{
					ProfileType:  runtimeapi.SecurityProfile_Localhost,
					LocalhostRef: apparmorProfileNamePrefix + "cri-validate-apparmor-test-audit-write",
				},
			}
			containerID := createContainerWithAppArmor(ctx, rc, ic, sandboxID, sandboxConfig, profile, true)
			checkContainerApparmor(ctx, rc, containerID, true)
		})
	})
})

func createContainerWithAppArmor(ctx context.Context, rc internalapi.RuntimeService, ic internalapi.ImageManagerService, sandboxID string, sandboxConfig *runtimeapi.PodSandboxConfig, profile *runtimeapi.LinuxContainerSecurityContext, shouldSucceed bool) string {
	By("create a container with apparmor")

	containerName := "apparmor-test-" + framework.NewUUID()
	containerConfig := &runtimeapi.ContainerConfig{
		Metadata: framework.BuildContainerMetadata(containerName, framework.DefaultAttempt),
		Image:    &runtimeapi.ImageSpec{Image: framework.TestContext.TestImageList.DefaultTestContainerImage},
		Command:  []string{"touch", "/tmp/foo"},
		Linux: &runtimeapi.LinuxContainerConfig{
			SecurityContext: profile,
		},
	}

	containerID, err := framework.CreateContainerWithError(ctx, rc, ic, containerConfig, sandboxID, sandboxConfig)
	if shouldSucceed {
		Expect(err).ToNot(HaveOccurred())
		By("start container with apparmor")

		err := rc.StartContainer(ctx, containerID)
		Expect(err).NotTo(HaveOccurred())

		// wait container started and check the status.
		Eventually(func() runtimeapi.ContainerState {
			return getContainerStatus(ctx, rc, containerID).GetState()
		}, time.Minute, time.Second*4).Should(Equal(runtimeapi.ContainerState_CONTAINER_EXITED))
	} else {
		Expect(err).To(HaveOccurred())
	}

	return containerID
}

func checkContainerApparmor(ctx context.Context, rc internalapi.RuntimeService, containerID string, shouldRun bool) {
	By("get container status")

	resp, err := rc.ContainerStatus(ctx, containerID, false)
	Expect(err).NotTo(HaveOccurred())

	if shouldRun {
		Expect(resp.GetStatus().GetExitCode()).To(Equal(int32(0)))
	} else {
		Expect(resp.GetStatus().GetExitCode()).NotTo(Equal(int32(0)))
	}
}

func loadTestProfiles(ctx context.Context) error {
	f, err := os.CreateTemp("/tmp", "apparmor")
	if err != nil {
		return fmt.Errorf("open temp file: %w", err)
	}

	defer os.Remove(f.Name())
	defer f.Close()

	if _, err = f.WriteString(testProfiles); err != nil {
		return fmt.Errorf("write profiles to file: %w", err)
	}

	binary, err := exec.LookPath("sudo")
	if err != nil {
		return fmt.Errorf("find sudo binary: %w", err)
	}

	cmd := exec.CommandContext(ctx, binary, "apparmor_parser", "-r", "-W", f.Name())
	stderr := &bytes.Buffer{}
	cmd.Stderr = stderr
	out, err := cmd.Output()
	// apparmor_parser does not always return an error code, so consider any stderr output an error.
	if err != nil || stderr.Len() > 0 {
		if stderr.Len() > 0 {
			logrus.Warn(stderr.String())
		}

		if len(out) > 0 {
			logrus.Infof("apparmor_parser: %s", out)
		}

		return fmt.Errorf("load profiles: %w", err)
	}

	return nil
}
