Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions controller/api/v1alpha1/lease_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ func LeaseFromProtobuf(
AllowDisabled: req.AllowDisabled,
BeginTime: beginTime,
EndTime: endTime,
SharedWith: req.SharedWith,
},
}, nil
}
Expand Down Expand Up @@ -302,6 +303,7 @@ func (l *Lease) ToProtobuf() *cpb.Lease {
Tags: l.Spec.Tags,
AllowDisabled: l.Spec.AllowDisabled,
Context: l.Spec.Context,
SharedWith: l.Spec.SharedWith,
}
if l.Spec.ExporterRef != nil {
lease.ExporterName = ptr.To(l.Spec.ExporterRef.Name)
Expand Down Expand Up @@ -396,6 +398,45 @@ func (l *Lease) SetStatusCondition(
})
}

func (l *Lease) IsAccessibleBy(clientName string) bool {
if l.Spec.ClientRef.Name == clientName {
return true
}
return slices.Contains(l.Spec.SharedWith, clientName)
}

func (l *Lease) IsOwnedBy(clientName string) bool {
return l.Spec.ClientRef.Name == clientName
}

func ClientAllowedByPolicy(
policies []ExporterAccessPolicy,
exporter *Exporter,
jclient *Client,
) bool {
for _, policy := range policies {
exporterSelector, err := metav1.LabelSelectorAsSelector(&policy.Spec.ExporterSelector)
if err != nil {
continue
}
if !exporterSelector.Matches(labels.Set(exporter.Labels)) {
continue
}
for _, p := range policy.Spec.Policies {
for _, from := range p.From {
clientSelector, err := metav1.LabelSelectorAsSelector(&from.ClientSelector)
if err != nil {
continue
}
if clientSelector.Matches(labels.Set(jclient.Labels)) {
return true
}
}
}
}
return false
}
Comment on lines +412 to +438

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect ExporterAccessPolicy spec shape and all evaluation sites.
set -euo pipefail

fd -e go . controller/api --exec rg -n -C3 'ExporterAccessPolicySpec|type ExporterAccessPolicy|ClientSelector|ExporterSelector' {}

echo '--- callers ---'
rg -nP --type=go -C6 '\bClientAllowedByPolicy\s*\('

Repository: jumpstarter-dev/jumpstarter

Length of output: 3702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the policy type definition and policy evaluation sites.
echo '--- all go files mentioning Policy/From/ExporterAccessPolicySpec/ClientAllowedByPolicy ---'
rg -l -n --type=go 'type Policy struct|type From struct|type ExporterAccessPolicySpec|ClientAllowedByPolicy' .
echo

for f in $(rg -l -n --type=go 'type Policy struct|type From struct|type ExporterAccessPolicySpec|ClientAllowedByPolicy' .); do
  echo "--- $f ---"
  nl -ba "$f" | sed -n '1,120p'
done

echo '--- all call sites ---'
rg -nC8 --type=go '\bClientAllowedByPolicy\s*\(' . || true

Repository: jumpstarter-dev/jumpstarter

Length of output: 630


🏁 Script executed:

#!/bin/bash
set -euo pipefail

find . -path '*/controller/api/v1alpha1/exporteraccesspolicy_types.go' -o -path '*/controller/api/v1alpha1/lease_helpers.go' -o -path '*/controller/internal/service/client/v1/client_service.go' -o -path '*/controller/internal/controller/lease_controller.go' -o -path '*/controller/internal/controller/lease_controller_test.go' -print | sort

echo '--- exporteraccesspolicy_types.go ---'
awk '{printf "%6d\t%s\n", NR, $0}' controller/api/v1alpha1/exporteraccesspolicy_types.go | sed -n '1,110p'

echo '--- lease_helpers.go ClientAllowedByPolicy ---'
awk '{printf "%6d\t%s\n", NR, $0}' controller/api/v1alpha1/lease_helpers.go | sed -n '398,448p'

echo '--- clients service call sites ---'
awk '{printf "%6d\t%s\n", NR, $0}' controller/internal/service/client/v1/client_service.go | sed -n '518,545p;658,680p'

echo '--- lease controller call sites ---'
awk '{printf "%6d\t%s\n", NR, $0}' controller/internal/controller/lease_controller.go | sed -n '518,540p;660,685p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 9649


Log parse failures for policy selectors.

Policy.From.ClientSelector and ExporterSelector are allow-list selectors only, so malformed selectors currently do not create an access-integrity defect. Skipping invalid selectors without logging can still hide an inactive policy from the operator.

[maintenance_and_code_quality]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/api/v1alpha1/lease_helpers.go` around lines 412 - 438, Update
ClientAllowedByPolicy to log selector parse failures when
LabelSelectorAsSelector cannot parse either policy.Spec.ExporterSelector or
from.ClientSelector, while continuing to skip the malformed selector and
preserve the existing access decision. Use the repository’s established logging
mechanism and include enough selector/policy context for operators to identify
the inactive policy.


func (l *Lease) GetExporterName() string {
if l.Status.ExporterRef == nil {
return "(none)"
Expand Down
143 changes: 143 additions & 0 deletions controller/api/v1alpha1/lease_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,3 +645,146 @@ var _ = Describe("LeaseFromProtobuf context", func() {
Expect(lease.Spec.Context).To(BeNil())
})
})

var _ = Describe("IsAccessibleBy", func() {
var lease *Lease

BeforeEach(func() {
lease = &Lease{
Spec: LeaseSpec{
ClientRef: corev1.LocalObjectReference{Name: "owner"},
},
}
})

It("should grant access to owner", func() {
Expect(lease.IsAccessibleBy("owner")).To(BeTrue())
})

It("should grant access to shared user", func() {
lease.Spec.SharedWith = []string{"alice", "bob"}
Expect(lease.IsAccessibleBy("alice")).To(BeTrue())
Expect(lease.IsAccessibleBy("bob")).To(BeTrue())
})

It("should deny access to unrelated client", func() {
lease.Spec.SharedWith = []string{"alice"}
Expect(lease.IsAccessibleBy("mallory")).To(BeFalse())
})

It("should deny access when SharedWith is empty", func() {
Expect(lease.IsAccessibleBy("alice")).To(BeFalse())
})
})

var _ = Describe("IsOwnedBy", func() {
var lease *Lease

BeforeEach(func() {
lease = &Lease{
Spec: LeaseSpec{
ClientRef: corev1.LocalObjectReference{Name: "owner"},
SharedWith: []string{"alice"},
},
}
})

It("should return true for owner", func() {
Expect(lease.IsOwnedBy("owner")).To(BeTrue())
})

It("should return false for shared user", func() {
Expect(lease.IsOwnedBy("alice")).To(BeFalse())
})

It("should return false for unrelated client", func() {
Expect(lease.IsOwnedBy("mallory")).To(BeFalse())
})
})

var _ = Describe("LeaseFromProtobuf SharedWith", func() {
It("should map shared_with from proto to spec", func() {
pbLease := &cpb.Lease{
Selector: "board=rpi4",
Duration: durationpb.New(time.Hour),
SharedWith: []string{"alice", "bob"},
}
key := types.NamespacedName{Name: "test-lease", Namespace: "default"}
clientRef := corev1.LocalObjectReference{Name: "test-client"}

lease, err := LeaseFromProtobuf(pbLease, key, clientRef)

Expect(err).NotTo(HaveOccurred())
Expect(lease.Spec.SharedWith).To(ConsistOf("alice", "bob"))
})

It("should leave SharedWith nil when proto has no shared_with", func() {
pbLease := &cpb.Lease{
Selector: "board=rpi4",
Duration: durationpb.New(time.Hour),
}
key := types.NamespacedName{Name: "test-lease", Namespace: "default"}
clientRef := corev1.LocalObjectReference{Name: "test-client"}

lease, err := LeaseFromProtobuf(pbLease, key, clientRef)

Expect(err).NotTo(HaveOccurred())
Expect(lease.Spec.SharedWith).To(BeNil())
})
})

var _ = Describe("Lease.ToProtobuf SharedWith", func() {
It("should include SharedWith in protobuf output", func() {
lease := &Lease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-lease",
Namespace: "default",
},
Spec: LeaseSpec{
ClientRef: corev1.LocalObjectReference{Name: "owner"},
Duration: &metav1.Duration{Duration: time.Hour},
Selector: metav1.LabelSelector{MatchLabels: map[string]string{"board": "rpi4"}},
SharedWith: []string{"alice", "bob"},
},
}

pb := lease.ToProtobuf()

Expect(pb.SharedWith).To(ConsistOf("alice", "bob"))
})

It("should handle nil SharedWith", func() {
lease := &Lease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-lease",
Namespace: "default",
},
Spec: LeaseSpec{
ClientRef: corev1.LocalObjectReference{Name: "owner"},
Duration: &metav1.Duration{Duration: time.Hour},
Selector: metav1.LabelSelector{MatchLabels: map[string]string{"board": "rpi4"}},
},
}

pb := lease.ToProtobuf()

Expect(pb.SharedWith).To(BeEmpty())
})

It("should roundtrip SharedWith through proto", func() {
original := []string{"alice", "bob"}
pbLease := &cpb.Lease{
Selector: "board=rpi4",
Duration: durationpb.New(time.Hour),
SharedWith: original,
}
key := types.NamespacedName{Name: "test-lease", Namespace: "default"}
clientRef := corev1.LocalObjectReference{Name: "owner"}

lease, err := LeaseFromProtobuf(pbLease, key, clientRef)
Expect(err).NotTo(HaveOccurred())

roundtripped := lease.ToProtobuf()
Expect(roundtripped.SharedWith).To(ConsistOf("alice", "bob"))
})
})
5 changes: 5 additions & 0 deletions controller/api/v1alpha1/lease_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ type LeaseSpec struct {
// Immutable after creation. Maximum 8 entries; keys max 32 chars, values max 64 chars.
// +kubebuilder:validation:MaxProperties=8
Context map[string]string `json:"context,omitempty"`
// List of client names that have shared access to this lease.
// Only the lease owner can modify this list.
// +listType=set
// +kubebuilder:validation:MaxItems=10
SharedWith []string `json:"sharedWith,omitempty"`
}

// LeaseStatus defines the observed state of Lease.
Expand Down
5 changes: 5 additions & 0 deletions controller/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
sharedWith:
description: |-
List of client names that have shared access to this lease.
Only the lease owner can modify this list.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: set
tags:
additionalProperties:
type: string
Expand Down
54 changes: 54 additions & 0 deletions controller/internal/controller/lease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ func (r *LeaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
return RequeueConflict(logger, result, err)
}

// SharedWith pruning must happen AFTER Status().Update() — that call
// refreshes the object from the API server, reverting in-memory spec
// mutations. Placing it here ensures the spec change is included in
// the r.Update() call below.
if err := r.reconcileSharedWithPolicies(ctx, &lease); err != nil {
return result, err
}

if lease.Labels == nil {
lease.Labels = make(map[string]string)
}
Expand Down Expand Up @@ -481,6 +489,52 @@ func (r *LeaseReconciler) attachMatchingPolicies(ctx context.Context, lease *jum
return approvedExporters, unmatchedDescriptions, nil
}

func (r *LeaseReconciler) reconcileSharedWithPolicies(
ctx context.Context,
lease *jumpstarterdevv1alpha1.Lease,
) error {
if len(lease.Spec.SharedWith) == 0 || lease.Status.ExporterRef == nil || lease.Status.Ended {
return nil
}

var policies jumpstarterdevv1alpha1.ExporterAccessPolicyList
if err := r.List(ctx, &policies, client.InNamespace(lease.Namespace)); err != nil {
return fmt.Errorf("reconcileSharedWithPolicies: failed to list policies: %w", err)
}

if len(policies.Items) == 0 {
return nil
}

var exporter jumpstarterdevv1alpha1.Exporter
if err := r.Get(ctx, types.NamespacedName{
Namespace: lease.Namespace,
Name: lease.Status.ExporterRef.Name,
}, &exporter); err != nil {
return fmt.Errorf("reconcileSharedWithPolicies: failed to get exporter: %w", err)
}

logger := log.FromContext(ctx)
var allowed []string
for _, clientName := range lease.Spec.SharedWith {
var jclient jumpstarterdevv1alpha1.Client
if err := r.Get(ctx, types.NamespacedName{
Namespace: lease.Namespace,
Name: clientName,
}, &jclient); err != nil {
logger.Info("removing shared client: not found", "client", clientName)
continue
}
if jumpstarterdevv1alpha1.ClientAllowedByPolicy(policies.Items, &exporter, &jclient) {
allowed = append(allowed, clientName)
} else {
logger.Info("removing shared client: denied by policy", "client", clientName)
}
}
lease.Spec.SharedWith = allowed
return nil
}

// ListMatchingExporters returns a list of exporters that match the selector of the lease
func (r *LeaseReconciler) ListMatchingExporters(ctx context.Context, lease *jumpstarterdevv1alpha1.Lease,
selector labels.Selector) (*jumpstarterdevv1alpha1.ExporterList, error) {
Expand Down
Loading
Loading