mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-29 17:58:15 +08:00
fix: Add SSH ResourceConstraint event messages, fix unmarshalling (#65721)
This commit is contained in:
@@ -1723,11 +1723,13 @@ message ResourceAccessID {
|
||||
ResourceID id = 1 [(gogoproto.nullable) = false];
|
||||
|
||||
oneof constraints {
|
||||
// unknown_constraints represents that constraints were present on the internal ResourceAccessID,
|
||||
// but are of an unknown type.
|
||||
// unknown_constraints represents that constraints were present on the internal
|
||||
// ResourceAccessID, but are of an unknown or unsupported type.
|
||||
UnknownConstraints unknown_constraints = 10;
|
||||
// aws_console captures audit-log–friendly data for AWS Console scoping constraints.
|
||||
AWSConsoleConstraints aws_console = 11;
|
||||
// ssh captures audit-log-friendly data for SSH node scoping constraints.
|
||||
SSHConstraints ssh = 12;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1747,6 +1749,14 @@ message AWSConsoleConstraints {
|
||||
repeated string role_arns_preview = 2;
|
||||
}
|
||||
|
||||
// SSHConstraints is an audit-log-friendly view of SSH node constraints.
|
||||
message SSHConstraints {
|
||||
// logins_count is the total number of logins requested.
|
||||
uint32 logins_count = 1;
|
||||
// logins_preview contains up to N logins for inspection; it may be truncated.
|
||||
repeated string logins_preview = 2;
|
||||
}
|
||||
|
||||
// AccessRequestDelete is emitted when an access request has been deleted.
|
||||
message AccessRequestDelete {
|
||||
// Metadata is a common event metadata
|
||||
|
||||
+1816
-1523
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 2026 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package events
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// constraintsJSON is the JSON-serialisable form of the protobuf oneof
|
||||
// constraints field in [ResourceAccessID]. Each variant maps to one of the
|
||||
// concrete oneof wrapper types.
|
||||
type constraintsJSON struct {
|
||||
UnknownConstraints *UnknownConstraints `json:"unknown_constraints,omitempty"`
|
||||
AWSConsole *AWSConsoleConstraints `json:"aws_console,omitempty"`
|
||||
SSH *SSHConstraints `json:"ssh,omitempty"`
|
||||
}
|
||||
|
||||
// resourceAccessIDJSON mirrors [ResourceAccessID] with a concrete Constraints
|
||||
// field so that encoding/json can marshal and unmarshal the value without
|
||||
// needing to know the concrete interface type at runtime.
|
||||
type resourceAccessIDJSON struct {
|
||||
Id ResourceID `json:"id"`
|
||||
Constraints *constraintsJSON `json:"constraints,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler] for [ResourceAccessID].
|
||||
func (r *ResourceAccessID) MarshalJSON() ([]byte, error) {
|
||||
out := resourceAccessIDJSON{Id: r.Id}
|
||||
switch c := r.Constraints.(type) {
|
||||
case *ResourceAccessID_UnknownConstraints:
|
||||
out.Constraints = &constraintsJSON{UnknownConstraints: c.UnknownConstraints}
|
||||
case *ResourceAccessID_AwsConsole:
|
||||
out.Constraints = &constraintsJSON{AWSConsole: c.AwsConsole}
|
||||
case *ResourceAccessID_Ssh:
|
||||
out.Constraints = &constraintsJSON{SSH: c.Ssh}
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler] for [ResourceAccessID].
|
||||
func (r *ResourceAccessID) UnmarshalJSON(data []byte) error {
|
||||
var tmp resourceAccessIDJSON
|
||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Id = tmp.Id
|
||||
if tmp.Constraints == nil {
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case tmp.Constraints.UnknownConstraints != nil:
|
||||
r.Constraints = &ResourceAccessID_UnknownConstraints{UnknownConstraints: tmp.Constraints.UnknownConstraints}
|
||||
case tmp.Constraints.AWSConsole != nil:
|
||||
r.Constraints = &ResourceAccessID_AwsConsole{AwsConsole: tmp.Constraints.AWSConsole}
|
||||
case tmp.Constraints.SSH != nil:
|
||||
r.Constraints = &ResourceAccessID_Ssh{Ssh: tmp.Constraints.SSH}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -67,29 +67,35 @@ func ToEventResourceAccessID(in types.ResourceAccessID) ResourceAccessID {
|
||||
|
||||
details := c.GetDetails()
|
||||
if details == nil {
|
||||
out.Constraints = &ResourceAccessID_UnknownConstraints{UnknownConstraints: &UnknownConstraints{}}
|
||||
return out
|
||||
}
|
||||
|
||||
switch d := details.(type) {
|
||||
// AWS Console constraints variant
|
||||
case *types.ResourceConstraints_AwsConsole:
|
||||
if d.AwsConsole == nil {
|
||||
// If payload is missing treat as unknown/unsupported
|
||||
out.Constraints = &ResourceAccessID_UnknownConstraints{UnknownConstraints: &UnknownConstraints{}}
|
||||
break
|
||||
}
|
||||
|
||||
roleARNs := d.AwsConsole.RoleArns
|
||||
count := len(roleARNs)
|
||||
preview := previewStrings(roleARNs, MaxAuditRoleARNPreview)
|
||||
|
||||
out.Constraints = &ResourceAccessID_AwsConsole{
|
||||
AwsConsole: &AWSConsoleConstraints{
|
||||
RoleArnsCount: uint32(count),
|
||||
RoleArnsPreview: preview,
|
||||
RoleArnsCount: uint32(len(roleARNs)),
|
||||
RoleArnsPreview: previewStrings(roleARNs, MaxAuditRoleARNPreview),
|
||||
},
|
||||
}
|
||||
case *types.ResourceConstraints_Ssh:
|
||||
if d.Ssh == nil {
|
||||
out.Constraints = &ResourceAccessID_UnknownConstraints{UnknownConstraints: &UnknownConstraints{}}
|
||||
break
|
||||
}
|
||||
logins := d.Ssh.Logins
|
||||
out.Constraints = &ResourceAccessID_Ssh{
|
||||
Ssh: &SSHConstraints{
|
||||
LoginsCount: uint32(len(logins)),
|
||||
LoginsPreview: previewStrings(logins, MaxAuditRoleARNPreview),
|
||||
},
|
||||
}
|
||||
// Unknown/unsupported constraint variant
|
||||
default:
|
||||
out.Constraints = &ResourceAccessID_UnknownConstraints{UnknownConstraints: &UnknownConstraints{}}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
Copyright 2026 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
)
|
||||
|
||||
func TestToEventResourceAccessID(t *testing.T) {
|
||||
id := types.ResourceID{ClusterName: "example.teleport.sh", Kind: "node", Name: "server-01"}
|
||||
eventID := ResourceID{ClusterName: "example.teleport.sh", Kind: "node", Name: "server-01"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in types.ResourceAccessID
|
||||
wantConstraints isResourceAccessID_Constraints
|
||||
}{
|
||||
{
|
||||
name: "no constraints",
|
||||
in: types.ResourceAccessID{Id: id},
|
||||
wantConstraints: nil,
|
||||
},
|
||||
{
|
||||
name: "aws console constraints",
|
||||
in: types.ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &types.ResourceConstraints{
|
||||
Details: &types.ResourceConstraints_AwsConsole{
|
||||
AwsConsole: &types.AWSConsoleResourceConstraints{
|
||||
RoleArns: []string{"arn:aws:iam::123:role/A", "arn:aws:iam::123:role/B"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantConstraints: &ResourceAccessID_AwsConsole{
|
||||
AwsConsole: &AWSConsoleConstraints{
|
||||
RoleArnsCount: 2,
|
||||
RoleArnsPreview: []string{"arn:aws:iam::123:role/A", "arn:aws:iam::123:role/B"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws console constraints truncated to preview limit",
|
||||
in: types.ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &types.ResourceConstraints{
|
||||
Details: &types.ResourceConstraints_AwsConsole{
|
||||
AwsConsole: &types.AWSConsoleResourceConstraints{
|
||||
RoleArns: slices.Repeat([]string{"arn:aws:iam::123456789012:role/Role"}, MaxAuditRoleARNPreview+5),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantConstraints: &ResourceAccessID_AwsConsole{
|
||||
AwsConsole: &AWSConsoleConstraints{
|
||||
RoleArnsCount: MaxAuditRoleARNPreview + 5,
|
||||
RoleArnsPreview: slices.Repeat([]string{"arn:aws:iam::123456789012:role/Role"}, MaxAuditRoleARNPreview),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ssh constraints",
|
||||
in: types.ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &types.ResourceConstraints{
|
||||
Details: &types.ResourceConstraints_Ssh{
|
||||
Ssh: &types.SSHResourceConstraints{
|
||||
Logins: []string{"alice", "root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantConstraints: &ResourceAccessID_Ssh{
|
||||
Ssh: &SSHConstraints{LoginsCount: 2, LoginsPreview: []string{"alice", "root"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown constraint type falls back to unknown_constraints",
|
||||
in: types.ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &types.ResourceConstraints{},
|
||||
},
|
||||
wantConstraints: &ResourceAccessID_UnknownConstraints{UnknownConstraints: &UnknownConstraints{}},
|
||||
},
|
||||
{
|
||||
name: "aws console with nil payload falls back to unknown_constraints",
|
||||
in: types.ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &types.ResourceConstraints{
|
||||
Details: &types.ResourceConstraints_AwsConsole{AwsConsole: nil},
|
||||
},
|
||||
},
|
||||
wantConstraints: &ResourceAccessID_UnknownConstraints{UnknownConstraints: &UnknownConstraints{}},
|
||||
},
|
||||
{
|
||||
name: "ssh with nil payload falls back to unknown_constraints",
|
||||
in: types.ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &types.ResourceConstraints{
|
||||
Details: &types.ResourceConstraints_Ssh{Ssh: nil},
|
||||
},
|
||||
},
|
||||
wantConstraints: &ResourceAccessID_UnknownConstraints{UnknownConstraints: &UnknownConstraints{}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ToEventResourceAccessID(tt.in)
|
||||
require.Equal(t, eventID, got.Id)
|
||||
require.Equal(t, tt.wantConstraints, got.Constraints)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceAccessIDJSONRoundTrip is the regression test for the audit-log
|
||||
// read path. Events are stored as JSON in the file log and decoded via
|
||||
// FastUnmarshal on read. The protobuf one of generates an interface field
|
||||
// (isResourceAccessID_Constraints) that encoding/json cannot unmarshal into.
|
||||
// MarshalJSON/UnmarshalJSON on ResourceAccessID fix this by using an intermediate struct.
|
||||
func TestResourceAccessIDJSONRoundTrip(t *testing.T) {
|
||||
id := ResourceID{ClusterName: "example.teleport.sh", Kind: "node", Name: "server-01"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in ResourceAccessID
|
||||
}{
|
||||
{
|
||||
name: "no constraints",
|
||||
in: ResourceAccessID{Id: id},
|
||||
},
|
||||
{
|
||||
name: "unknown constraints",
|
||||
in: ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &ResourceAccessID_UnknownConstraints{UnknownConstraints: &UnknownConstraints{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws console constraints",
|
||||
in: ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &ResourceAccessID_AwsConsole{
|
||||
AwsConsole: &AWSConsoleConstraints{
|
||||
RoleArnsCount: 3,
|
||||
RoleArnsPreview: []string{"arn:aws:iam::123:role/A", "arn:aws:iam::123:role/B"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ssh constraints",
|
||||
in: ResourceAccessID{
|
||||
Id: id,
|
||||
Constraints: &ResourceAccessID_Ssh{
|
||||
Ssh: &SSHConstraints{LoginsCount: 2, LoginsPreview: []string{"alice", "root"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := json.Marshal(&tt.in)
|
||||
require.NoError(t, err)
|
||||
|
||||
var got ResourceAccessID
|
||||
require.NoError(t, json.Unmarshal(data, &got))
|
||||
|
||||
require.Equal(t, tt.in.Id, got.Id)
|
||||
require.Equal(t, tt.in.Constraints, got.Constraints)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user