Commit 58aa139f authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #48836 from ericchiang/audit-policy-subresource-resource-name

Automatic merge from submit-queue (batch tested with PRs 51574, 51534, 49257, 44680, 48836) audit policy: support subresources and resource names Updates #48561 policy: - level: Metadata resources: - group: "" resources ["pods/logs"] - level: None resources: - group: "" resources: ["configmaps"] resourceNames: ["controller-leader"] The top level resource no longer matches the subresource. For example "pods" no longer matches requests to the logs subresource on pods. ```release-note Audit policy supports matching subresources and resource names, but the top level resource no longer matches the subresouce. For example "pods" no longer matches requests to the logs subresource of pods. Use "pods/logs" to match subresources. ``` /cc @sttts @soltysh @crassirostris @ihmccreery @timstclair
parents 43a40f7a 9caff690
......@@ -215,10 +215,17 @@ type GroupResources struct {
// The empty string represents the core API group.
// +optional
Group string
// Resources is a list of resources within the API group.
// Any empty list implies every resource kind in the API group.
// Resources is a list of resources within the API group. Subresources are
// matched using a "/" to indicate the subresource. For example, "pods/logs"
// would match request to the logs subresource of pods. The top level resource
// does not match subresources, "pods" doesn't match "pods/logs".
// +optional
Resources []string
// ResourceNames is a list of resource instance names that the policy matches.
// Using this field requires Resources to be specified.
// An empty list implies that every instance of the resource is matched.
// +optional
ResourceNames []string
}
// ObjectReference contains enough information to let you inspect or modify the referred object.
......
......@@ -106,10 +106,18 @@ message GroupResources {
// +optional
optional string group = 1;
// Resources is a list of resources within the API group.
// Any empty list implies every resource kind in the API group.
// Resources is a list of resources within the API group. Subresources are
// matched using a "/" to indicate the subresource. For example, "pods/logs"
// would match request to the logs subresource of pods. The top level resource
// does not match subresources, "pods" doesn't match "pods/logs".
// +optional
repeated string resources = 2;
// ResourceNames is a list of resource instance names that the policy matches.
// Using this field requires Resources to be specified.
// An empty list implies that every instance of the resource is matched.
// +optional
repeated string resourceNames = 3;
}
// ObjectReference contains enough information to let you inspect or modify the referred object.
......
......@@ -2887,14 +2887,15 @@ func (x *GroupResources) CodecEncodeSelf(e *codec1978.Encoder) {
} else {
yysep2 := !z.EncBinary()
yy2arr2 := z.EncBasicHandle().StructToArray
var yyq2 [2]bool
var yyq2 [3]bool
_, _, _ = yysep2, yyq2, yy2arr2
const yyr2 bool = false
yyq2[0] = x.Group != ""
yyq2[1] = len(x.Resources) != 0
yyq2[2] = len(x.ResourceNames) != 0
var yynn2 int
if yyr2 || yy2arr2 {
r.EncodeArrayStart(2)
r.EncodeArrayStart(3)
} else {
yynn2 = 0
for _, b := range yyq2 {
......@@ -2964,6 +2965,39 @@ func (x *GroupResources) CodecEncodeSelf(e *codec1978.Encoder) {
}
}
if yyr2 || yy2arr2 {
z.EncSendContainerState(codecSelfer_containerArrayElem1234)
if yyq2[2] {
if x.ResourceNames == nil {
r.EncodeNil()
} else {
yym10 := z.EncBinary()
_ = yym10
if false {
} else {
z.F.EncSliceStringV(x.ResourceNames, false, e)
}
}
} else {
r.EncodeNil()
}
} else {
if yyq2[2] {
z.EncSendContainerState(codecSelfer_containerMapKey1234)
r.EncodeString(codecSelferC_UTF81234, string("resourceNames"))
z.EncSendContainerState(codecSelfer_containerMapValue1234)
if x.ResourceNames == nil {
r.EncodeNil()
} else {
yym11 := z.EncBinary()
_ = yym11
if false {
} else {
z.F.EncSliceStringV(x.ResourceNames, false, e)
}
}
}
}
if yyr2 || yy2arr2 {
z.EncSendContainerState(codecSelfer_containerArrayEnd1234)
} else {
z.EncSendContainerState(codecSelfer_containerMapEnd1234)
......@@ -3048,6 +3082,18 @@ func (x *GroupResources) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) {
z.F.DecSliceStringX(yyv6, false, d)
}
}
case "resourceNames":
if r.TryDecodeAsNil() {
x.ResourceNames = nil
} else {
yyv8 := &x.ResourceNames
yym9 := z.DecBinary()
_ = yym9
if false {
} else {
z.F.DecSliceStringX(yyv8, false, d)
}
}
default:
z.DecStructFieldNotFound(-1, yys3)
} // end switch yys3
......@@ -3059,16 +3105,16 @@ func (x *GroupResources) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
var h codecSelfer1234
z, r := codec1978.GenHelperDecoder(d)
_, _, _ = h, z, r
var yyj8 int
var yyb8 bool
var yyhl8 bool = l >= 0
yyj8++
if yyhl8 {
yyb8 = yyj8 > l
var yyj10 int
var yyb10 bool
var yyhl10 bool = l >= 0
yyj10++
if yyhl10 {
yyb10 = yyj10 > l
} else {
yyb8 = r.CheckBreak()
yyb10 = r.CheckBreak()
}
if yyb8 {
if yyb10 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return
}
......@@ -3076,21 +3122,21 @@ func (x *GroupResources) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
if r.TryDecodeAsNil() {
x.Group = ""
} else {
yyv9 := &x.Group
yym10 := z.DecBinary()
_ = yym10
yyv11 := &x.Group
yym12 := z.DecBinary()
_ = yym12
if false {
} else {
*((*string)(yyv9)) = r.DecodeString()
*((*string)(yyv11)) = r.DecodeString()
}
}
yyj8++
if yyhl8 {
yyb8 = yyj8 > l
yyj10++
if yyhl10 {
yyb10 = yyj10 > l
} else {
yyb8 = r.CheckBreak()
yyb10 = r.CheckBreak()
}
if yyb8 {
if yyb10 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return
}
......@@ -3098,26 +3144,48 @@ func (x *GroupResources) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) {
if r.TryDecodeAsNil() {
x.Resources = nil
} else {
yyv11 := &x.Resources
yym12 := z.DecBinary()
_ = yym12
yyv13 := &x.Resources
yym14 := z.DecBinary()
_ = yym14
if false {
} else {
z.F.DecSliceStringX(yyv13, false, d)
}
}
yyj10++
if yyhl10 {
yyb10 = yyj10 > l
} else {
yyb10 = r.CheckBreak()
}
if yyb10 {
z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
return
}
z.DecSendContainerState(codecSelfer_containerArrayElem1234)
if r.TryDecodeAsNil() {
x.ResourceNames = nil
} else {
yyv15 := &x.ResourceNames
yym16 := z.DecBinary()
_ = yym16
if false {
} else {
z.F.DecSliceStringX(yyv11, false, d)
z.F.DecSliceStringX(yyv15, false, d)
}
}
for {
yyj8++
if yyhl8 {
yyb8 = yyj8 > l
yyj10++
if yyhl10 {
yyb10 = yyj10 > l
} else {
yyb8 = r.CheckBreak()
yyb10 = r.CheckBreak()
}
if yyb8 {
if yyb10 {
break
}
z.DecSendContainerState(codecSelfer_containerArrayElem1234)
z.DecStructFieldNotFound(yyj8-1, "")
z.DecStructFieldNotFound(yyj10-1, "")
}
z.DecSendContainerState(codecSelfer_containerArrayEnd1234)
}
......@@ -4063,7 +4131,7 @@ func (x codecSelfer1234) decSliceGroupResources(v *[]GroupResources, d *codec197
yyrg1 := len(yyv1) > 0
yyv21 := yyv1
yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 40)
yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 64)
if yyrt1 {
if yyrl1 <= cap(yyv1) {
yyv1 = yyv1[:yyrl1]
......
......@@ -216,10 +216,17 @@ type GroupResources struct {
// The empty string represents the core API group.
// +optional
Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"`
// Resources is a list of resources within the API group.
// Any empty list implies every resource kind in the API group.
// Resources is a list of resources within the API group. Subresources are
// matched using a "/" to indicate the subresource. For example, "pods/logs"
// would match request to the logs subresource of pods. The top level resource
// does not match subresources, "pods" doesn't match "pods/logs".
// +optional
Resources []string `json:"resources,omitempty" protobuf:"bytes,2,rep,name=resources"`
// ResourceNames is a list of resource instance names that the policy matches.
// Using this field requires Resources to be specified.
// An empty list implies that every instance of the resource is matched.
// +optional
ResourceNames []string `json:"resourceNames,omitempty" protobuf:"bytes,3,rep,name=resourceNames"`
}
// ObjectReference contains enough information to let you inspect or modify the referred object.
......
......@@ -132,6 +132,7 @@ func Convert_audit_EventList_To_v1alpha1_EventList(in *audit.EventList, out *Eve
func autoConvert_v1alpha1_GroupResources_To_audit_GroupResources(in *GroupResources, out *audit.GroupResources, s conversion.Scope) error {
out.Group = in.Group
out.Resources = *(*[]string)(unsafe.Pointer(&in.Resources))
out.ResourceNames = *(*[]string)(unsafe.Pointer(&in.ResourceNames))
return nil
}
......@@ -143,6 +144,7 @@ func Convert_v1alpha1_GroupResources_To_audit_GroupResources(in *GroupResources,
func autoConvert_audit_GroupResources_To_v1alpha1_GroupResources(in *audit.GroupResources, out *GroupResources, s conversion.Scope) error {
out.Group = in.Group
out.Resources = *(*[]string)(unsafe.Pointer(&in.Resources))
out.ResourceNames = *(*[]string)(unsafe.Pointer(&in.ResourceNames))
return nil
}
......
......@@ -190,6 +190,11 @@ func (in *GroupResources) DeepCopyInto(out *GroupResources) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ResourceNames != nil {
in, out := &in.ResourceNames, &out.ResourceNames
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
......
......@@ -209,10 +209,17 @@ type GroupResources struct {
// The empty string represents the core API group.
// +optional
Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"`
// Resources is a list of resources within the API group.
// Any empty list implies every resource kind in the API group.
// Resources is a list of resources within the API group. Subresources are
// matched using a "/" to indicate the subresource. For example, "pods/logs"
// would match request to the logs subresource of pods. The top level resource
// does not match subresources, "pods" doesn't match "pods/logs".
// +optional
Resources []string `json:"resources,omitempty" protobuf:"bytes,2,rep,name=resources"`
// ResourceNames is a list of resource instance names that the policy matches.
// Using this field requires Resources to be specified.
// An empty list implies that every instance of the resource is matched.
// +optional
ResourceNames []string `json:"resourceNames,omitempty" protobuf:"bytes,3,rep,name=resourceNames"`
}
// ObjectReference contains enough information to let you inspect or modify the referred object.
......
......@@ -132,6 +132,7 @@ func Convert_audit_EventList_To_v1beta1_EventList(in *audit.EventList, out *Even
func autoConvert_v1beta1_GroupResources_To_audit_GroupResources(in *GroupResources, out *audit.GroupResources, s conversion.Scope) error {
out.Group = in.Group
out.Resources = *(*[]string)(unsafe.Pointer(&in.Resources))
out.ResourceNames = *(*[]string)(unsafe.Pointer(&in.ResourceNames))
return nil
}
......@@ -143,6 +144,7 @@ func Convert_v1beta1_GroupResources_To_audit_GroupResources(in *GroupResources,
func autoConvert_audit_GroupResources_To_v1beta1_GroupResources(in *audit.GroupResources, out *GroupResources, s conversion.Scope) error {
out.Group = in.Group
out.Resources = *(*[]string)(unsafe.Pointer(&in.Resources))
out.ResourceNames = *(*[]string)(unsafe.Pointer(&in.ResourceNames))
return nil
}
......
......@@ -190,6 +190,11 @@ func (in *GroupResources) DeepCopyInto(out *GroupResources) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ResourceNames != nil {
in, out := &in.ResourceNames, &out.ResourceNames
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
......
......@@ -88,16 +88,18 @@ func validateResources(groupResources []audit.GroupResources, fldPath *field.Pat
var allErrs field.ErrorList
for _, groupResource := range groupResources {
// The empty string represents the core API group.
if len(groupResource.Group) == 0 {
continue
if len(groupResource.Group) != 0 {
// Group names must be lower case and be valid DNS subdomains.
// reference: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md
// an error is returned for group name like rbac.authorization.k8s.io/v1beta1
// rbac.authorization.k8s.io is the valid one
if msgs := validation.NameIsDNSSubdomain(groupResource.Group, false); len(msgs) != 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), groupResource.Group, strings.Join(msgs, ",")))
}
}
// Group names must be lower case and be valid DNS subdomains.
// reference: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md
// an error is returned for group name like rbac.authorization.k8s.io/v1beta1
// rbac.authorization.k8s.io is the valid one
if msgs := validation.NameIsDNSSubdomain(groupResource.Group, false); len(msgs) != 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), groupResource.Group, strings.Join(msgs, ",")))
if len(groupResource.ResourceNames) > 0 && len(groupResource.Resources) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceNames"), groupResource.ResourceNames, "using resourceNames requires at least one resource"))
}
}
return allErrs
......
......@@ -102,6 +102,12 @@ func TestValidatePolicy(t *testing.T) {
"/metrics",
},
},
{ // ResourceNames without Resources
Level: audit.LevelMetadata,
Verbs: []string{"get"},
Resources: []audit.GroupResources{{ResourceNames: []string{"leader"}}},
Namespaces: []string{"kube-system"},
},
}
errorCases := []audit.Policy{}
for _, rule := range invalidRules {
......
......@@ -188,6 +188,11 @@ func (in *GroupResources) DeepCopyInto(out *GroupResources) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ResourceNames != nil {
in, out := &in.ResourceNames, &out.ResourceNames
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
......
......@@ -143,6 +143,15 @@ func ruleMatchesResource(r *audit.PolicyRule, attrs authorizer.Attributes) bool
apiGroup := attrs.GetAPIGroup()
resource := attrs.GetResource()
// If subresource, the resource in the policy must match "(resource)/(subresource)"
//
// TODO: consider adding options like "pods/*" to match all subresources.
if sr := attrs.GetSubresource(); sr != "" {
resource = resource + "/" + sr
}
name := attrs.GetName()
for _, gr := range r.Resources {
if gr.Group == apiGroup {
if len(gr.Resources) == 0 {
......@@ -150,7 +159,9 @@ func ruleMatchesResource(r *audit.PolicyRule, attrs authorizer.Attributes) bool
}
for _, res := range gr.Resources {
if res == resource {
return true
if len(gr.ResourceNames) == 0 || hasString(gr.ResourceNames, name) {
return true
}
}
}
}
......
......@@ -61,6 +61,18 @@ func TestChecker(t *testing.T) {
ResourceRequest: false,
Path: "/logs/kubelet.log",
},
"subresource": &authorizer.AttributesRecord{
User: tim,
Verb: "get",
Namespace: "default",
APIGroup: "", // Core
APIVersion: "v1",
Resource: "pods",
Subresource: "log",
Name: "busybox",
ResourceRequest: true,
Path: "/api/v1/namespaces/default/pods/busybox",
},
}
rules := map[string]audit.PolicyRule{
......@@ -88,6 +100,11 @@ func TestChecker(t *testing.T) {
Verbs: []string{"get"},
Resources: []audit.GroupResources{{Resources: []string{"pods"}}},
},
"getPodLogs": {
Level: audit.LevelRequest,
Verbs: []string{"get"},
Resources: []audit.GroupResources{{Resources: []string{"pods/log"}}},
},
"getClusterRoles": {
Level: audit.LevelRequestResponse,
Verbs: []string{"get"},
......@@ -111,6 +128,14 @@ func TestChecker(t *testing.T) {
"/metrics",
},
},
"clusterRoleEdit": {
Level: audit.LevelRequest,
Resources: []audit.GroupResources{{
Group: "rbac.authorization.k8s.io",
Resources: []string{"clusterroles"},
ResourceNames: []string{"edit"},
}},
},
}
test := func(req string, expected audit.Level, ruleNames ...string) {
......@@ -135,6 +160,7 @@ func TestChecker(t *testing.T) {
test("namespaced", audit.LevelNone, "getMetrics")
test("namespaced", audit.LevelMetadata, "getMetrics", "serviceAccounts", "default")
test("namespaced", audit.LevelRequestResponse, "getMetrics", "getPods", "default")
test("namespaced", audit.LevelRequestResponse, "getPodLogs", "getPods")
test("cluster", audit.LevelMetadata, "default")
test("cluster", audit.LevelNone, "create")
......@@ -143,10 +169,12 @@ func TestChecker(t *testing.T) {
test("cluster", audit.LevelNone, "serviceAccounts")
test("cluster", audit.LevelNone, "getPods")
test("cluster", audit.LevelRequestResponse, "getClusterRoles")
test("cluster", audit.LevelRequest, "clusterRoleEdit", "getClusterRoles")
test("cluster", audit.LevelNone, "getLogs")
test("cluster", audit.LevelNone, "getMetrics")
test("cluster", audit.LevelMetadata, "getMetrics", "serviceAccounts", "default")
test("cluster", audit.LevelRequestResponse, "getMetrics", "getClusterRoles", "default")
test("cluster", audit.LevelNone, "getPodLogs", "getPods")
test("nonResource", audit.LevelMetadata, "default")
test("nonResource", audit.LevelNone, "create")
......@@ -159,4 +187,8 @@ func TestChecker(t *testing.T) {
test("nonResource", audit.LevelNone, "getMetrics")
test("nonResource", audit.LevelMetadata, "getMetrics", "serviceAccounts", "default")
test("nonResource", audit.LevelRequestResponse, "getLogs", "getClusterRoles", "default")
test("nonResource", audit.LevelNone, "getPodLogs", "getPods")
test("subresource", audit.LevelRequest, "getPodLogs", "getPods")
test("subresource", audit.LevelRequest, "getPods", "getPodLogs")
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment