1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-06 07:57:07 +00:00

Merge pull request #468 from nirmata/345_support_usergroup_info

345 support usergroup info
This commit is contained in:
shuting 2019-11-13 10:04:33 -08:00 committed by GitHub
commit 1beb067c58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1171 additions and 120 deletions

View file

@ -43,6 +43,30 @@ spec:
required:
- resources
properties:
roles:
type: array
items:
type: string
clusterRoles:
type: array
items:
type: string
subjects:
type: array
items:
type: object
required:
- kind
- name
properties:
kind:
type: string
apiGroup:
type: string
name:
type: string
Namespace:
type: string
resources:
type: object
required:
@ -85,6 +109,30 @@ spec:
required:
- resources
properties:
roles:
type: array
items:
type: string
clusterRoles:
type: array
items:
type: string
subjects:
type: array
items:
type: object
required:
- kind
- name
properties:
kind:
type: string
apiGroup:
type: string
name:
type: string
Namespace:
type: string
resources:
type: object
properties:

View file

@ -43,6 +43,30 @@ spec:
required:
- resources
properties:
roles:
type: array
items:
type: string
clusterRoles:
type: array
items:
type: string
subjects:
type: array
items:
type: object
required:
- kind
- name
properties:
kind:
type: string
apiGroup:
type: string
name:
type: string
namespace:
type: string
resources:
type: object
required:
@ -85,6 +109,30 @@ spec:
required:
- resources
properties:
roles:
type: array
items:
type: string
clusterRoles:
type: array
items:
type: string
subjects:
type: array
items:
type: object
required:
- kind
- name
properties:
kind:
type: string
apiGroup:
type: string
name:
type: string
Namespace:
type: string
resources:
type: object
properties:
@ -238,13 +286,4 @@ spec:
namespace:
type: string
creationBlocked:
type: boolean
---
apiVersion: v1
kind: ConfigMap
metadata:
name: init-config
namespace: kyverno
data:
# resource types to be skipped by kyverno policy engine
resourceFilters: "[Event,*,*][*,kube-system,*][*,kube-public,*][*,kube-node-lease,*][Node,*,*][APIService,*,*][TokenReview,*,*][SubjectAccessReview,*,*][*,kyverno,*]"
type: boolean

View file

@ -144,7 +144,8 @@ func main() {
// -- annotations on resources with update details on mutation JSON patches
// -- generate policy violation resource
// -- generate events on policy and resource
server, err := webhooks.NewWebhookServer(pclient, client, tlsPair, pInformer.Kyverno().V1alpha1().ClusterPolicies(), pInformer.Kyverno().V1alpha1().ClusterPolicyViolations(), egen, webhookRegistrationClient, pc.GetPolicyStatusAggregator(), configData, cleanUp)
server, err := webhooks.NewWebhookServer(pclient, client, tlsPair, pInformer.Kyverno().V1alpha1().ClusterPolicies(), pInformer.Kyverno().V1alpha1().ClusterPolicyViolations(),
kubeInformer.Rbac().V1().RoleBindings(),kubeInformer.Rbac().V1().ClusterRoleBindings(),egen, webhookRegistrationClient, pc.GetPolicyStatusAggregator(), configData, cleanUp)
if err != nil {
glog.Fatalf("Unable to create webhook server: %v\n", err)
}

View file

@ -1,6 +1,7 @@
package v1alpha1
import (
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -63,11 +64,17 @@ type Rule struct {
//MatchResources contains resource description of the resources that the rule is to apply on
type MatchResources struct {
Roles []string `json:"roles"`
ClusterRoles []string `json:"clusterRoles"`
Subjects []rbacv1.Subject `json:"subjects"`
ResourceDescription `json:"resources"`
}
//ExcludeResources container resource description of the resources that are to be excluded from the applying the policy rule
type ExcludeResources struct {
Roles []string `json:"roles"`
ClusterRoles []string `json:"clusterRoles"`
Subjects []rbacv1.Subject `json:"subjects"`
ResourceDescription `json:"resources"`
}

View file

@ -21,7 +21,8 @@ limitations under the License.
package v1alpha1
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@ -166,6 +167,21 @@ func (in *ClusterPolicyViolationList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExcludeResources) DeepCopyInto(out *ExcludeResources) {
*out = *in
if in.Roles != nil {
in, out := &in.Roles, &out.Roles
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ClusterRoles != nil {
in, out := &in.ClusterRoles, &out.ClusterRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]v1.Subject, len(*in))
copy(*out, *in)
}
in.ResourceDescription.DeepCopyInto(&out.ResourceDescription)
return
}
@ -209,6 +225,21 @@ func (in *ManagedResourceSpec) DeepCopy() *ManagedResourceSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MatchResources) DeepCopyInto(out *MatchResources) {
*out = *in
if in.Roles != nil {
in, out := &in.Roles, &out.Roles
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ClusterRoles != nil {
in, out := &in.ClusterRoles, &out.ClusterRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Subjects != nil {
in, out := &in.Subjects, &out.Subjects
*out = make([]v1.Subject, len(*in))
copy(*out, *in)
}
in.ResourceDescription.DeepCopyInto(&out.ResourceDescription)
return
}
@ -348,7 +379,7 @@ func (in *ResourceDescription) DeepCopyInto(out *ResourceDescription) {
}
if in.Selector != nil {
in, out := &in.Selector, &out.Selector
*out = new(v1.LabelSelector)
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
return

View file

@ -4,13 +4,15 @@ import (
"time"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// Mutate performs mutation. Overlay first and then mutation patches
func Mutate(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) (response EngineResponse) {
func Mutate(policyContext PolicyContext) (response EngineResponse) {
startTime := time.Now()
policy := policyContext.Policy
resource := policyContext.Resource
// policy information
func() {
// set policy information
@ -39,6 +41,15 @@ func Mutate(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) (r
if !rule.HasMutate() {
continue
}
startTime := time.Now()
if !matchAdmissionInfo(rule, policyContext.AdmissionInfo) {
glog.V(3).Infof("rule '%s' cannot be applied on %s/%s/%s, admission permission: %v",
rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), policyContext.AdmissionInfo)
continue
}
glog.V(4).Infof("Time: Mutate matchAdmissionInfo %v", time.Since(startTime))
// check if the resource satisfies the filter conditions defined in the rule
//TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that
// dont statisfy a policy rule resource description
@ -53,9 +64,10 @@ func Mutate(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) (r
ruleResponse, patchedResource = processOverlay(rule, resource)
if ruleResponse.Success == true && ruleResponse.Patches == nil {
// overlay pattern does not match the resource conditions
glog.Infof(ruleResponse.Message)
glog.V(4).Infof(ruleResponse.Message)
continue
}
glog.Infof("Mutate overlay in rule '%s' successfully applied on %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
response.PolicyResponse.Rules = append(response.PolicyResponse.Rules, ruleResponse)
incrementAppliedRuleCount()
}
@ -64,6 +76,7 @@ func Mutate(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) (r
if rule.Mutation.Patches != nil {
var ruleResponse RuleResponse
ruleResponse, patchedResource = processPatches(rule, resource)
glog.Infof("Mutate patches in rule '%s' successfully applied on %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
response.PolicyResponse.Rules = append(response.PolicyResponse.Rules, ruleResponse)
incrementAppliedRuleCount()
}

View file

@ -31,13 +31,6 @@ func processOverlay(rule kyverno.Rule, resource unstructured.Unstructured) (resp
// resource does not satisfy the overlay pattern, we don't apply this rule
if !reflect.DeepEqual(overlayerr, overlayError{}) {
switch overlayerr.statusCode {
// condition key is not present in the resource, don't apply this rule
// consider as success
case conditionNotPresent:
glog.Infof("Resource %s/%s/%s: %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.ErrorMsg())
response.Success = true
response.Message = overlayerr.ErrorMsg()
return response, resource
// conditions are not met, don't apply this rule
// consider as failure
case conditionFailure:
@ -95,13 +88,8 @@ func processOverlay(rule kyverno.Rule, resource unstructured.Unstructured) (resp
func processOverlayPatches(resource, overlay interface{}) ([][]byte, overlayError) {
if path, overlayerr := meetConditions(resource, overlay); !reflect.DeepEqual(overlayerr, overlayError{}) {
switch overlayerr.statusCode {
// anchor key does not exist in the resource, skip applying policy
case conditionNotPresent:
glog.V(4).Infof("Mutate rule: policy not applied: %v at %s", overlayerr, path)
return nil, newOverlayError(overlayerr.statusCode, fmt.Sprintf("policy not applied: %v at %s", overlayerr.ErrorMsg(), path))
// anchor key is not satisfied in the resource, skip applying policy
case conditionFailure:
if overlayerr.statusCode == conditionFailure {
// anchor key is not satisfied in the resource, skip applying policy
glog.V(4).Infof("Mutate rule: failed to validate condition at %s, err: %v", path, overlayerr)
return nil, newOverlayError(overlayerr.statusCode, fmt.Sprintf("Conditions are not met at %s, %v", path, overlayerr))
}

View file

@ -98,7 +98,7 @@ func validateConditionAnchorMap(resourceMap, anchors map[string]interface{}, pat
}
} else {
// noAnchorKey doesn't exist in resource
return curPath, newOverlayError(conditionNotPresent, fmt.Sprintf("resource field is not present %s", noAnchorKey))
continue
}
}
return "", overlayError{}

View file

@ -6,7 +6,6 @@ type codeKey int
const (
conditionFailure codeKey = iota
conditionNotPresent
overlayFailure
)

View file

@ -0,0 +1,26 @@
package engine
import (
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// PolicyContext contains the contexts for engine to process
type PolicyContext struct {
// policy to be processed
Policy kyverno.ClusterPolicy
// resource to be processed
Resource unstructured.Unstructured
AdmissionInfo RequestInfo
}
// RequestInfo contains permission info carried in an admission request
type RequestInfo struct {
// Roles is a list of possible role send the request
Roles []string
// ClusterRoles is a list of possible clusterRoles send the request
ClusterRoles []string
// UserInfo is the userInfo carried in the admission request
AdmissionUserInfo authenticationv1.UserInfo
}

View file

@ -0,0 +1,116 @@
package engine
import (
"reflect"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1"
utils "github.com/nirmata/kyverno/pkg/utils"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
)
const (
SaPrefix = "system:serviceaccount:"
)
// matchAdmissionInfo return true if the rule can be applied to the request
func matchAdmissionInfo(rule kyverno.Rule, requestInfo RequestInfo) bool {
// when processing existing resource, it does not contain requestInfo
// skip permission checking
if reflect.DeepEqual(requestInfo, RequestInfo{}) {
return true
}
if !validateMatch(rule.MatchResources, requestInfo) {
return false
}
return validateExclude(rule.ExcludeResources, requestInfo)
}
// match:
// roles: role1, role2
// clusterRoles: clusterRole1,clusterRole2
// subjects: subject1, subject2
// validateMatch return true if (role1 || role2) and (clusterRole1 || clusterRole2)
// and (subject1 || subject2) are found in requestInfo, OR operation for each list
func validateMatch(match kyverno.MatchResources, requestInfo RequestInfo) bool {
if len(match.Roles) > 0 {
if !matchRoleRefs(match.Roles, requestInfo.Roles) {
return false
}
}
if len(match.ClusterRoles) > 0 {
if !matchRoleRefs(match.ClusterRoles, requestInfo.ClusterRoles) {
return false
}
}
if len(match.Subjects) > 0 {
if !matchSubjects(match.Subjects, requestInfo.AdmissionUserInfo) {
return false
}
}
return true
}
// exclude:
// roles: role1, role2
// clusterRoles: clusterRole1,clusterRole2
// subjects: subject1, subject2
// validateExclude return true if none of the above found in requestInfo
// otherwise return false immediately means rule should not be applied
func validateExclude(exclude kyverno.ExcludeResources, requestInfo RequestInfo) bool {
if len(exclude.Roles) > 0 {
if matchRoleRefs(exclude.Roles, requestInfo.Roles) {
return false
}
}
if len(exclude.ClusterRoles) > 0 {
if matchRoleRefs(exclude.ClusterRoles, requestInfo.ClusterRoles) {
return false
}
}
if len(exclude.Subjects) > 0 {
if matchSubjects(exclude.Subjects, requestInfo.AdmissionUserInfo) {
return false
}
}
return true
}
// matchRoleRefs return true if one of ruleRoleRefs exist in resourceRoleRefs
func matchRoleRefs(ruleRoleRefs, resourceRoleRefs []string) bool {
for _, ruleRoleRef := range ruleRoleRefs {
if utils.ContainsString(resourceRoleRefs, ruleRoleRef) {
return true
}
}
return false
}
// matchSubjects return true if one of ruleSubjects exist in userInfo
func matchSubjects(ruleSubjects []rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
userGroups := append(userInfo.Groups, userInfo.Username)
for _, subject := range ruleSubjects {
switch subject.Kind {
case "ServiceAccount":
if len(userInfo.Username) <= len(SaPrefix) {
continue
}
subjectServiceAccount := subject.Namespace + ":" + subject.Name
if userInfo.Username[len(SaPrefix):] == subjectServiceAccount {
return true
}
case "User", "Group":
if utils.ContainsString(userGroups, subject.Name) {
return true
}
}
}
return false
}

View file

@ -0,0 +1,287 @@
package engine
import (
"flag"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
)
func Test_matchAdmissionInfo(t *testing.T) {
flag.Parse()
flag.Set("logtostderr", "true")
flag.Set("v", "3")
tests := []struct {
rule kyverno.Rule
info RequestInfo
expected bool
}{
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{},
},
info: RequestInfo{},
expected: true,
},
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
Roles: []string{"ns-a:role-a"},
},
},
info: RequestInfo{
Roles: []string{"ns-a:role-a"},
},
expected: true,
},
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
Roles: []string{"ns-a:role-a"},
},
},
info: RequestInfo{
Roles: []string{"ns-a:role"},
},
expected: false,
},
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
Subjects: testSubjects(),
},
},
info: RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "serviceaccount:mynamespace:mysa",
},
},
expected: false,
},
{
rule: kyverno.Rule{
ExcludeResources: kyverno.ExcludeResources{
Subjects: testSubjects(),
},
},
info: RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
UID: "1",
},
},
expected: true,
},
{
rule: kyverno.Rule{
ExcludeResources: kyverno.ExcludeResources{
Subjects: testSubjects(),
},
},
info: RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
},
},
expected: false,
},
}
for _, test := range tests {
assert.Assert(t, test.expected == matchAdmissionInfo(test.rule, test.info))
}
}
func Test_validateMatch(t *testing.T) {
requestInfo := []struct {
info RequestInfo
expected bool
}{
{
info: RequestInfo{
Roles: []string{},
},
expected: false,
},
{
info: RequestInfo{
Roles: []string{"ns-b:role-b"},
},
expected: true,
},
{
info: RequestInfo{
Roles: []string{"ns:role"},
},
expected: false,
},
}
matchRoles := kyverno.MatchResources{
Roles: []string{"ns-a:role-a", "ns-b:role-b"},
}
for _, info := range requestInfo {
assert.Assert(t, info.expected == validateMatch(matchRoles, info.info))
}
requestInfo = []struct {
info RequestInfo
expected bool
}{
{
info: RequestInfo{
ClusterRoles: []string{},
},
expected: false,
},
{
info: RequestInfo{
ClusterRoles: []string{"role-b"},
},
expected: false,
},
{
info: RequestInfo{
ClusterRoles: []string{"clusterrole-b"},
},
expected: true,
},
{
info: RequestInfo{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
},
expected: true,
},
{
info: RequestInfo{
ClusterRoles: []string{"fake-a", "fake-b"},
},
expected: false,
},
}
matchClusterRoles := kyverno.MatchResources{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
}
for _, info := range requestInfo {
assert.Assert(t, info.expected == validateMatch(matchClusterRoles, info.info))
}
}
func Test_validateExclude(t *testing.T) {
requestInfo := []struct {
info RequestInfo
expected bool
}{
{
info: RequestInfo{
Roles: []string{},
},
expected: true,
},
{
info: RequestInfo{
Roles: []string{"ns-b:role-b"},
},
expected: false,
},
{
info: RequestInfo{
Roles: []string{"ns:role"},
},
expected: true,
},
}
excludeRoles := kyverno.ExcludeResources{
Roles: []string{"ns-a:role-a", "ns-b:role-b"},
}
for _, info := range requestInfo {
assert.Assert(t, info.expected == validateExclude(excludeRoles, info.info))
}
requestInfo = []struct {
info RequestInfo
expected bool
}{
{
info: RequestInfo{
ClusterRoles: []string{},
},
expected: true,
},
{
info: RequestInfo{
ClusterRoles: []string{"role-b"},
},
expected: true,
},
{
info: RequestInfo{
ClusterRoles: []string{"clusterrole-b"},
},
expected: false,
},
{
info: RequestInfo{
ClusterRoles: []string{"fake-a", "fake-b"},
},
expected: true,
},
}
excludeClusterRoles := kyverno.ExcludeResources{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
}
for _, info := range requestInfo {
assert.Assert(t, info.expected == validateExclude(excludeClusterRoles, info.info))
}
}
func Test_matchSubjects(t *testing.T) {
group := authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
}
sa := authenticationv1.UserInfo{
Username: "system:serviceaccount:mynamespace:mysa",
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:mynamespace", "system:authenticated"},
}
user := authenticationv1.UserInfo{
Username: "system:kube-scheduler",
Groups: []string{"system:authenticated"},
}
subjects := testSubjects()
assert.Assert(t, matchSubjects(subjects, sa))
assert.Assert(t, !matchSubjects(subjects, user))
assert.Assert(t, matchSubjects(subjects, group))
}
func testSubjects() []rbacv1.Subject {
return []rbacv1.Subject{
{
Kind: "User",
Name: "kube-scheduler",
},
{
Kind: "Group",
Name: "system:masters",
},
{
Kind: "ServiceAccount",
Name: "mysa",
Namespace: "mynamespace",
},
}
}

View file

@ -16,8 +16,11 @@ import (
)
//Validate applies validation rules from policy on the resource
func Validate(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) (response EngineResponse) {
func Validate(policyContext PolicyContext) (response EngineResponse) {
startTime := time.Now()
policy := policyContext.Policy
resource := policyContext.Resource
// policy information
func() {
// set policy information
@ -45,6 +48,15 @@ func Validate(policy kyverno.ClusterPolicy, resource unstructured.Unstructured)
if !rule.HasValidate() {
continue
}
startTime := time.Now()
if !matchAdmissionInfo(rule, policyContext.AdmissionInfo) {
glog.V(3).Infof("rule '%s' cannot be applied on %s/%s/%s, admission permission: %v",
rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), policyContext.AdmissionInfo)
continue
}
glog.V(4).Infof("Time: Validate matchAdmissionInfo %v", time.Since(startTime))
// check if the resource satisfies the filter conditions defined in the rule
// TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that
// dont statisfy a policy rule resource description

View file

@ -1628,7 +1628,8 @@ func TestValidate_ServiceTest(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
assert.Assert(t, len(er.PolicyResponse.Rules) == 0)
}
@ -1725,7 +1726,7 @@ func TestValidate_MapHasFloats(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
assert.Assert(t, len(er.PolicyResponse.Rules) == 0)
}
@ -1820,7 +1821,7 @@ func TestValidate_image_tag_fail(t *testing.T) {
"Validation rule 'validate-tag' succeeded.",
"Validation error: imagePullPolicy 'Always' required with tag 'latest'\nValidation rule 'validate-latest' failed at path '/spec/containers/0/imagePullPolicy/'.",
}
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
}
@ -1918,7 +1919,7 @@ func TestValidate_image_tag_pass(t *testing.T) {
"Validation rule 'validate-tag' succeeded.",
"Validation rule 'validate-latest' succeeded.",
}
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
}
@ -1991,7 +1992,7 @@ func TestValidate_Fail_anyPattern(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation error: A namespace is required\nValidation rule check-default-namespace anyPattern[0] failed at path /metadata/namespace/.\nValidation rule check-default-namespace anyPattern[1] failed at path /metadata/namespace/."}
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
@ -2072,7 +2073,7 @@ func TestValidate_host_network_port(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation error: Host network and port are not allowed\nValidation rule 'validate-host-network-port' failed at path '/spec/containers/0/ports/0/hostPort/'."}
for index, r := range er.PolicyResponse.Rules {
@ -2161,7 +2162,7 @@ func TestValidate_anchor_arraymap_pass(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation rule 'validate-host-path' succeeded."}
for index, r := range er.PolicyResponse.Rules {
@ -2249,7 +2250,7 @@ func TestValidate_anchor_arraymap_fail(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation error: Host path '/var/lib/' is not allowed\nValidation rule 'validate-host-path' failed at path '/spec/volumes/0/hostPath/path/'."}
for index, r := range er.PolicyResponse.Rules {
@ -2318,7 +2319,7 @@ func TestValidate_anchor_map_notfound(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation rule 'pod rule 2' succeeded."}
for index, r := range er.PolicyResponse.Rules {
@ -2390,7 +2391,7 @@ func TestValidate_anchor_map_found_valid(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation rule 'pod rule 2' succeeded."}
for index, r := range er.PolicyResponse.Rules {
@ -2462,7 +2463,7 @@ func TestValidate_anchor_map_found_invalid(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation error: pod: validate run as non root user\nValidation rule 'pod rule 2' failed at path '/spec/securityContext/runAsNonRoot/'."}
for index, r := range er.PolicyResponse.Rules {
@ -2536,7 +2537,7 @@ func TestValidate_AnchorList_pass(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation rule 'pod image rule' succeeded."}
for index, r := range er.PolicyResponse.Rules {
@ -2610,7 +2611,7 @@ func TestValidate_AnchorList_fail(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
// msgs := []string{"Validation rule 'pod image rule' failed at '/spec/containers/1/name/' for resource Pod//myapp-pod."}
// for index, r := range er.PolicyResponse.Rules {
// // t.Log(r.Message)
@ -2684,7 +2685,7 @@ func TestValidate_existenceAnchor_fail(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
// msgs := []string{"Validation rule 'pod image rule' failed at '/spec/containers/' for resource Pod//myapp-pod."}
// for index, r := range er.PolicyResponse.Rules {
@ -2759,7 +2760,7 @@ func TestValidate_existenceAnchor_pass(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation rule 'pod image rule' succeeded."}
for index, r := range er.PolicyResponse.Rules {
@ -2846,7 +2847,7 @@ func TestValidate_negationAnchor_deny(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation error: Host path is not allowed\nValidation rule 'validate-host-path' failed at path '/spec/volumes/0/hostPath/'."}
for index, r := range er.PolicyResponse.Rules {
@ -2932,7 +2933,7 @@ func TestValidate_negationAnchor_pass(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(policy, *resourceUnstructured)
er := Validate(PolicyContext{Policy: policy, Resource: *resourceUnstructured})
msgs := []string{"Validation rule 'validate-host-path' succeeded."}
for index, r := range er.PolicyResponse.Rules {

View file

@ -105,7 +105,7 @@ func applyPolicyOnRaw(policy *kyverno.ClusterPolicy, rawResource []byte, gvk *me
}
//TODO check if the kind information is present resource
// Process Mutation
engineResponse := engine.Mutate(*policy, *resource)
engineResponse := engine.Mutate(engine.PolicyContext{Policy: *policy, Resource: *resource})
if !engineResponse.IsSuccesful() {
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
for _, r := range engineResponse.PolicyResponse.Rules {
@ -115,7 +115,7 @@ func applyPolicyOnRaw(policy *kyverno.ClusterPolicy, rawResource []byte, gvk *me
glog.Infof("Mutation from policy %s has applied succesfully to %s %s/%s", policy.Name, gvk.Kind, rname, rns)
// Process Validation
engineResponse := engine.Validate(*policy, *resource)
engineResponse := engine.Validate(engine.PolicyContext{Policy: *policy, Resource: *resource})
if !engineResponse.IsSuccesful() {
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)

View file

@ -41,6 +41,10 @@ func (nsl *NamespaceLister) ListResources(selector labels.Selector) (ret []*v1.N
//GetResource is a wrapper to get the resource and inject the GVK
func (nsl *NamespaceLister) GetResource(name string) (*v1.Namespace, error) {
namespace, err := nsl.Get(name)
if err != nil {
return nil, err
}
namespace.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Namespace"))
return namespace, err
}

View file

@ -69,7 +69,7 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure
sendStat(false)
//VALIDATION
engineResponse = engine.Validate(policy, resource)
engineResponse = engine.Validate(engine.PolicyContext{Policy: policy, Resource: resource})
engineResponses = append(engineResponses, engineResponse)
// gather stats
gatherStat(policy.Name, engineResponse.PolicyResponse)
@ -80,7 +80,7 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure
return engineResponses
}
func mutation(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, policyStatus PolicyStatusInterface) (engine.EngineResponse, error) {
engineResponse := engine.Mutate(policy, resource)
engineResponse := engine.Mutate(engine.PolicyContext{Policy: policy, Resource: resource})
if !engineResponse.IsSuccesful() {
glog.V(4).Infof("mutation had errors reporting them")
return engineResponse, nil

View file

@ -148,7 +148,7 @@ func runTestCase(t *testing.T, tc scaseT) bool {
var er engine.EngineResponse
er = engine.Mutate(*policy, *resource)
er = engine.Mutate(engine.PolicyContext{Policy: *policy, Resource: *resource})
t.Log("---Mutation---")
validateResource(t, er.PatchedResource, tc.Expected.Mutation.PatchedResource)
validateResponse(t, er.PolicyResponse, tc.Expected.Mutation.PolicyResponse)
@ -158,7 +158,7 @@ func runTestCase(t *testing.T, tc scaseT) bool {
resource = &er.PatchedResource
}
er = engine.Validate(*policy, *resource)
er = engine.Validate(engine.PolicyContext{Policy: *policy, Resource: *resource})
t.Log("---Validation---")
validateResponse(t, er.PolicyResponse, tc.Expected.Validation.PolicyResponse)

124
pkg/userinfo/roleRef.go Normal file
View file

@ -0,0 +1,124 @@
package userinfo
import (
"fmt"
"strings"
"github.com/golang/glog"
engine "github.com/nirmata/kyverno/pkg/engine"
v1beta1 "k8s.io/api/admission/v1beta1"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
labels "k8s.io/apimachinery/pkg/labels"
rbaclister "k8s.io/client-go/listers/rbac/v1"
)
func GetRoleRef(rbLister rbaclister.RoleBindingLister, crbLister rbaclister.ClusterRoleBindingLister, request *v1beta1.AdmissionRequest) (roles []string, clusterRoles []string, err error) {
// rolebindings
roleBindings, err := rbLister.List(labels.NewSelector())
if err != nil {
return roles, clusterRoles, fmt.Errorf("failed to list rolebindings: %v", err)
}
rs, crs, err := getRoleRefByRoleBindings(roleBindings, request.UserInfo)
if err != nil {
return roles, clusterRoles, err
}
roles = append(roles, rs...)
clusterRoles = append(clusterRoles, crs...)
// clusterrolebindings
clusterroleBindings, err := crbLister.List(labels.NewSelector())
if err != nil {
return roles, clusterRoles, fmt.Errorf("failed to list clusterrolebindings: %v", err)
}
crs, err = getRoleRefByClusterRoleBindings(clusterroleBindings, request.UserInfo)
if err != nil {
return roles, clusterRoles, err
}
clusterRoles = append(clusterRoles, crs...)
return roles, clusterRoles, nil
}
func getRoleRefByRoleBindings(roleBindings []*rbacv1.RoleBinding, userInfo authenticationv1.UserInfo) (roles []string, clusterRoles []string, err error) {
for _, rolebinding := range roleBindings {
for _, subject := range rolebinding.Subjects {
if !matchSubjectsMap(subject, userInfo) {
continue
}
// roleRefMap := roleRef.(map[string]interface{})
switch rolebinding.RoleRef.Kind {
case "role":
roles = append(roles, rolebinding.Namespace+":"+rolebinding.RoleRef.Name)
case "clusterRole":
clusterRoles = append(clusterRoles, rolebinding.RoleRef.Name)
}
}
}
return roles, clusterRoles, nil
}
// RoleRef in ClusterRoleBindings can only reference a ClusterRole in the global namespace
func getRoleRefByClusterRoleBindings(clusterroleBindings []*rbacv1.ClusterRoleBinding, userInfo authenticationv1.UserInfo) (clusterRoles []string, err error) {
for _, clusterRoleBinding := range clusterroleBindings {
for _, subject := range clusterRoleBinding.Subjects {
if !matchSubjectsMap(subject, userInfo) {
continue
}
if clusterRoleBinding.RoleRef.Kind == "clusterRole" {
clusterRoles = append(clusterRoles, clusterRoleBinding.RoleRef.Name)
}
}
}
return clusterRoles, nil
}
// matchSubjectsMap checks if userInfo found in subject
// return true directly if found a match
// subject["kind"] can only be ServiceAccount, User and Group
func matchSubjectsMap(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
// ServiceAccount
if isServiceaccountUserInfo(userInfo.Username) {
return matchServiceAccount(subject, userInfo)
}
// User or Group
return matchUserOrGroup(subject, userInfo)
}
func isServiceaccountUserInfo(username string) bool {
if strings.Contains(username, engine.SaPrefix) {
return true
}
return false
}
// matchServiceAccount checks if userInfo sa matche the subject sa
// serviceaccount represents as saPrefix:namespace:name in userInfo
func matchServiceAccount(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
subjectServiceAccount := subject.Namespace + ":" + subject.Name
if userInfo.Username[len(engine.SaPrefix):] != subjectServiceAccount {
glog.V(3).Infof("service account not match, expect %s, got %s", subjectServiceAccount, userInfo.Username[len(engine.SaPrefix):])
return false
}
return true
}
// matchUserOrGroup checks if userInfo contains user or group info in a subject
func matchUserOrGroup(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
keys := append(userInfo.Groups, userInfo.Username)
for _, key := range keys {
if subject.Name == key {
return true
}
}
glog.V(3).Infof("user/group '%v' info not found in request userInfo: %v", subject.Name, keys)
return false
}

View file

@ -0,0 +1,275 @@
package userinfo
import (
"flag"
"reflect"
"testing"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func Test_isServiceaccountUserInfo(t *testing.T) {
tests := []struct {
username string
expected bool
}{
{
username: "system:serviceaccount:default:saconfig",
expected: true,
},
{
username: "serviceaccount:default:saconfig",
expected: false,
},
}
for _, test := range tests {
res := isServiceaccountUserInfo(test.username)
assert.Assert(t, test.expected == res)
}
}
func Test_matchServiceAccount_subject_variants(t *testing.T) {
userInfo := authenticationv1.UserInfo{
Username: "system:serviceaccount:default:saconfig",
}
tests := []struct {
subject rbacv1.Subject
expected bool
}{
{
subject: rbacv1.Subject{},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "serviceaccount",
},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "testnamespace",
},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "1",
},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "testnamespace",
Name: "",
},
expected: false,
},
{
subject: rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "testnamespace",
Name: "testname",
},
expected: false,
},
}
for _, test := range tests {
res := matchServiceAccount(test.subject, userInfo)
assert.Assert(t, test.expected == res)
}
}
func Test_matchUserOrGroup(t *testing.T) {
group := authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
}
sa := authenticationv1.UserInfo{
Username: "system:serviceaccount:kube-system:deployment-controller",
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:kube-system", "system:authenticated"},
}
user := authenticationv1.UserInfo{
Username: "system:kube-scheduler",
Groups: []string{"system:authenticated"},
}
userContext := rbacv1.Subject{
Kind: "User",
Name: "system:kube-scheduler",
}
groupContext := rbacv1.Subject{
Kind: "Group",
Name: "system:masters",
}
fakegroupContext := rbacv1.Subject{
Kind: "Group",
Name: "fakeGroup",
}
res := matchUserOrGroup(userContext, user)
assert.Assert(t, res)
res = matchUserOrGroup(groupContext, group)
assert.Assert(t, res)
res = matchUserOrGroup(groupContext, sa)
assert.Assert(t, !res)
res = matchUserOrGroup(fakegroupContext, group)
assert.Assert(t, !res)
}
func Test_matchSubjectsMap(t *testing.T) {
sa := authenticationv1.UserInfo{
Username: "system:serviceaccount:default:saconfig",
}
group := authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
}
sasubject := rbacv1.Subject{
Kind: "ServiceAccount",
Namespace: "default",
Name: "saconfig",
}
groupsubject := rbacv1.Subject{
Kind: "Group",
Name: "fakeGroup",
}
res := matchSubjectsMap(sasubject, sa)
assert.Assert(t, res)
res = matchSubjectsMap(groupsubject, group)
assert.Assert(t, !res)
}
func Test_getRoleRefByRoleBindings(t *testing.T) {
flag.Parse()
flag.Set("logtostderr", "true")
flag.Set("v", "3")
list := []*rbacv1.RoleBinding{
&rbacv1.RoleBinding{
metav1.TypeMeta{
Kind: "RoleBinding",
APIVersion: "rbac.authorization.k8s.io/v1",
},
metav1.ObjectMeta{Name: "test1", Namespace: "mynamespace"},
[]rbacv1.Subject{
rbacv1.Subject{
Kind: "ServiceAccount",
Name: "saconfig",
Namespace: "default",
},
},
rbacv1.RoleRef{
Kind: "role",
Name: "myrole",
},
},
&rbacv1.RoleBinding{
metav1.TypeMeta{
Kind: "RoleBinding",
APIVersion: "rbac.authorization.k8s.io/v1",
},
metav1.ObjectMeta{Name: "test2", Namespace: "mynamespace"},
[]rbacv1.Subject{
rbacv1.Subject{
Kind: "ServiceAccount",
Name: "saconfig",
Namespace: "default",
},
},
rbacv1.RoleRef{
Kind: "clusterRole",
Name: "myclusterrole",
},
},
}
sa := authenticationv1.UserInfo{
Username: "system:serviceaccount:default:saconfig",
}
expectedrole := []string{"mynamespace:myrole"}
expectedClusterRole := []string{"myclusterrole"}
roles, clusterroles, err := getRoleRefByRoleBindings(list, sa)
assert.Assert(t, err == nil)
assert.Assert(t, reflect.DeepEqual(roles, expectedrole))
assert.Assert(t, reflect.DeepEqual(clusterroles, expectedClusterRole))
}
func Test_getRoleRefByClusterRoleBindings(t *testing.T) {
list := []*rbacv1.ClusterRoleBinding{
&rbacv1.ClusterRoleBinding{
metav1.TypeMeta{
Kind: "ClusterRoleBinding",
APIVersion: "rbac.authorization.k8s.io/v1",
},
metav1.ObjectMeta{Name: "test1", Namespace: "mynamespace"},
[]rbacv1.Subject{
rbacv1.Subject{
Kind: "User",
Name: "kube-scheduler",
},
},
rbacv1.RoleRef{
Kind: "clusterRole",
Name: "fakeclusterrole",
},
},
&rbacv1.ClusterRoleBinding{
metav1.TypeMeta{
Kind: "ClusterRoleBinding",
APIVersion: "rbac.authorization.k8s.io/v1",
},
metav1.ObjectMeta{Name: "test2", Namespace: "mynamespace"},
[]rbacv1.Subject{
rbacv1.Subject{
Kind: "Group",
Name: "system:masters",
},
},
rbacv1.RoleRef{
Kind: "clusterRole",
Name: "myclusterrole",
},
},
}
group := authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
}
user := authenticationv1.UserInfo{
Username: "system:kube-scheduler",
Groups: []string{"system:authenticated"},
}
clusterroles, err := getRoleRefByClusterRoleBindings(list, group)
assert.Assert(t, err == nil)
assert.Assert(t, reflect.DeepEqual(clusterroles, []string{"myclusterrole"}))
clusterroles, err = getRoleRefByClusterRoleBindings(list, user)
assert.Assert(t, err == nil)
assert.Assert(t, len(clusterroles) == 0)
}

View file

@ -9,7 +9,7 @@ import (
)
const (
policyAnnotation = "policies.kyverno.io/patches"
policyAnnotation = "policies.kyverno.patches"
)
type policyPatch struct {
@ -29,7 +29,12 @@ type response struct {
Value interface{} `json:"value"`
}
func generateAnnotationPatches(annotations map[string]string, engineResponses []engine.EngineResponse) []byte {
func generateAnnotationPatches(engineResponses []engine.EngineResponse) []byte {
var annotations map[string]string
if len(engineResponses) > 0 {
annotations = engineResponses[0].PatchedResource.GetAnnotations()
}
if annotations == nil {
annotations = make(map[string]string)
}
@ -41,15 +46,30 @@ func generateAnnotationPatches(annotations map[string]string, engineResponses []
return nil
}
// Kyverno uses jsonpath to patch obejct
// since policyAnnotation=policies.kyverno.io/patches contains "/"
// the operation here should always be "add"
// otherwise the key when patching will be "patches" which is missing
annotations[policyAnnotation] = string(value)
patchResponse = response{
Op: "add",
Path: "/metadata/annotations",
Value: annotations,
if _, ok := annotations[policyAnnotation]; ok {
// create update patch string
patchResponse = response{
Op: "replace",
Path: "/metadata/annotations/" + policyAnnotation,
Value: string(value),
}
} else {
// mutate rule has annotation patches
if len(annotations) > 0 {
patchResponse = response{
Op: "add",
Path: "/metadata/annotations/" + policyAnnotation,
Value: string(value),
}
} else {
// insert 'policies.kyverno.patches' entry in annotation map
annotations[policyAnnotation] = string(value)
patchResponse = response{
Op: "add",
Path: "/metadata/annotations",
Value: annotations,
}
}
}
patchByte, _ := json.Marshal(patchResponse)

View file

@ -5,6 +5,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine"
"gotest.tools/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func newPolicyResponse(policy, rule string, patchesStr []string, success bool) engine.PolicyResponse {
@ -24,18 +25,25 @@ func newPolicyResponse(policy, rule string, patchesStr []string, success bool) e
}
}
func newEngineResponse(policy, rule string, patchesStr []string, success bool) engine.EngineResponse {
func newEngineResponse(policy, rule string, patchesStr []string, success bool, annotation map[string]string) engine.EngineResponse {
return engine.EngineResponse{
PatchedResource: unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"annotation": annotation,
},
},
},
PolicyResponse: newPolicyResponse(policy, rule, patchesStr, success),
}
}
func Test_empty_annotation(t *testing.T) {
patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }`
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true)
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, nil)
annPatches := generateAnnotationPatches(nil, []engine.EngineResponse{engineResponse})
expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.io/patches":"[{\"policyname\":\"mutate-container\",\"patches\":[{\"rulename\":\"default-imagepullpolicy\",\"op\":\"replace\",\"path\":\"/spec/containers/0/imagePullPolicy\"}]}]"}}`
annPatches := generateAnnotationPatches([]engine.EngineResponse{engineResponse})
expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.patches":"[{\"policyname\":\"mutate-container\",\"patches\":[{\"rulename\":\"default-imagepullpolicy\",\"op\":\"replace\",\"path\":\"/spec/containers/0/imagePullPolicy\"}]}]"}}`
assert.Assert(t, string(annPatches) == expectedPatches)
}
@ -45,48 +53,47 @@ func Test_exist_annotation(t *testing.T) {
}
patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }`
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true)
annPatches := generateAnnotationPatches(annotation, []engine.EngineResponse{engineResponse})
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, annotation)
annPatches := generateAnnotationPatches([]engine.EngineResponse{engineResponse})
expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.io/patches":"[{\"policyname\":\"mutate-container\",\"patches\":[{\"rulename\":\"default-imagepullpolicy\",\"op\":\"replace\",\"path\":\"/spec/containers/0/imagePullPolicy\"}]}]","test":"annotation"}}`
expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.patches":"[{\"policyname\":\"mutate-container\",\"patches\":[{\"rulename\":\"default-imagepullpolicy\",\"op\":\"replace\",\"path\":\"/spec/containers/0/imagePullPolicy\"}]}]"}}`
assert.Assert(t, string(annPatches) == expectedPatches)
}
func Test_exist_kyverno_annotation(t *testing.T) {
annotation := map[string]string{
"policies.kyverno.io/patches": "old-annotation",
"policies.kyverno.patches": "old-annotation",
}
patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }`
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true)
annPatches := generateAnnotationPatches(annotation, []engine.EngineResponse{engineResponse})
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, annotation)
annPatches := generateAnnotationPatches([]engine.EngineResponse{engineResponse})
expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.io/patches":"[{\"policyname\":\"mutate-container\",\"patches\":[{\"rulename\":\"default-imagepullpolicy\",\"op\":\"replace\",\"path\":\"/spec/containers/0/imagePullPolicy\"}]}]"}}`
expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.patches":"[{\"policyname\":\"mutate-container\",\"patches\":[{\"rulename\":\"default-imagepullpolicy\",\"op\":\"replace\",\"path\":\"/spec/containers/0/imagePullPolicy\"}]}]"}}`
assert.Assert(t, string(annPatches) == expectedPatches)
}
func Test_annotation_nil_patch(t *testing.T) {
annotation := map[string]string{
"policies.kyverno.io/patches": "old-annotation",
"policies.kyverno.patches": "old-annotation",
}
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, true)
annPatches := generateAnnotationPatches(annotation, []engine.EngineResponse{engineResponse})
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, true, annotation)
annPatches := generateAnnotationPatches([]engine.EngineResponse{engineResponse})
assert.Assert(t, annPatches == nil)
engineResponseNew := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{""}, true)
annPatchesNew := generateAnnotationPatches(annotation, []engine.EngineResponse{engineResponseNew})
engineResponseNew := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{""}, true, annotation)
annPatchesNew := generateAnnotationPatches([]engine.EngineResponse{engineResponseNew})
assert.Assert(t, annPatchesNew == nil)
}
func Test_annotation_failed_Patch(t *testing.T) {
annotation := map[string]string{
"policies.kyverno.io/patches": "old-annotation",
"policies.kyverno.patches": "old-annotation",
}
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, false)
annPatches := generateAnnotationPatches(annotation, []engine.EngineResponse{engineResponse})
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, false, annotation)
annPatches := generateAnnotationPatches([]engine.EngineResponse{engineResponse})
assert.Assert(t, annPatches == nil)
}

View file

@ -2,16 +2,17 @@ package webhooks
import (
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1"
engine "github.com/nirmata/kyverno/pkg/engine"
policyctr "github.com/nirmata/kyverno/pkg/policy"
"github.com/nirmata/kyverno/pkg/utils"
v1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// HandleMutation handles mutating webhook admission request
func (ws *WebhookServer) handleMutation(request *v1beta1.AdmissionRequest) (bool, []byte, string) {
func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest,
policies []*v1alpha1.ClusterPolicy, roles, clusterRoles []string) (bool, []byte, string) {
glog.V(4).Infof("Receive request in mutating webhook: Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation)
@ -61,18 +62,18 @@ func (ws *WebhookServer) handleMutation(request *v1beta1.AdmissionRequest) (bool
//TODO: check if the name and namespace is also passed right in the resource?
// if not then set it from the api request
resource.SetGroupVersionKind(schema.GroupVersionKind{Group: request.Kind.Group, Version: request.Kind.Version, Kind: request.Kind.Kind})
policies, err := ws.pLister.List(labels.NewSelector())
if err != nil {
//TODO check if the CRD is created ?
// Unable to connect to policy Lister to access policies
glog.Errorln("Unable to connect to policy controller to access policies. Mutation Rules are NOT being applied")
glog.Warning(err)
return true, nil, ""
}
var engineResponses []engine.EngineResponse
for _, policy := range policies {
policyContext := engine.PolicyContext{
Resource: *resource,
AdmissionInfo: engine.RequestInfo{
Roles: roles,
ClusterRoles: clusterRoles,
AdmissionUserInfo: request.UserInfo},
}
for _, policy := range policies {
policyContext.Policy = *policy
// check if policy has a rule for the admission request kind
if !utils.ContainsString(getApplicableKindsForPolicy(policy), request.Kind.Kind) {
continue
@ -81,7 +82,7 @@ func (ws *WebhookServer) handleMutation(request *v1beta1.AdmissionRequest) (bool
glog.V(2).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
resource.GetKind(), resource.GetNamespace(), resource.GetName(), request.UID, request.Operation)
// TODO: this can be
engineResponse := engine.Mutate(*policy, *resource)
engineResponse := engine.Mutate(policyContext)
engineResponses = append(engineResponses, engineResponse)
// Gather policy application statistics
gatherStat(policy.Name, engineResponse.PolicyResponse)
@ -98,7 +99,7 @@ func (ws *WebhookServer) handleMutation(request *v1beta1.AdmissionRequest) (bool
}
// generate annotations
if annPatches := generateAnnotationPatches(resource.GetAnnotations(), engineResponses); annPatches != nil {
if annPatches := generateAnnotationPatches(engineResponses); annPatches != nil {
patches = append(patches, annPatches)
}

View file

@ -20,9 +20,13 @@ import (
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policy"
tlsutils "github.com/nirmata/kyverno/pkg/tls"
userinfo "github.com/nirmata/kyverno/pkg/userinfo"
"github.com/nirmata/kyverno/pkg/webhookconfig"
v1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
rbacinformer "k8s.io/client-go/informers/rbac/v1"
rbaclister "k8s.io/client-go/listers/rbac/v1"
"k8s.io/client-go/tools/cache"
)
@ -36,6 +40,8 @@ type WebhookServer struct {
pvLister kyvernolister.ClusterPolicyViolationLister
pListerSynced cache.InformerSynced
pvListerSynced cache.InformerSynced
rbLister rbaclister.RoleBindingLister
crbLister rbaclister.ClusterRoleBindingLister
eventGen event.Interface
webhookRegistrationClient *webhookconfig.WebhookRegistrationClient
// API to send policy stats for aggregation
@ -56,6 +62,8 @@ func NewWebhookServer(
tlsPair *tlsutils.TlsPemPair,
pInformer kyvernoinformer.ClusterPolicyInformer,
pvInformer kyvernoinformer.ClusterPolicyViolationInformer,
rbInformer rbacinformer.RoleBindingInformer,
crbInformer rbacinformer.ClusterRoleBindingInformer,
eventGen event.Interface,
webhookRegistrationClient *webhookconfig.WebhookRegistrationClient,
policyStatus policy.PolicyStatusInterface,
@ -85,6 +93,8 @@ func NewWebhookServer(
webhookRegistrationClient: webhookRegistrationClient,
policyStatus: policyStatus,
configHandler: configHandler,
rbLister: rbInformer.Lister(),
crbLister: crbInformer.Lister(),
cleanUp: cleanUp,
lastReqTime: checker.NewLastReqTime(),
}
@ -154,8 +164,32 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
}
func (ws *WebhookServer) handleAdmissionRequest(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
// TODO: this will be replaced by policy store lookup
policies, err := ws.pLister.List(labels.NewSelector())
if err != nil {
//TODO check if the CRD is created ?
// Unable to connect to policy Lister to access policies
glog.Errorf("Unable to connect to policy controller to access policies. Policies are NOT being applied: %v", err)
return &v1beta1.AdmissionResponse{Allowed: true}
}
var roles, clusterRoles []string
// TODO(shuting): replace containRBACinfo after policy cache lookup is introduced
// getRoleRef only if policy has roles/clusterroles defined
startTime := time.Now()
if containRBACinfo(policies) {
roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request)
if err != nil {
// TODO(shuting): continue apply policy if error getting roleRef?
glog.Errorf("Unable to get rbac information for request Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s: %v",
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation, err)
}
}
glog.V(4).Infof("Time: webhook GetRoleRef %v", time.Since(startTime))
// MUTATION
ok, patches, msg := ws.handleMutation(request)
ok, patches, msg := ws.HandleMutation(request, policies, roles, clusterRoles)
if !ok {
glog.V(4).Infof("Deny admission request: %v/%s/%s", request.Kind, request.Namespace, request.Name)
return &v1beta1.AdmissionResponse{
@ -171,7 +205,7 @@ func (ws *WebhookServer) handleAdmissionRequest(request *v1beta1.AdmissionReques
patchedResource := processResourceWithPatches(patches, request.Object.Raw)
// VALIDATION
ok, msg = ws.handleValidation(request, patchedResource)
ok, msg = ws.HandleValidation(request, policies, patchedResource, roles, clusterRoles)
if !ok {
glog.V(4).Infof("Deny admission request: %v/%s/%s", request.Kind, request.Namespace, request.Name)
return &v1beta1.AdmissionResponse{

View file

@ -98,3 +98,14 @@ func processResourceWithPatches(patch []byte, resource []byte) []byte {
}
return resource
}
func containRBACinfo(policies []*kyverno.ClusterPolicy) bool {
for _, policy := range policies {
for _, rule := range policy.Spec.Rules {
if len(rule.MatchResources.Roles) > 0 || len(rule.MatchResources.ClusterRoles) > 0 {
return true
}
}
}
return false
}

View file

@ -2,19 +2,20 @@ package webhooks
import (
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1"
engine "github.com/nirmata/kyverno/pkg/engine"
policyctr "github.com/nirmata/kyverno/pkg/policy"
"github.com/nirmata/kyverno/pkg/policyviolation"
"github.com/nirmata/kyverno/pkg/utils"
v1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// handleValidation handles validating webhook admission request
// If there are no errors in validating rule we apply generation rules
// patchedResource is the (resource + patches) after applying mutation rules
func (ws *WebhookServer) handleValidation(request *v1beta1.AdmissionRequest, patchedResource []byte) (bool, string) {
func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest,
policies []*v1alpha1.ClusterPolicy, patchedResource []byte, roles, clusterRoles []string) (bool, string) {
glog.V(4).Infof("Receive request in validating webhook: Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation)
@ -71,18 +72,17 @@ func (ws *WebhookServer) handleValidation(request *v1beta1.AdmissionRequest, pat
// resource namespace is empty for the first CREATE operation
resource.SetNamespace(request.Namespace)
policies, err := ws.pLister.List(labels.NewSelector())
if err != nil {
//TODO check if the CRD is created ?
// Unable to connect to policy Lister to access policies
glog.Error("Unable to connect to policy controller to access policies. Validation Rules are NOT being applied")
glog.Warning(err)
return true, ""
policyContext := engine.PolicyContext{
Resource: *resource,
AdmissionInfo: engine.RequestInfo{
Roles: roles,
ClusterRoles: clusterRoles,
AdmissionUserInfo: request.UserInfo},
}
var engineResponses []engine.EngineResponse
for _, policy := range policies {
policyContext.Policy = *policy
if !utils.ContainsString(getApplicableKindsForPolicy(policy), request.Kind.Kind) {
continue
}
@ -90,7 +90,7 @@ func (ws *WebhookServer) handleValidation(request *v1beta1.AdmissionRequest, pat
glog.V(2).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
resource.GetKind(), resource.GetNamespace(), resource.GetName(), request.UID, request.Operation)
engineResponse := engine.Validate(*policy, *resource)
engineResponse := engine.Validate(policyContext)
engineResponses = append(engineResponses, engineResponse)
// Gather policy application statistics
gatherStat(policy.Name, engineResponse.PolicyResponse)

View file

@ -18,7 +18,7 @@ spec:
overlay:
metadata:
annotations:
+(cluster-autoscaler.kubernetes.io/safe-to-evict): true
+(cluster-autoscaler.kubernetes.io/safe-to-evict): "true"
spec:
volumes:
- (emptyDir): {}
@ -31,7 +31,7 @@ spec:
overlay:
metadata:
annotations:
+(cluster-autoscaler.kubernetes.io/safe-to-evict): true
+(cluster-autoscaler.kubernetes.io/safe-to-evict): "true"
spec:
volumes:
- (hostPath):

View file

@ -2,9 +2,8 @@ apiVersion: v1
kind: Pod
metadata:
name: pod-with-emptydir
metadata:
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: true
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
spec:
containers:
- image: k8s.gcr.io/test-webserver

View file

@ -3,7 +3,7 @@ kind: Pod
metadata:
name: pod-with-hostpath
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: true
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
spec:
containers:
- image: k8s.gcr.io/test-webserver

View file

@ -14,6 +14,10 @@ expected:
name: pod-with-emptydir
rules:
- name: annotate-empty-dir
type: Mutation
success: true
message: "successfully processed overlay"
- name: annotate-host-path
type: Mutation
success: true
message: "successfully processed overlay"

View file

@ -13,7 +13,11 @@ expected:
namespace: ''
name: pod-with-hostpath
rules:
- name: annotate-host-path
- name: annotate-empty-dir
type: Mutation
success: true
message: "successfully processed overlay"
- name: annotate-host-path
type: Mutation
success: true
message: "successfully processed overlay"