mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
support where RBAC on application session recordings (#69630)
* support `where` RBAC on application session recordings application session recordings are backed by AppSessionChunk events, but those events were never wired into the session recording RBAC path, so `where` clauses on `kind: session` rules could not match them. Fixes https://github.com/gravitational/teleport/issues/67775 Signed-off-by: Tiago Silva <[email protected]> * fix unit test * handle feedback --------- Signed-off-by: Tiago Silva <[email protected]>
This commit is contained in:
@@ -3279,6 +3279,10 @@ message AppSessionChunk {
|
||||
(gogoproto.embed) = true,
|
||||
(gogoproto.jsontag) = ""
|
||||
];
|
||||
// Participants is the list of participants in a session.
|
||||
// App sessions only have one participant but we keep it a repeated field
|
||||
// to keep compatibility with other session end events.
|
||||
repeated string Participants = 8 [(gogoproto.jsontag) = "participants,omitempty"];
|
||||
}
|
||||
|
||||
// AppSessionRequest is an HTTP request and response.
|
||||
|
||||
@@ -126,4 +126,6 @@ const (
|
||||
EventProtocolTDP = "tdp"
|
||||
// EventProtocolDB specifies database as a type of captured protocol
|
||||
EventProtocolDB = "db"
|
||||
// EventProtocolApp specifies app as a type of captured protocol
|
||||
EventProtocolApp = "app"
|
||||
)
|
||||
|
||||
+1633
-1582
File diff suppressed because it is too large
Load Diff
@@ -595,9 +595,8 @@ resource type and the session creator (user who initiated the session) roles and
|
||||
| Session Field | Description | Type |
|
||||
|---|---|---|
|
||||
| `session.id` | The unique identifier of the session. | `string` |
|
||||
| `session.kind` | The kind of session (e.g., "ssh", "kube", "db", "rdp"). | `string` |
|
||||
| `session.participants` | The list of users who participated in the session. | `array<string>` |
|
||||
| `session.proto` | The proto used for the session (e.g., "ssh", "kube", "db", "rdp"). | `string` |
|
||||
| `session.proto` | The proto used for the session (e.g., "ssh", "kube", "db", "tdp", "app"). | `string` |
|
||||
| `session.user` | The user who initiated the session. | `string` |
|
||||
| `session.user_roles` | The roles assigned to the user who initiated the session. | `array<string>` |
|
||||
| `session.user_traits` | The traits assigned to the user who initiated the session. | `dict<string, array<string>>` |
|
||||
@@ -669,6 +668,22 @@ for Windows Desktop sessions together with the user and session fields above.
|
||||
| `session.windows_user` | The username used to log in to the Windows desktop during the session. | `string` |
|
||||
| `session.desktop_labels` | The labels assigned to the Windows desktop where the session took place. | `dict<string, string>` |
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="Application Sessions" label="Application Sessions">
|
||||
|
||||
The following application session fields can be used in `where` expressions to filter access
|
||||
for application sessions together with the user and session fields above.
|
||||
|
||||
| Application Session Field | Description | Type |
|
||||
|---|---|---|
|
||||
| `session.app_name` | The name of the application where the session took place. | `string` |
|
||||
| `session.app_uri` | The endpoint of the application where the session took place. | `string` |
|
||||
| `session.app_public_addr` | The public address of the application where the session took place. | `string` |
|
||||
| `session.app_labels` | The labels assigned to the application where the session took place. | `dict<string, string>` |
|
||||
|
||||
Only HTTP/HTTPS applications produce session recordings, so these fields are not
|
||||
available for TCP applications.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -753,6 +753,7 @@ func testAuditEvents(p *Pack, t *testing.T) {
|
||||
AppPublicAddr: p.rootAppPublicAddr,
|
||||
AppName: p.rootAppName,
|
||||
},
|
||||
Participants: []string{p.username},
|
||||
}
|
||||
return len(cmp.Diff(
|
||||
expectedEvent,
|
||||
@@ -760,6 +761,7 @@ func testAuditEvents(p *Pack, t *testing.T) {
|
||||
cmpopts.IgnoreTypes(apievents.ServerMetadata{}, apievents.SessionMetadata{}, apievents.UserMetadata{}, apievents.ConnectionMetadata{}),
|
||||
cmpopts.IgnoreFields(apievents.Metadata{}, "ID", "Time", "Index"),
|
||||
cmpopts.IgnoreFields(apievents.AppSessionChunk{}, "SessionChunkID"),
|
||||
cmpopts.IgnoreFields(apievents.AppMetadata{}, "AppLabels"),
|
||||
)) == 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -843,10 +843,10 @@ func (p *Pack) startRootAppServers(t *testing.T, count int, opts AppTestOptions)
|
||||
URI: p.rootTCPMultiPortAppURI,
|
||||
PublicAddr: p.rootTCPMultiPortPublicAddr,
|
||||
TCPPorts: []servicecfg.PortRange{
|
||||
servicecfg.PortRange{
|
||||
{
|
||||
Port: p.rootTCPMultiPortAppPortAlpha,
|
||||
},
|
||||
servicecfg.PortRange{
|
||||
{
|
||||
Port: p.rootTCPMultiPortAppPortBeta,
|
||||
},
|
||||
},
|
||||
@@ -1008,10 +1008,10 @@ func (p *Pack) startLeafAppServers(t *testing.T, count int, opts AppTestOptions)
|
||||
URI: p.leafTCPMultiPortAppURI,
|
||||
PublicAddr: p.leafTCPMultiPortPublicAddr,
|
||||
TCPPorts: []servicecfg.PortRange{
|
||||
servicecfg.PortRange{
|
||||
{
|
||||
Port: p.leafTCPMultiPortAppPortAlpha,
|
||||
},
|
||||
servicecfg.PortRange{
|
||||
{
|
||||
Port: p.leafTCPMultiPortAppPortBeta,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -60,6 +60,8 @@ const (
|
||||
EventProtocolTDP = apievents.EventProtocolTDP
|
||||
// EventProtocolDB specifies database as a type of captured protocol
|
||||
EventProtocolDB = apievents.EventProtocolDB
|
||||
// EventProtocolApp specifies app as a type of captured protocol
|
||||
EventProtocolApp = apievents.EventProtocolApp
|
||||
// LocalAddr is a target address on the host
|
||||
LocalAddr = "addr.local"
|
||||
// RemoteAddr is a client (user's) address
|
||||
|
||||
@@ -480,7 +480,7 @@ func (ctx *Context) GetIdentifier(fields []string) (any, error) {
|
||||
case SessionIdentifier:
|
||||
var session events.AuditEvent = &events.SessionEnd{}
|
||||
switch ctx.Session.(type) {
|
||||
case *events.SessionEnd, *events.WindowsDesktopSessionEnd, *events.LinuxDesktopSessionEnd, *events.DatabaseSessionEnd:
|
||||
case *events.SessionEnd, *events.WindowsDesktopSessionEnd, *events.LinuxDesktopSessionEnd, *events.DatabaseSessionEnd, *events.AppSessionChunk:
|
||||
session = ctx.Session
|
||||
}
|
||||
v, origErr := predicate.GetFieldByTag(session, teleport.JSON, fields[1:])
|
||||
@@ -537,7 +537,7 @@ func (ctx *Context) GetIdentifier(fields []string) (any, error) {
|
||||
}
|
||||
|
||||
func getMissingEmptyFieldForSessionEnd(fields []string) (any, error) {
|
||||
for _, emptySession := range []events.AuditEvent{&events.SessionEnd{}, &events.WindowsDesktopSessionEnd{}, &events.DatabaseSessionEnd{}} {
|
||||
for _, emptySession := range []events.AuditEvent{&events.SessionEnd{}, &events.WindowsDesktopSessionEnd{}, &events.DatabaseSessionEnd{}, &events.AppSessionChunk{}} {
|
||||
v, err := predicate.GetFieldByTag(emptySession, teleport.JSON, fields[1:])
|
||||
if err == nil {
|
||||
return v, nil
|
||||
|
||||
@@ -11245,6 +11245,9 @@ func TestSessionRecordingRBAC(t *testing.T) {
|
||||
NodeLabels: types.Labels{
|
||||
"env": []string{"prod"},
|
||||
},
|
||||
AppLabels: types.Labels{
|
||||
"env": []string{"prod"},
|
||||
},
|
||||
Rules: allowRules,
|
||||
},
|
||||
Deny: types.RoleConditions{
|
||||
@@ -11270,6 +11273,25 @@ func TestSessionRecordingRBAC(t *testing.T) {
|
||||
"env": "dev",
|
||||
})
|
||||
|
||||
newApp := func(name string, labels map[string]string) types.Application {
|
||||
app, err := types.NewAppV3(types.Metadata{
|
||||
Name: name,
|
||||
Labels: labels,
|
||||
}, types.AppSpecV3{
|
||||
URI: "http://localhost:8080",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return app
|
||||
}
|
||||
|
||||
appWithAccess := newApp("app-with-access", map[string]string{
|
||||
"env": "prod",
|
||||
})
|
||||
|
||||
appWithoutAccess := newApp("app-without-access", map[string]string{
|
||||
"env": "dev",
|
||||
})
|
||||
|
||||
alice := newUser(
|
||||
"alice",
|
||||
map[string][]string{
|
||||
@@ -11764,6 +11786,70 @@ func TestSessionRecordingRBAC(t *testing.T) {
|
||||
},
|
||||
errCheck: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "24 - bob can read app session because his group matches with env label for app",
|
||||
roles: RoleSet{
|
||||
newRole(&types.Rule{
|
||||
Resources: []string{types.KindSession},
|
||||
Verbs: []string{types.VerbRead},
|
||||
Where: `contains(user.spec.traits["group"], session.app_labels["env"])`,
|
||||
}, nil),
|
||||
},
|
||||
context: Context{
|
||||
User: bob,
|
||||
Session: newAppSessionChunkEvent(),
|
||||
Resource: appWithAccess,
|
||||
},
|
||||
errCheck: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "25 - john can not read app session because his group does not match with env label for app",
|
||||
roles: RoleSet{
|
||||
newRole(&types.Rule{
|
||||
Resources: []string{types.KindSession},
|
||||
Verbs: []string{types.VerbRead},
|
||||
Where: `contains(user.spec.traits["group"], session.app_labels["env"])`,
|
||||
}, nil),
|
||||
},
|
||||
context: Context{
|
||||
User: john,
|
||||
Session: newAppSessionChunkEvent(),
|
||||
Resource: appWithAccess,
|
||||
},
|
||||
errCheck: requireAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "26 - bob can read app session because he has access to the app",
|
||||
roles: RoleSet{
|
||||
newRole(&types.Rule{
|
||||
Resources: []string{types.KindSession},
|
||||
Verbs: []string{types.VerbRead},
|
||||
Where: `can_view()`,
|
||||
}, nil),
|
||||
},
|
||||
context: Context{
|
||||
User: bob,
|
||||
Session: newAppSessionChunkEvent(),
|
||||
Resource: appWithAccess,
|
||||
},
|
||||
errCheck: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "27 - bob can not read app session because he has no access to the app",
|
||||
roles: RoleSet{
|
||||
newRole(&types.Rule{
|
||||
Resources: []string{types.KindSession},
|
||||
Verbs: []string{types.VerbRead},
|
||||
Where: `can_view()`,
|
||||
}, nil),
|
||||
},
|
||||
context: Context{
|
||||
User: bob,
|
||||
Session: newAppSessionChunkEvent(),
|
||||
Resource: appWithoutAccess,
|
||||
},
|
||||
errCheck: requireAccessDenied,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -11865,6 +11951,37 @@ func newDatabaseSessionEndEvent() *apievents.DatabaseSessionEnd {
|
||||
}
|
||||
}
|
||||
|
||||
func newAppSessionChunkEvent() *apievents.AppSessionChunk {
|
||||
return &apievents.AppSessionChunk{
|
||||
Metadata: apievents.Metadata{
|
||||
Index: 20,
|
||||
Type: "app.session.chunk",
|
||||
ID: "da455e0f-c27d-459f-a218-4e83b3db9426",
|
||||
Code: "T2008I",
|
||||
Time: time.Date(2020, 3, 30, 15, 58, 54, 561*int(time.Millisecond), time.UTC),
|
||||
},
|
||||
AppMetadata: apievents.AppMetadata{
|
||||
AppName: "app-with-access",
|
||||
AppURI: "http://localhost:8080",
|
||||
AppPublicAddr: "app.example.com",
|
||||
AppLabels: map[string]string{"env": "prod"},
|
||||
},
|
||||
ConnectionMetadata: apievents.ConnectionMetadata{
|
||||
Protocol: apievents.EventProtocolApp,
|
||||
},
|
||||
UserMetadata: apievents.UserMetadata{
|
||||
User: "alice",
|
||||
UserRoles: []string{"role1"},
|
||||
UserTraits: map[string][]string{
|
||||
"group": {"prod"},
|
||||
"team": {"engineering"},
|
||||
},
|
||||
},
|
||||
Participants: []string{"alice"},
|
||||
SessionChunkID: "cb116fb0-9227-4889-9392-aedd13a914a6",
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSubmitForUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -106,6 +106,23 @@ func rebuildResourceFromSessionEndEvent(event apievents.AuditEvent) types.Resour
|
||||
Domain: sEnd.Domain,
|
||||
},
|
||||
}
|
||||
case *apievents.AppSessionChunk:
|
||||
if sEnd == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.AppV3{
|
||||
Kind: types.KindApp,
|
||||
Version: types.V3,
|
||||
Metadata: types.Metadata{
|
||||
Name: sEnd.AppName,
|
||||
Namespace: apidefaults.Namespace,
|
||||
Labels: sEnd.AppLabels,
|
||||
},
|
||||
Spec: types.AppSpecV3{
|
||||
URI: sEnd.AppURI,
|
||||
PublicAddr: sEnd.AppPublicAddr,
|
||||
},
|
||||
}
|
||||
case *apievents.DatabaseSessionEnd:
|
||||
if sEnd == nil {
|
||||
return nil
|
||||
|
||||
@@ -53,6 +53,11 @@ func TestRebuildResourceFromSessionEndEvent(t *testing.T) {
|
||||
event: (*apievents.DatabaseSessionEnd)(nil),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "nil with type set app.session.chunk",
|
||||
event: (*apievents.AppSessionChunk)(nil),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "non session.end event",
|
||||
event: &apievents.UserLogin{
|
||||
@@ -188,6 +193,36 @@ func TestRebuildResourceFromSessionEndEvent(t *testing.T) {
|
||||
Spec: types.DatabaseSpecV3{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "app session chunk",
|
||||
event: &apievents.AppSessionChunk{
|
||||
AppMetadata: apievents.AppMetadata{
|
||||
AppName: "app-name",
|
||||
AppURI: "http://localhost:8080",
|
||||
AppPublicAddr: "app.example.com",
|
||||
AppLabels: map[string]string{
|
||||
"env": "prod",
|
||||
"team": "web",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &types.AppV3{
|
||||
Kind: types.KindApp,
|
||||
Version: types.V3,
|
||||
Metadata: types.Metadata{
|
||||
Name: "app-name",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"env": "prod",
|
||||
"team": "web",
|
||||
},
|
||||
},
|
||||
Spec: types.AppSpecV3{
|
||||
URI: "http://localhost:8080",
|
||||
PublicAddr: "app.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -29,7 +29,9 @@ import (
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
apievents "github.com/gravitational/teleport/api/types/events"
|
||||
"github.com/gravitational/teleport/lib/authz"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
awsutils "github.com/gravitational/teleport/lib/utils/aws"
|
||||
)
|
||||
@@ -156,6 +158,14 @@ func (a *audit) OnSessionEnd(ctx context.Context, serverID string, identity *tls
|
||||
|
||||
// OnSessionChunk is called when a new session chunk is created.
|
||||
func (a *audit) OnSessionChunk(ctx context.Context, serverID, chunkID string, identity *tlsca.Identity, app types.Application) error {
|
||||
// Enforce IP Pinning if it is present in the user's certificate.
|
||||
var clientAddr string
|
||||
if clientSrcAddr, err := authz.ClientSrcAddrFromContext(ctx); err == nil {
|
||||
clientAddr = clientSrcAddr.String()
|
||||
} else {
|
||||
clientAddr = identity.LoginIP
|
||||
}
|
||||
|
||||
event := &apievents.AppSessionChunk{
|
||||
Metadata: apievents.Metadata{
|
||||
Type: events.AppSessionChunkEvent,
|
||||
@@ -167,15 +177,28 @@ func (a *audit) OnSessionChunk(ctx context.Context, serverID, chunkID string, id
|
||||
ServerID: serverID,
|
||||
ServerNamespace: apidefaults.Namespace,
|
||||
},
|
||||
ConnectionMetadata: apievents.ConnectionMetadata{
|
||||
LocalAddr: clientAddr,
|
||||
RemoteAddr: app.GetURI(),
|
||||
Protocol: events.EventProtocolApp,
|
||||
},
|
||||
SessionMetadata: getSessionMetadata(identity),
|
||||
UserMetadata: identity.GetUserMetadata(),
|
||||
AppMetadata: apievents.AppMetadata{
|
||||
AppURI: app.GetURI(),
|
||||
AppPublicAddr: app.GetPublicAddr(),
|
||||
AppName: app.GetName(),
|
||||
AppLabels: app.GetAllLabels(),
|
||||
// Session chunks are not created for TCP apps, so there's no need to pass TargetPort here.
|
||||
},
|
||||
SessionChunkID: chunkID,
|
||||
Participants: []string{services.UsernameForCluster(
|
||||
services.UsernameForClusterConfig{
|
||||
User: identity.Username,
|
||||
OriginClusterName: identity.OriginClusterName,
|
||||
LocalClusterName: identity.TeleportCluster,
|
||||
},
|
||||
)},
|
||||
}
|
||||
return trace.Wrap(a.EmitEvent(ctx, event))
|
||||
}
|
||||
|
||||
@@ -644,7 +644,6 @@ func TestShutdown(t *testing.T) {
|
||||
return
|
||||
}
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1037,7 +1036,6 @@ func TestRewriteJWT(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestAuthorize verifies that only authorized requests are handled.
|
||||
@@ -1293,7 +1291,12 @@ func TestRequestAuditEvents(t *testing.T) {
|
||||
AppURI: app.Spec.URI,
|
||||
AppPublicAddr: app.Spec.PublicAddr,
|
||||
AppName: app.Metadata.Name,
|
||||
AppLabels: map[string]string{
|
||||
"bar": "baz",
|
||||
"qux": "4",
|
||||
},
|
||||
},
|
||||
Participants: []string{"foo"},
|
||||
}
|
||||
require.Empty(t, gocmp.Diff(
|
||||
expectedEvent,
|
||||
@@ -1370,7 +1373,12 @@ func TestRequestAuditEvents(t *testing.T) {
|
||||
AppURI: app.Spec.URI,
|
||||
AppPublicAddr: app.Spec.PublicAddr,
|
||||
AppName: app.Metadata.Name,
|
||||
AppLabels: map[string]string{
|
||||
"bar": "baz",
|
||||
"qux": "4",
|
||||
},
|
||||
},
|
||||
Participants: []string{"foo"},
|
||||
}
|
||||
require.Empty(t, gocmp.Diff(
|
||||
expectedEvent,
|
||||
|
||||
Reference in New Issue
Block a user