tailcfg,cmd/k8s-operator,kube: move Kubernetes cap to a location that can be shared with control (#12236)

This PR is in prep of adding logic to control to be able to parse
tailscale.com/cap/kubernetes grants in control:
- moves the type definition of PeerCapabilityKubernetes cap to a location
shared with control.
- update the Kubernetes cap rule definition with fields for granting
kubectl exec session recording capabilities.
- adds a convenience function to produce tailcfg.RawMessage from an
arbitrary cap rule and a test for it.

An example grant defined via ACLs:
"grants": [{
      "src": ["tag:eng"],
      "dst": ["tag:k8s-operator"],
      "app": {
        "tailscale.com/cap/kubernetes": [{
            "recorder": ["tag:my-recorder"]
	    “enforceRecorder”: true
        }],
      },
    }
]
This grant enforces `kubectl exec` sessions from tailnet clients,
matching `tag:eng` via API server proxy matching `tag:k8s-operator`
to be recorded and recording to be sent to a tsrecorder instance,
matching `tag:my-recorder`.

The type needs to be shared with control because we want
control to parse this cap and resolve tags to peer IPs.

Updates tailscale/corp#19821

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
This commit is contained in:
Irbe Krumina
2024-06-10 16:36:22 +01:00
committed by GitHub
parent ba46495e11
commit c3e2b7347b
5 changed files with 120 additions and 14 deletions

View File

@@ -258,6 +258,15 @@ func (m *RawMessage) UnmarshalJSON(data []byte) error {
return nil
}
// MarshalCapJSON returns a capability rule in RawMessage string format.
func MarshalCapJSON[T any](capRule T) (RawMessage, error) {
bs, err := json.Marshal(capRule)
if err != nil {
return "", fmt.Errorf("error marshalling capability rule: %w", err)
}
return RawMessage(string(bs)), nil
}
type Node struct {
ID NodeID
StableID StableNodeID

View File

@@ -854,6 +854,55 @@ func TestRawMessage(t *testing.T) {
}
}
func TestMarshalToRawMessageAndBack(t *testing.T) {
type inner struct {
Groups []string `json:"groups,omitempty"`
}
type testRule struct {
Ports []int `json:"ports,omitempty"`
ToggleOn bool `json:"toggleOn,omitempty"`
Name string `json:"name,omitempty"`
Groups inner `json:"groups,omitempty"`
}
tests := []struct {
name string
capType PeerCapability
val testRule
}{
{
name: "empty",
val: testRule{},
capType: PeerCapability("foo"),
},
{
name: "some values",
val: testRule{Ports: []int{80, 443}, Name: "foo"},
capType: PeerCapability("foo"),
},
{
name: "all values",
val: testRule{Ports: []int{80, 443}, Name: "foo", ToggleOn: true, Groups: inner{Groups: []string{"foo", "bar"}}},
capType: PeerCapability("foo"),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
raw, err := MarshalCapJSON(tc.val)
if err != nil {
t.Fatalf("unexpected error marshalling raw message: %v", err)
}
cap := PeerCapMap{tc.capType: []RawMessage{raw}}
after, err := UnmarshalCapJSON[testRule](cap, tc.capType)
if err != nil {
t.Fatalf("unexpected error unmarshaling raw message: %v", err)
}
if !reflect.DeepEqual([]testRule{tc.val}, after) {
t.Errorf("got %#v; want %#v", after, []testRule{tc.val})
}
})
}
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
BadDeps: map[string]string{