// Copyright IBM Corp. 2015, 2025
// SPDX-License-Identifier: MPL-2.0

package capabilities

import (
	"fmt"
	"regexp"
	"runtime"

	"github.com/moby/sys/capability"
)

const (
	// HCLSpecLiteral is an equivalent list to NomadDefaults, expressed as a literal
	// HCL string for use in HCL config parsing.
	HCLSpecLiteral = `["AUDIT_WRITE","CHOWN","DAC_OVERRIDE","FOWNER","FSETID","KILL","MKNOD","NET_BIND_SERVICE","SETFCAP","SETGID","SETPCAP","SETUID","SYS_CHROOT"]`
)

var (
	extractLiteral = regexp.MustCompile(`([\w]+)`)
)

// NomadDefaults is the set of Linux capabilities that Nomad enables by
// default. This list originates from what Docker enabled by default, but then
// excludes NET_RAW for security reasons.
//
// This set is use in the as HCL configuration default, described by HCLSpecLiteral.
func NomadDefaults() *Set {
	return New(extractLiteral.FindAllString(HCLSpecLiteral, -1))
}

// Supported returns the set of capabilities supported by the operating system.
//
// This set will expand over time as new capabilities are introduced to the kernel
// and the capability library is updated (which tends to happen to keep up with
// run-container libraries).
//
// Defers to a library generated from
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h
func Supported() *Set {
	s := New(nil)

	var list []capability.Cap

	switch runtime.GOOS {
	case "linux":
		list, _ = capability.ListSupported()
	default:
		// capability.ListSupported() will always return an empty list on
		// non-linux systems
		list = capability.ListKnown()
	}

	// accumulate every capability supported by this system
	for _, c := range list {
		s.Add(c.String())
	}

	return s
}

// LegacySupported returns the historical set of capabilities used when a task is
// configured to run as root using the exec task driver. Older versions of Nomad
// always allowed the root user to make use of any capability. Now that the exec
// task driver supports configuring the allowed capabilities, operators are
// encouraged to explicitly opt-in to capabilities beyond this legacy set. We
// maintain the legacy list here, because previous versions of Nomad deferred to
// the capability.List library function, which adds new capabilities over time.
//
// https://github.com/hashicorp/nomad/blob/v1.0.4/vendor/github.com/syndtr/gocapability/capability/enum_gen.go#L88
func LegacySupported() *Set {
	return New([]string{
		"CAP_CHOWN",
		"CAP_DAC_OVERRIDE",
		"CAP_DAC_READ_SEARCH",
		"CAP_FOWNER",
		"CAP_FSETID",
		"CAP_KILL",
		"CAP_SETGID",
		"CAP_SETUID",
		"CAP_SETPCAP",
		"CAP_LINUX_IMMUTABLE",
		"CAP_NET_BIND_SERVICE",
		"CAP_NET_BROADCAST",
		"CAP_NET_ADMIN",
		"CAP_NET_RAW",
		"CAP_IPC_LOCK",
		"CAP_IPC_OWNER",
		"CAP_SYS_MODULE",
		"CAP_SYS_RAWIO",
		"CAP_SYS_CHROOT",
		"CAP_SYS_PTRACE",
		"CAP_SYS_PACCT",
		"CAP_SYS_ADMIN",
		"CAP_SYS_BOOT",
		"CAP_SYS_NICE",
		"CAP_SYS_RESOURCE",
		"CAP_SYS_TIME",
		"CAP_SYS_TTY_CONFIG",
		"CAP_MKNOD",
		"CAP_LEASE",
		"CAP_AUDIT_WRITE",
		"CAP_AUDIT_CONTROL",
		"CAP_SETFCAP",
		"CAP_MAC_OVERRIDE",
		"CAP_MAC_ADMIN",
		"CAP_SYSLOG",
		"CAP_WAKE_ALARM",
		"CAP_BLOCK_SUSPEND",
		"CAP_AUDIT_READ",
	})
}

// Calculate the resulting set of linux capabilities to enable for a task, taking
// into account:
// - default capability basis
// - driver allowable capabilities
// - task capability drops
// - task capability adds
//
// Nomad establishes a standard set of enabled capabilities allowed by the task
// driver if allow_caps is not set. This is the same set that the task will be
// enabled with by default if allow_caps does not further reduce permissions,
// in which case the task capabilities will also be reduced accordingly.
//
// The task will drop any capabilities specified in cap_drop, and add back
// capabilities specified in cap_add. The task will not be allowed to add capabilities
// not set in the allow_caps setting (which by default is the same as the basis).
//
// cap_add takes precedence over cap_drop, enabling the common pattern of dropping
// all capabilities, then adding back the desired smaller set. e.g.
//
//	cap_drop = ["all"]
//	cap_add = ["chown", "kill"]
//
// Note that the resulting capability names are upper-cased and prefixed with
// "CAP_", which is the expected input for the exec/java driver implementation.
func Calculate(basis *Set, allowCaps, capAdd, capDrop []string) ([]string, error) {
	allow := New(allowCaps)
	adds := New(capAdd)

	// determine caps the task wants that are not allowed
	missing := allow.Difference(adds)
	if !missing.Empty() {
		return nil, fmt.Errorf("driver does not allow the following capabilities: %s", missing)
	}

	// the realized enabled capabilities starts with what is allowed both by driver
	// config AND is a member of the basis (i.e. nomad defaults)
	result := basis.Intersect(allow)

	// then remove capabilities the task explicitly drops
	result.Remove(capDrop)

	// then add back capabilities the task explicitly adds
	return result.Union(adds).Slice(true), nil
}

// Delta calculates the set of capabilities that must be added and dropped relative
// to a basis to achieve a desired result. The use case is that the docker driver
// assumes a default set (DockerDefault), and we must calculate what to pass into
// --cap-add and --cap-drop on container creation given the inputs of the docker
// plugin config for allow_caps, and the docker task configuration for cap_add and
// cap_drop. Note that the user provided cap_add and cap_drop settings are always
// included, even if they are redundant with the basis (maintaining existing
// behavior, working with existing tests).
//
// Note that the resulting capability names are lower-cased and not prefixed with
// "CAP_", which is the existing style used with the docker driver implementation.
func Delta(basis *Set, allowCaps, capAdd, capDrop []string) ([]string, []string, error) {
	all := func(caps []string) bool {
		for _, c := range caps {
			if normalize(c) == "all" {
				return true
			}
		}
		return false
	}

	// set of caps allowed by driver
	allow := New(allowCaps)

	// determine caps the task wants that are not allowed
	missing := allow.Difference(New(capAdd))
	if !missing.Empty() {
		return nil, nil, fmt.Errorf("driver does not allow the following capabilities: %s", missing)
	}

	// add what the task is asking for
	add := New(capAdd).Slice(false)
	if all(capAdd) {
		add = []string{"all"}
	}

	// drop what the task removes plus whatever is in the basis that is not
	// in the driver allow configuration
	drop := New(allowCaps).Difference(basis).Union(New(capDrop)).Slice(false)
	if all(capDrop) {
		drop = []string{"all"}
	}

	return add, drop, nil
}
