Skip to content

Commit 97ea889

Browse files
authored
Merge pull request #71 from authzed/authorize-updates
add support for authorizing update request (`update` and `patch` verbs)
2 parents c49f8bb + d042722 commit 97ea889

File tree

6 files changed

+110
-6
lines changed

6 files changed

+110
-6
lines changed

e2e/proxy_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package e2e
44

55
import (
66
"context"
7+
"encoding/json"
78
"fmt"
89
"sync"
910
"time"
@@ -15,10 +16,12 @@ import (
1516
corev1 "k8s.io/api/core/v1"
1617
k8serrors "k8s.io/apimachinery/pkg/api/errors"
1718
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/types"
1820
"k8s.io/apiserver/pkg/endpoints/request"
1921
"k8s.io/apiserver/pkg/storage/names"
2022
"k8s.io/client-go/kubernetes"
2123
"k8s.io/client-go/tools/clientcmd"
24+
"k8s.io/utils/pointer"
2225

2326
"github.com/authzed/spicedb-kubeapi-proxy/pkg/authz/distributedtx"
2427
"github.com/authzed/spicedb-kubeapi-proxy/pkg/config/proxyrule"
@@ -136,6 +139,41 @@ var _ = Describe("Proxy", func() {
136139
_, err := client.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
137140
return err
138141
}
142+
GetAndUpdatePodLabels := func(ctx context.Context, client kubernetes.Interface, namespace, name string, labels map[string]string) error {
143+
pod, err := client.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
144+
if err != nil {
145+
return err
146+
}
147+
pod.Labels = labels
148+
_, err = client.CoreV1().Pods(namespace).Update(ctx, pod, metav1.UpdateOptions{})
149+
return err
150+
}
151+
PatchPodLabels := func(ctx context.Context, client kubernetes.Interface, namespace, name string, labels map[string]string) error {
152+
patch := &corev1.Pod{
153+
TypeMeta: metav1.TypeMeta{
154+
Kind: "Pod",
155+
APIVersion: "v1",
156+
},
157+
ObjectMeta: metav1.ObjectMeta{
158+
Name: name,
159+
Namespace: namespace,
160+
Labels: labels,
161+
},
162+
}
163+
data, err := json.Marshal(patch)
164+
if err != nil {
165+
return err
166+
}
167+
_, err = client.CoreV1().Pods(namespace).Patch(ctx, name, types.ApplyPatchType, data, metav1.PatchOptions{FieldManager: "Test", Force: pointer.Bool(true)})
168+
return err
169+
}
170+
ListPods := func(ctx context.Context, client kubernetes.Interface, namespace string) []string {
171+
visibleNamespaces, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
172+
Expect(err).To(Succeed())
173+
return lo.Map(visibleNamespaces.Items, func(item corev1.Pod, index int) string {
174+
return item.Name
175+
})
176+
}
139177
WatchPods := func(ctx context.Context, client kubernetes.Interface, namespace string, expected int, timeout time.Duration) []string {
140178
ctx, cancel := context.WithTimeout(ctx, timeout)
141179
defer cancel()
@@ -167,6 +205,43 @@ var _ = Describe("Proxy", func() {
167205
})
168206

169207
AssertDualWriteBehavior := func() {
208+
209+
It("supports rules for every verb", func(ctx context.Context) {
210+
// watch
211+
var wg sync.WaitGroup
212+
defer wg.Wait()
213+
wg.Add(1)
214+
go func() {
215+
defer GinkgoRecover()
216+
Expect(WatchPods(ctx, paulClient, paulNamespace, 1, 10*time.Second)).To(ContainElement(paulPod))
217+
wg.Done()
218+
}()
219+
220+
// create
221+
Expect(CreateNamespace(ctx, paulClient, paulNamespace)).To(Succeed())
222+
Expect(CreatePod(ctx, paulClient, paulNamespace, paulPod)).To(Succeed())
223+
224+
// get
225+
Expect(GetPod(ctx, paulClient, paulNamespace, paulPod)).To(Succeed())
226+
Expect(GetPod(ctx, chaniClient, paulNamespace, paulPod)).To(Not(Succeed()))
227+
228+
// update
229+
Expect(GetAndUpdatePodLabels(ctx, paulClient, paulNamespace, paulPod, map[string]string{"a": "label"})).To(Succeed())
230+
Expect(GetAndUpdatePodLabels(ctx, chaniClient, paulNamespace, paulPod, map[string]string{"a": "label"})).To(Not(Succeed()))
231+
232+
// patch
233+
Expect(PatchPodLabels(ctx, paulClient, paulNamespace, paulPod, map[string]string{"b": "label"})).To(Succeed())
234+
Expect(PatchPodLabels(ctx, chaniClient, paulNamespace, paulPod, map[string]string{"b": "label"})).To(Not(Succeed()))
235+
236+
// list
237+
Expect(ListPods(ctx, paulClient, paulNamespace)).To(Equal([]string{paulPod}))
238+
Expect(ListPods(ctx, chaniClient, paulNamespace)).To(Equal([]string{}))
239+
240+
// delete
241+
Expect(DeletePod(ctx, chaniClient, paulNamespace, paulPod)).To(Not(Succeed()))
242+
Expect(DeletePod(ctx, paulClient, paulNamespace, paulPod)).To(Succeed())
243+
})
244+
170245
It("doesn't show users namespaces the other has created", func(ctx context.Context) {
171246
var wg sync.WaitGroup
172247
defer wg.Wait()
@@ -623,6 +698,9 @@ var (
623698
Resource: "pods",
624699
Verbs: []string{"delete"},
625700
}},
701+
Checks: []proxyrule.StringOrTemplate{{
702+
Template: "pod:{{namespacedName}}#edit@user:{{user.Name}}",
703+
}},
626704
Writes: []proxyrule.StringOrTemplate{{
627705
Template: "pod:{{namespacedName}}#creator@user:{{user.Name}}",
628706
}, {
@@ -644,6 +722,19 @@ var (
644722
},
645723
}
646724

725+
updatePod = proxyrule.Config{
726+
Spec: proxyrule.Spec{
727+
Matches: []proxyrule.Match{{
728+
GroupVersion: "v1",
729+
Resource: "pods",
730+
Verbs: []string{"update", "patch"},
731+
}},
732+
Checks: []proxyrule.StringOrTemplate{{
733+
Template: "pod:{{namespacedName}}#edit@user:{{user.Name}}",
734+
}},
735+
},
736+
}
737+
647738
listWatchPod = proxyrule.Config{
648739
Spec: proxyrule.Spec{
649740
Matches: []proxyrule.Match{{
@@ -669,6 +760,7 @@ func testOptimisticMatcher() rules.MapMatcher {
669760
createPod(),
670761
deletePod(),
671762
getPod,
763+
updatePod,
672764
listWatchPod,
673765
})
674766
Expect(err).To(Succeed())
@@ -701,6 +793,7 @@ func testPessimisticMatcher() rules.MapMatcher {
701793
pessimisticCreatePod,
702794
pessimisticDeletePod,
703795
getPod,
796+
updatePod,
704797
listWatchPod,
705798
})
706799
Expect(err).To(Succeed())

pkg/authz/authz.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ func WithAuthorization(handler, failed http.Handler, permissionsClient v1.Permis
5151
"APIVersion", input.Request.APIVersion,
5252
"Resource", input.Request.Resource)
5353
}
54+
5455
// run all checks for this request
5556
if err := runAllMatchingChecks(ctx, matchingRules, input, permissionsClient); err != nil {
5657
handleError(w, failed, req, err)
@@ -72,7 +73,7 @@ func WithAuthorization(handler, failed http.Handler, permissionsClient v1.Permis
7273
removedNNC: make(chan types.NamespacedName),
7374
allowedNN: map[types.NamespacedName]struct{}{},
7475
}
75-
authorizeGet(input, authzData)
76+
alreadyAuthorized(input, authzData)
7677
if err := filterResponse(ctx, matchingRules, input, authzData, permissionsClient, watchClient); err != nil {
7778
failed.ServeHTTP(w, req)
7879
return

pkg/authz/filter.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"slices"
910

1011
"github.com/kyverno/go-jmespath"
1112
"k8s.io/klog/v2"
@@ -49,16 +50,19 @@ func filterResponse(ctx context.Context, matchingRules []*rules.RunnableRule, in
4950
return nil
5051
}
5152

52-
func authorizeGet(input *rules.ResolveInput, authzData *AuthzData) {
53-
if input.Request.Verb != "get" {
53+
// alreadyAuthorized adds the input object to the authorized list.
54+
// This is used for requests where the `check` on the rule is expected to authorize
55+
// a single object for filtering, i.e. for `get`, `update`, or `patch`
56+
// (where update/patch don't create the object).
57+
func alreadyAuthorized(input *rules.ResolveInput, authzData *AuthzData) {
58+
if !slices.Contains([]string{"get", "update", "patch"}, input.Request.Verb) {
5459
return
5560
}
5661
close(authzData.allowedNNC)
5762
close(authzData.removedNNC)
5863
authzData.Lock()
5964
defer authzData.Unlock()
60-
// gets aren't really filtered, but we add them to the allowed list so that
61-
// downstream can know it's passed all configured checks.
65+
6266
authzData.allowedNN[types.NamespacedName{Name: input.Name, Namespace: input.Namespace}] = struct{}{}
6367
}
6468

pkg/proxy/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func NewServer(ctx context.Context, o Options) (*Server, error) {
120120
}
121121
s.WorkflowWorker = worker
122122

123+
// Matcher is a pointer to an interface to make it easy to swap at runtime in tests
123124
s.Matcher = &s.opts.Matcher
124125
handler, err := authz.WithAuthorization(clusterProxy, failHandler, o.PermissionsClient, o.WatchClient, workflowClient, s.Matcher, s.opts.inputExtractor)
125126
if err != nil {

pkg/rules/rules.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,13 @@ type RelExpr struct {
114114
// with resolved values.
115115
type ResolvedRel UncompiledRelExpr
116116

117+
// ResolveInputExtractor defines how ResolveInput are extracted from requests.
118+
// This interface exists so that tests can easily fake the request data.
117119
type ResolveInputExtractor interface {
118120
ExtractFromHttp(req *http.Request) (*ResolveInput, error)
119121
}
120122

121-
// ResolveInputExtractorFunc is a function type that implements Matcher
123+
// ResolveInputExtractorFunc is a function type that implements ResolveInputExtractor
122124
type ResolveInputExtractorFunc func(req *http.Request) (*ResolveInput, error)
123125

124126
func (f ResolveInputExtractorFunc) ExtractFromHttp(req *http.Request) (*ResolveInput, error) {
@@ -163,6 +165,8 @@ func NewResolveInputFromHttp(req *http.Request) (*ResolveInput, error) {
163165
return nil, fmt.Errorf("unable to decode request body as kube object: %w", err)
164166
}
165167
object = &pom
168+
169+
req.Body = io.NopCloser(bytes.NewReader(body))
166170
}
167171
return NewResolveInput(requestInfo, userInfo.(*user.DefaultInfo), object, body, req.Header.Clone()), nil
168172
}

pkg/spicedb/bootstrap.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ schema: |-
1111
relation namespace: namespace
1212
relation creator: user
1313
relation viewer: user
14+
permission edit = creator
1415
permission view = viewer + creator
1516
}
1617
definition lock {

0 commit comments

Comments
 (0)