1/*
2Copyright 2021 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package policy
18
19import (
20	"strings"
21
22	corev1 "k8s.io/api/core/v1"
23	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24	"k8s.io/pod-security-admission/api"
25)
26
27type Check struct {
28	// ID is the unique ID of the check.
29	ID string
30	// Level is the policy level this check belongs to.
31	// Must be Baseline or Restricted.
32	// Baseline checks are evaluated for baseline and restricted namespaces.
33	// Restricted checks are only evaluated for restricted namespaces.
34	Level api.Level
35	// Versions contains one or more revisions of the check that apply to different versions.
36	// If the check is not yet assigned to a version, this must be a single-item list with a MinimumVersion of "".
37	// Otherwise, MinimumVersion of items must represent strictly increasing versions.
38	Versions []VersionedCheck
39}
40
41type VersionedCheck struct {
42	// MinimumVersion is the first policy version this check applies to.
43	// If unset, this check is not yet assigned to a policy version.
44	// If set, must not be "latest".
45	MinimumVersion api.Version
46	// CheckPod determines if the pod is allowed.
47	CheckPod CheckPodFn
48}
49
50type CheckPodFn func(*metav1.ObjectMeta, *corev1.PodSpec) CheckResult
51
52// CheckResult contains the result of checking a pod and indicates whether the pod is allowed,
53// and if not, why it was forbidden.
54//
55// Example output for (false, "host ports", "8080, 9090"):
56//   When checking all pods in a namespace:
57//     disallowed by policy "baseline": host ports, privileged containers, non-default capabilities
58//   When checking an individual pod:
59//     disallowed by policy "baseline": host ports (8080, 9090), privileged containers, non-default capabilities (CAP_NET_RAW)
60type CheckResult struct {
61	// Allowed indicates if the check allowed the pod.
62	Allowed bool
63	// ForbiddenReason must be set if Allowed is false.
64	// ForbiddenReason should be as succinct as possible and is always output.
65	// Examples:
66	// - "host ports"
67	// - "privileged containers"
68	// - "non-default capabilities"
69	ForbiddenReason string
70	// ForbiddenDetail should only be set if Allowed is false, and is optional.
71	// ForbiddenDetail can include specific values that were disallowed and is used when checking an individual object.
72	// Examples:
73	// - list specific invalid host ports: "8080, 9090"
74	// - list specific invalid containers: "container1, container2"
75	// - list specific non-default capabilities: "CAP_NET_RAW"
76	ForbiddenDetail string
77}
78
79// AggergateCheckResult holds the aggregate result of running CheckPod across multiple checks.
80type AggregateCheckResult struct {
81	// Allowed indicates if all checks allowed the pod.
82	Allowed bool
83	// ForbiddenReasons is a slice of the forbidden reasons from all the forbidden checks. It should not include empty strings.
84	// ForbiddenReasons and ForbiddenDetails must have the same number of elements, and the indexes are for the same check.
85	ForbiddenReasons []string
86	// ForbiddenDetails is a slice of the forbidden details from all the forbidden checks. It may include empty strings.
87	// ForbiddenReasons and ForbiddenDetails must have the same number of elements, and the indexes are for the same check.
88	ForbiddenDetails []string
89}
90
91// ForbiddenReason returns a comma-separated string of of the forbidden reasons.
92// Example: host ports, privileged containers, non-default capabilities
93func (a *AggregateCheckResult) ForbiddenReason() string {
94	return strings.Join(a.ForbiddenReasons, ", ")
95}
96
97// ForbiddenDetail returns a detailed forbidden message, with non-empty details formatted in
98// parentheses with the associated reason.
99// Example: host ports (8080, 9090), privileged containers, non-default capabilities (NET_RAW)
100func (a *AggregateCheckResult) ForbiddenDetail() string {
101	var b strings.Builder
102	for i := 0; i < len(a.ForbiddenReasons); i++ {
103		b.WriteString(a.ForbiddenReasons[i])
104		if a.ForbiddenDetails[i] != "" {
105			b.WriteString(" (")
106			b.WriteString(a.ForbiddenDetails[i])
107			b.WriteString(")")
108		}
109		if i != len(a.ForbiddenReasons)-1 {
110			b.WriteString(", ")
111		}
112	}
113	return b.String()
114}
115
116// UnknownForbiddenReason is used as the placeholder forbidden reason for checks that incorrectly disallow without providing a reason.
117const UnknownForbiddenReason = "unknown forbidden reason"
118
119// AggregateCheckPod runs all the checks and aggregates the forbidden results into a single CheckResult.
120// The aggregated reason is a comma-separated
121func AggregateCheckResults(results []CheckResult) AggregateCheckResult {
122	var (
123		reasons []string
124		details []string
125	)
126	for _, result := range results {
127		if !result.Allowed {
128			if len(result.ForbiddenReason) == 0 {
129				reasons = append(reasons, UnknownForbiddenReason)
130			} else {
131				reasons = append(reasons, result.ForbiddenReason)
132			}
133			details = append(details, result.ForbiddenDetail)
134		}
135	}
136	return AggregateCheckResult{
137		Allowed:          len(reasons) == 0,
138		ForbiddenReasons: reasons,
139		ForbiddenDetails: details,
140	}
141}
142
143var (
144	defaultChecks      []func() Check
145	experimentalChecks []func() Check
146)
147
148func addCheck(f func() Check) {
149	// add to experimental or versioned list
150	c := f()
151	if len(c.Versions) == 1 && c.Versions[0].MinimumVersion == (api.Version{}) {
152		experimentalChecks = append(experimentalChecks, f)
153	} else {
154		defaultChecks = append(defaultChecks, f)
155	}
156}
157
158// DefaultChecks returns checks that are expected to be enabled by default.
159// The results are mutually exclusive with ExperimentalChecks.
160// It returns a new copy of checks on each invocation and is expected to be called once at setup time.
161func DefaultChecks() []Check {
162	retval := make([]Check, 0, len(defaultChecks))
163	for _, f := range defaultChecks {
164		retval = append(retval, f())
165	}
166	return retval
167}
168
169// ExperimentalChecks returns checks that have not yet been assigned to policy versions.
170// The results are mutually exclusive with DefaultChecks.
171// It returns a new copy of checks on each invocation and is expected to be called once at setup time.
172func ExperimentalChecks() []Check {
173	retval := make([]Check, 0, len(experimentalChecks))
174	for _, f := range experimentalChecks {
175		retval = append(retval, f())
176	}
177	return retval
178}
179