Skip to content

Commit e1b0bc3

Browse files
author
Alexander Zielenski
committed
bugfix: use matched resource for AdmissionRequest.resource, not the resource it was converted from
use existing admission request for audit annotation eval populate matchResource in empty rules case
1 parent 5e2e8c8 commit e1b0bc3

File tree

12 files changed

+117
-85
lines changed

12 files changed

+117
-85
lines changed

staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/google/cel-go/cel"
2525

26+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
"k8s.io/apiserver/pkg/admission"
2728
celconfig "k8s.io/apiserver/pkg/apis/cel"
2829
"k8s.io/apiserver/pkg/cel/environment"
@@ -141,7 +142,7 @@ func TestCompositedPolicies(t *testing.T) {
141142
if costBudget == 0 {
142143
costBudget = celconfig.RuntimeCELCostBudget
143144
}
144-
result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, nil, costBudget)
145+
result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, v1.GroupVersionResource(tc.attributes.GetResource()), v1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, nil, costBudget)
145146
if !tc.expectErr && err != nil {
146147
t.Fatalf("failed evaluation: %v", err)
147148
}

staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,13 @@ func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.Versione
253253
}
254254

255255
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
256-
func CreateAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionRequest {
257-
// FIXME: how to get resource GVK, GVR and subresource?
258-
gvk := attr.GetKind()
259-
gvr := attr.GetResource()
256+
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
257+
// Attempting to use same logic as webhook for constructing resource
258+
// GVK, GVR, subresource
259+
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
260+
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
261+
gvk := equivalentKind
262+
gvr := equivalentGVR
260263
subresource := attr.GetSubresource()
261264

262265
requestGVK := attr.GetKind()

staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ func TestFilter(t *testing.T) {
787787

788788
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
789789
ctx := context.TODO()
790-
evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, CreateNamespaceObject(tc.namespaceObject), celconfig.RuntimeCELCostBudget)
790+
evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, CreateNamespaceObject(tc.namespaceObject), celconfig.RuntimeCELCostBudget)
791791
if err != nil {
792792
t.Fatalf("unexpected error: %v", err)
793793
}
@@ -933,7 +933,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
933933
}
934934
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
935935
ctx := context.TODO()
936-
evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, nil, tc.testRuntimeCELCostBudget)
936+
evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, nil, tc.testRuntimeCELCostBudget)
937937
if tc.exceedBudget && err == nil {
938938
t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil")
939939
}

staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go

Lines changed: 25 additions & 25 deletions
Large diffs are not rendered by default.

staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func (c *celAdmissionController) Validate(
244244
var versionedAttr *admission.VersionedAttributes
245245

246246
definition := definitionInfo.lastReconciledValue
247-
matches, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition)
247+
matches, matchResource, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition)
248248
if err != nil {
249249
// Configuration error.
250250
addConfigError(err, definition, nil)
@@ -323,7 +323,7 @@ func (c *celAdmissionController) Validate(
323323
nested: param,
324324
}
325325
}
326-
validationResults = append(validationResults, bindingInfo.validator.Validate(ctx, versionedAttr, p, namespace, celconfig.RuntimeCELCostBudget, authz))
326+
validationResults = append(validationResults, bindingInfo.validator.Validate(ctx, matchResource, versionedAttr, p, namespace, celconfig.RuntimeCELCostBudget, authz))
327327
}
328328

329329
for _, validationResult := range validationResults {

staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ type Matcher interface {
8686

8787
// DefinitionMatches says whether this policy definition matches the provided admission
8888
// resource request
89-
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error)
89+
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error)
9090

9191
// BindingMatches says whether this policy definition matches the provided admission
9292
// resource request
@@ -109,5 +109,5 @@ type ValidateResult struct {
109109
type Validator interface {
110110
// Validate is used to take cel evaluations and convert into decisions
111111
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
112-
Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
112+
Validate(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
113113
}

staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matcher.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (c *matcher) ValidateInitialization() error {
6363
}
6464

6565
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
66-
func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) {
66+
func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
6767
criteria := matchCriteria{constraints: definition.Spec.MatchConstraints}
6868
return c.Matcher.Matches(a, o, &criteria)
6969
}
@@ -74,7 +74,7 @@ func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInter
7474
return true, nil
7575
}
7676
criteria := matchCriteria{constraints: binding.Spec.MatchResources}
77-
isMatch, _, err := c.Matcher.Matches(a, o, &criteria)
77+
isMatch, _, _, err := c.Matcher.Matches(a, o, &criteria)
7878
return isMatch, err
7979
}
8080

staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching/matching.go

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,56 +71,60 @@ func (m *Matcher) ValidateInitialization() error {
7171
return nil
7272
}
7373

74-
func (m *Matcher) Matches(attr admission.Attributes, o admission.ObjectInterfaces, criteria MatchCriteria) (bool, schema.GroupVersionKind, error) {
74+
func (m *Matcher) Matches(attr admission.Attributes, o admission.ObjectInterfaces, criteria MatchCriteria) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
7575
matches, matchNsErr := m.namespaceMatcher.MatchNamespaceSelector(criteria, attr)
7676
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
7777
if !matches && matchNsErr == nil {
78-
return false, schema.GroupVersionKind{}, nil
78+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
7979
}
8080

8181
matches, matchObjErr := m.objectMatcher.MatchObjectSelector(criteria, attr)
8282
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
8383
if !matches && matchObjErr == nil {
84-
return false, schema.GroupVersionKind{}, nil
84+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
8585
}
8686

8787
matchResources := criteria.GetMatchResources()
8888
matchPolicy := matchResources.MatchPolicy
89-
if isExcluded, _, err := matchesResourceRules(matchResources.ExcludeResourceRules, matchPolicy, attr, o); isExcluded || err != nil {
90-
return false, schema.GroupVersionKind{}, err
89+
if isExcluded, _, _, err := matchesResourceRules(matchResources.ExcludeResourceRules, matchPolicy, attr, o); isExcluded || err != nil {
90+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, err
9191
}
9292

9393
var (
94-
isMatch bool
95-
matchKind schema.GroupVersionKind
96-
matchErr error
94+
isMatch bool
95+
matchResource schema.GroupVersionResource
96+
matchKind schema.GroupVersionKind
97+
matchErr error
9798
)
9899
if len(matchResources.ResourceRules) == 0 {
99100
isMatch = true
100101
matchKind = attr.GetKind()
102+
matchResource = attr.GetResource()
101103
} else {
102-
isMatch, matchKind, matchErr = matchesResourceRules(matchResources.ResourceRules, matchPolicy, attr, o)
104+
isMatch, matchResource, matchKind, matchErr = matchesResourceRules(matchResources.ResourceRules, matchPolicy, attr, o)
103105
}
104106
if matchErr != nil {
105-
return false, schema.GroupVersionKind{}, matchErr
107+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchErr
106108
}
107109
if !isMatch {
108-
return false, schema.GroupVersionKind{}, nil
110+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
109111
}
110112

111113
// now that we know this applies to this request otherwise, if there were selector errors, return them
112114
if matchNsErr != nil {
113-
return false, schema.GroupVersionKind{}, matchNsErr
115+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchNsErr
114116
}
115117
if matchObjErr != nil {
116-
return false, schema.GroupVersionKind{}, matchObjErr
118+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchObjErr
117119
}
118120

119-
return true, matchKind, nil
121+
return true, matchResource, matchKind, nil
120122
}
121123

122-
func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPolicy *v1beta1.MatchPolicyType, attr admission.Attributes, o admission.ObjectInterfaces) (bool, schema.GroupVersionKind, error) {
124+
func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPolicy *v1beta1.MatchPolicyType, attr admission.Attributes, o admission.ObjectInterfaces) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
123125
matchKind := attr.GetKind()
126+
matchResource := attr.GetResource()
127+
124128
for _, namedRule := range namedRules {
125129
rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
126130
ruleMatcher := rules.Matcher{
@@ -132,22 +136,22 @@ func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPol
132136
}
133137
// an empty name list always matches
134138
if len(namedRule.ResourceNames) == 0 {
135-
return true, matchKind, nil
139+
return true, matchResource, matchKind, nil
136140
}
137141
// TODO: GetName() can return an empty string if the user is relying on
138142
// the API server to generate the name... figure out what to do for this edge case
139143
name := attr.GetName()
140144
for _, matchedName := range namedRule.ResourceNames {
141145
if name == matchedName {
142-
return true, matchKind, nil
146+
return true, matchResource, matchKind, nil
143147
}
144148
}
145149
}
146150

147151
// if match policy is undefined or exact, don't perform fuzzy matching
148152
// note that defaulting to fuzzy matching is set by the API
149153
if matchPolicy == nil || *matchPolicy == v1beta1.Exact {
150-
return false, schema.GroupVersionKind{}, nil
154+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
151155
}
152156

153157
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
@@ -169,24 +173,24 @@ func matchesResourceRules(namedRules []v1beta1.NamedRuleWithOperations, matchPol
169173
}
170174
matchKind = o.GetEquivalentResourceMapper().KindFor(equivalent, attr.GetSubresource())
171175
if matchKind.Empty() {
172-
return false, schema.GroupVersionKind{}, fmt.Errorf("unable to convert to %v: unknown kind", equivalent)
176+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("unable to convert to %v: unknown kind", equivalent)
173177
}
174178
// an empty name list always matches
175179
if len(namedRule.ResourceNames) == 0 {
176-
return true, matchKind, nil
180+
return true, equivalent, matchKind, nil
177181
}
178182

179183
// TODO: GetName() can return an empty string if the user is relying on
180184
// the API server to generate the name... figure out what to do for this edge case
181185
name := attr.GetName()
182186
for _, matchedName := range namedRule.ResourceNames {
183187
if name == matchedName {
184-
return true, matchKind, nil
188+
return true, equivalent, matchKind, nil
185189
}
186190
}
187191
}
188192
}
189-
return false, schema.GroupVersionKind{}, nil
193+
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
190194
}
191195

192196
type attrWithResourceOverride struct {

staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching/matching_test.go

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ func TestMatcher(t *testing.T) {
9898
criteria *v1beta1.MatchResources
9999
attrs admission.Attributes
100100

101-
expectMatches bool
102-
expectMatchKind schema.GroupVersionKind
103-
expectErr string
101+
expectMatches bool
102+
expectMatchKind schema.GroupVersionKind
103+
expectMatchResource schema.GroupVersionResource
104+
expectErr string
104105
}{
105106
{
106107
name: "no rules (just write)",
@@ -204,9 +205,10 @@ func TestMatcher(t *testing.T) {
204205
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
205206
},
206207
}}},
207-
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
208-
expectMatches: true,
209-
expectMatchKind: gvk("extensions", "v1beta1", "Deployment"),
208+
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
209+
expectMatches: true,
210+
expectMatchResource: gvr("extensions", "v1beta1", "deployments"),
211+
expectMatchKind: gvk("extensions", "v1beta1", "Deployment"),
210212
},
211213
{
212214
name: "specific rules, equivalent match, prefer apps",
@@ -225,9 +227,10 @@ func TestMatcher(t *testing.T) {
225227
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
226228
},
227229
}}},
228-
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
229-
expectMatches: true,
230-
expectMatchKind: gvk("apps", "v1beta1", "Deployment"),
230+
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
231+
expectMatches: true,
232+
expectMatchResource: gvr("apps", "v1beta1", "deployments"),
233+
expectMatchKind: gvk("apps", "v1beta1", "Deployment"),
231234
},
232235

233236
{
@@ -311,9 +314,10 @@ func TestMatcher(t *testing.T) {
311314
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
312315
},
313316
}}},
314-
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
315-
expectMatches: true,
316-
expectMatchKind: gvk("extensions", "v1beta1", "Scale"),
317+
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
318+
expectMatches: true,
319+
expectMatchResource: gvr("extensions", "v1beta1", "deployments"),
320+
expectMatchKind: gvk("extensions", "v1beta1", "Scale"),
317321
},
318322
{
319323
name: "specific rules, subresource equivalent match, prefer apps",
@@ -332,9 +336,10 @@ func TestMatcher(t *testing.T) {
332336
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
333337
},
334338
}}},
335-
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
336-
expectMatches: true,
337-
expectMatchKind: gvk("apps", "v1beta1", "Scale"),
339+
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
340+
expectMatches: true,
341+
expectMatchResource: gvr("apps", "v1beta1", "deployments"),
342+
expectMatchKind: gvk("apps", "v1beta1", "Scale"),
338343
},
339344
{
340345
name: "specific rules, prefer exact match and name match",
@@ -380,9 +385,10 @@ func TestMatcher(t *testing.T) {
380385
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
381386
},
382387
}}},
383-
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("extensions", "v1beta1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
384-
expectMatches: true,
385-
expectMatchKind: gvk("autoscaling", "v1", "Scale"),
388+
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("extensions", "v1beta1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
389+
expectMatches: true,
390+
expectMatchResource: gvr("apps", "v1", "deployments"),
391+
expectMatchKind: gvk("autoscaling", "v1", "Scale"),
386392
},
387393
{
388394
name: "specific rules, subresource equivalent match, prefer extensions and name match miss",
@@ -536,7 +542,7 @@ func TestMatcher(t *testing.T) {
536542

537543
for _, testcase := range testcases {
538544
t.Run(testcase.name, func(t *testing.T) {
539-
matches, matchKind, err := a.Matches(testcase.attrs, interfaces, &fakeCriteria{matchResources: *testcase.criteria})
545+
matches, matchResource, matchKind, err := a.Matches(testcase.attrs, interfaces, &fakeCriteria{matchResources: *testcase.criteria})
540546
if err != nil {
541547
if len(testcase.expectErr) == 0 {
542548
t.Fatal(err)
@@ -558,6 +564,22 @@ func TestMatcher(t *testing.T) {
558564
if matches != testcase.expectMatches {
559565
t.Fatalf("expected matches = %v; got %v", testcase.expectMatches, matches)
560566
}
567+
568+
expectResource := testcase.expectMatchResource
569+
if !expectResource.Empty() && !matches {
570+
t.Fatalf("expectResource is non-empty, but did not match")
571+
} else if expectResource.Empty() {
572+
// Test for exact match by default. Tests that expect an equivalent
573+
// resource to match should explicitly state so by supplying
574+
// expectMatchResource
575+
expectResource = testcase.attrs.GetResource()
576+
}
577+
578+
if matches {
579+
if matchResource != expectResource {
580+
t.Fatalf("expected matchResource %v, got %v", expectResource, matchResource)
581+
}
582+
}
561583
})
562584
}
563585
}

0 commit comments

Comments
 (0)