Skip to content

Commit aec863c

Browse files
committed
Implemented permission checker
1 parent 0ea73a9 commit aec863c

6 files changed

Lines changed: 207 additions & 2 deletions

File tree

cmd/operator-opamp-bridge/internal/standalone/client.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,61 @@ func eventObject(obj any) client.Object {
183183
return object
184184
}
185185

186+
func (c *Client) checkPermissions(ctx context.Context, agents []config.StandaloneAgentConfig, remoteConfigEnabled bool) error {
187+
perms := []permission{}
188+
for _, rule := range []struct {
189+
apiGroup string
190+
resource string
191+
}{
192+
{resource: "configmaps"},
193+
{apiGroup: "apps", resource: "deployments"},
194+
{apiGroup: "apps", resource: "daemonsets"},
195+
{apiGroup: "apps", resource: "statefulsets"},
196+
} {
197+
for _, verb := range []string{"list", "watch"} {
198+
perms = append(perms, permission{verb: verb, apiGroup: rule.apiGroup, resource: rule.resource})
199+
}
200+
}
201+
202+
for _, agent := range agents {
203+
workloadResource, err := standaloneWorkloadResource(agent.WorkloadType)
204+
if err != nil {
205+
return err
206+
}
207+
perms = append(perms, permission{verb: "get", apiGroup: "apps", resource: workloadResource, namespace: agent.Namespace, name: agent.WorkloadName})
208+
if remoteConfigEnabled {
209+
perms = append(perms, permission{verb: "update", apiGroup: "apps", resource: workloadResource, namespace: agent.Namespace, name: agent.WorkloadName})
210+
}
211+
for _, entry := range agent.Config {
212+
if !strings.EqualFold(entry.Kind, standaloneConfigMapKind) {
213+
continue
214+
}
215+
perms = append(perms, permission{verb: "get", resource: "configmaps", namespace: agent.Namespace, name: entry.Name})
216+
if remoteConfigEnabled {
217+
perms = append(perms, permission{verb: "update", resource: "configmaps", namespace: agent.Namespace, name: entry.Name})
218+
}
219+
}
220+
}
221+
222+
if err := newPermissionsChecker(c.k8sClient).Check(ctx, perms); err != nil {
223+
return fmt.Errorf("standalone permission check failed: %w", err)
224+
}
225+
return nil
226+
}
227+
228+
func standaloneWorkloadResource(workloadType string) (string, error) {
229+
switch strings.ToLower(workloadType) {
230+
case "deployment":
231+
return "deployments", nil
232+
case "daemonset":
233+
return "daemonsets", nil
234+
case "statefulset":
235+
return "statefulsets", nil
236+
default:
237+
return "", fmt.Errorf("unsupported workload type %q", workloadType)
238+
}
239+
}
240+
186241
func (c *Client) getConfigMapFile(namespace string, entry config.StandaloneConfigEntry) ([]byte, error) {
187242
cm := &v1.ConfigMap{}
188243
if err := c.k8sClient.Get(context.Background(), client.ObjectKey{Name: entry.Name, Namespace: namespace}, cm); err != nil {

cmd/operator-opamp-bridge/internal/standalone/client_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ import (
1515
"github.com/stretchr/testify/assert"
1616
"github.com/stretchr/testify/require"
1717
appsv1 "k8s.io/api/apps/v1"
18+
authorizationv1 "k8s.io/api/authorization/v1"
1819
v1 "k8s.io/api/core/v1"
1920
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2021
"k8s.io/apimachinery/pkg/runtime"
2122
"k8s.io/utils/ptr"
2223
"sigs.k8s.io/controller-runtime/pkg/client"
2324
"sigs.k8s.io/controller-runtime/pkg/client/fake"
25+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
2426

2527
"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/config"
2628
"github.com/open-telemetry/opentelemetry-operator/cmd/operator-opamp-bridge/internal/operator"
@@ -45,13 +47,37 @@ func getFakeK8sClient(t *testing.T, objs ...client.Object) client.Client {
4547
scheme := runtime.NewScheme()
4648
require.NoError(t, v1.AddToScheme(scheme))
4749
require.NoError(t, appsv1.AddToScheme(scheme))
50+
require.NoError(t, authorizationv1.AddToScheme(scheme))
4851
builder := fake.NewClientBuilder().WithScheme(scheme)
4952
for _, obj := range objs {
5053
builder = builder.WithObjects(obj)
5154
}
5255
return builder.Build()
5356
}
5457

58+
func getPermissionReviewClient(t *testing.T, allowed func(authorizationv1.ResourceAttributes) bool) client.Client {
59+
scheme := runtime.NewScheme()
60+
require.NoError(t, v1.AddToScheme(scheme))
61+
require.NoError(t, appsv1.AddToScheme(scheme))
62+
require.NoError(t, authorizationv1.AddToScheme(scheme))
63+
return fake.NewClientBuilder().
64+
WithScheme(scheme).
65+
WithInterceptorFuncs(interceptor.Funcs{
66+
Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
67+
review, ok := obj.(*authorizationv1.SelfSubjectAccessReview)
68+
if !ok {
69+
return c.Create(ctx, obj, opts...)
70+
}
71+
review.Status.Allowed = allowed(*review.Spec.ResourceAttributes)
72+
if !review.Status.Allowed {
73+
review.Status.Reason = "denied by test"
74+
}
75+
return nil
76+
},
77+
}).
78+
Build()
79+
}
80+
5581
func testConfigMap() *v1.ConfigMap {
5682
return &v1.ConfigMap{
5783
ObjectMeta: metav1.ObjectMeta{
@@ -284,6 +310,45 @@ func TestClientNotifyWorkloadHealthUpdate(t *testing.T) {
284310
assert.True(t, called)
285311
}
286312

313+
func TestClientCheckPermissionsAllowsConfiguredStandaloneResources(t *testing.T) {
314+
c := newTestClient(getPermissionReviewClient(t, func(_ authorizationv1.ResourceAttributes) bool {
315+
return true
316+
}))
317+
318+
err := c.checkPermissions(context.Background(), []config.StandaloneAgentConfig{testAgentConfig()}, true)
319+
320+
require.NoError(t, err)
321+
}
322+
323+
func TestClientCheckPermissionsRejectsMissingConfiguredResourcePermission(t *testing.T) {
324+
c := newTestClient(getPermissionReviewClient(t, func(attrs authorizationv1.ResourceAttributes) bool {
325+
return attrs.Verb != "update" || attrs.Resource != "deployments" || attrs.Name != "standalone-collector"
326+
}))
327+
328+
err := c.checkPermissions(context.Background(), []config.StandaloneAgentConfig{testAgentConfig()}, true)
329+
330+
require.Error(t, err)
331+
assert.Contains(t, err.Error(), "standalone permission check failed")
332+
assert.Contains(t, err.Error(), "missing update permission for apps/deployments default/standalone-collector")
333+
}
334+
335+
func TestPermissionsCheckerRejectsDeniedPermission(t *testing.T) {
336+
checker := newPermissionsChecker(getPermissionReviewClient(t, func(_ authorizationv1.ResourceAttributes) bool {
337+
return false
338+
}))
339+
340+
err := checker.Check(context.Background(), []permission{{
341+
verb: "update",
342+
apiGroup: "apps",
343+
resource: "deployments",
344+
namespace: "default",
345+
name: "standalone-collector",
346+
}})
347+
348+
require.Error(t, err)
349+
assert.Contains(t, err.Error(), "missing update permission for apps/deployments default/standalone-collector")
350+
}
351+
287352
func TestScopedApplierApplyUpdatesConfiguredConfigMapKeyAndRestartsWorkload(t *testing.T) {
288353
cm := testConfigMap()
289354
deploy := testDeployment()

cmd/operator-opamp-bridge/internal/standalone/configmap_instance.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func (p *standaloneCollectorInstance) GetNamespace() string {
3030
return p.namespace
3131
}
3232

33-
func (p *standaloneCollectorInstance) GetDeletionTimestamp() *metav1.Time {
33+
func (*standaloneCollectorInstance) GetDeletionTimestamp() *metav1.Time {
3434
return nil
3535
}
3636

cmd/operator-opamp-bridge/internal/standalone/manager.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ func NewManager(log logr.Logger, cfg *config.Config, c client.Client, restCfg *r
4747
}
4848

4949
func (m *Manager) Start(ctx context.Context) error {
50+
if err := m.client.checkPermissions(ctx, m.cfg.Standalone.Agents, m.cfg.RemoteConfigEnabled()); err != nil {
51+
return err
52+
}
5053
if err := m.startHealthServer(ctx); err != nil {
5154
return err
5255
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package standalone
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"strings"
11+
12+
authorizationv1 "k8s.io/api/authorization/v1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
)
15+
16+
type permission struct {
17+
verb string
18+
apiGroup string
19+
resource string
20+
namespace string
21+
name string
22+
}
23+
24+
type permissionsChecker struct {
25+
k8sClient client.Client
26+
}
27+
28+
func newPermissionsChecker(k8sClient client.Client) permissionsChecker {
29+
return permissionsChecker{k8sClient: k8sClient}
30+
}
31+
32+
func (p permissionsChecker) Check(ctx context.Context, perms []permission) error {
33+
var errs []error
34+
for _, perm := range perms {
35+
if err := p.check(ctx, perm); err != nil {
36+
errs = append(errs, err)
37+
}
38+
}
39+
return errors.Join(errs...)
40+
}
41+
42+
func (p permissionsChecker) check(ctx context.Context, perm permission) error {
43+
review := &authorizationv1.SelfSubjectAccessReview{
44+
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
45+
ResourceAttributes: &authorizationv1.ResourceAttributes{
46+
Verb: perm.verb,
47+
Group: perm.apiGroup,
48+
Resource: perm.resource,
49+
Namespace: perm.namespace,
50+
Name: perm.name,
51+
},
52+
},
53+
}
54+
if err := p.k8sClient.Create(ctx, review); err != nil {
55+
return fmt.Errorf("failed to check %s permission for %s: %w", perm.verb, perm.description(), err)
56+
}
57+
if review.Status.Allowed {
58+
return nil
59+
}
60+
reason := review.Status.Reason
61+
if review.Status.EvaluationError != "" {
62+
reason = strings.TrimSpace(reason + ": " + review.Status.EvaluationError)
63+
}
64+
if reason == "" {
65+
reason = "access denied"
66+
}
67+
return fmt.Errorf("missing %s permission for %s: %s", perm.verb, perm.description(), reason)
68+
}
69+
70+
func (p permission) description() string {
71+
resource := p.resource
72+
if p.apiGroup != "" {
73+
resource = p.apiGroup + "/" + resource
74+
}
75+
if p.namespace == "" && p.name == "" {
76+
return resource
77+
}
78+
if p.name == "" {
79+
return fmt.Sprintf("%s in namespace %s", resource, p.namespace)
80+
}
81+
return fmt.Sprintf("%s %s/%s", resource, p.namespace, p.name)
82+
}

config/standalone-bridge/kustomization.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ resources:
1313
images:
1414
- name: operator-opamp-bridge
1515
newName: ghcr.io/open-telemetry/opentelemetry-operator/operator-opamp-bridge
16-
newTag: v0.150.0-96-g0360ab9e
16+
newTag: v0.150.0-120-gf71eb58d

0 commit comments

Comments
 (0)