Skip to content

Commit c900a70

Browse files
committed
adding local service implementation for recording encryption resources
1 parent 32e26ff commit c900a70

File tree

6 files changed

+268
-1
lines changed

6 files changed

+268
-1
lines changed

api/types/constants.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,20 @@ const (
303303
// KindSessionRecordingConfig is the resource for session recording configuration.
304304
KindSessionRecordingConfig = "session_recording_config"
305305

306+
// KindRecordingEncryption is the collection of active session recording encryption keys.
307+
KindRecordingEncryption = "recording_encryption"
308+
309+
// KindRotatedKeys is a collection of rotated keys identified by public key.
310+
KindRotatedKeys = "rotated_keys"
311+
306312
// MetaNameSessionRecordingConfig is the exact name of the singleton resource for
307313
// session recording configuration.
308314
MetaNameSessionRecordingConfig = "session-recording-config"
309315

316+
// MetaNameRecordingEncryption is the exact name of the singleton resource for
317+
// session recording configuration.
318+
MetaNameRecordingEncryption = "recording-encryption"
319+
310320
// KindExternalAuditStorage the resource kind for External Audit Storage
311321
// configuration.
312322
KindExternalAuditStorage = "external_audit_storage"

api/types/resource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ func GetRevision(v any) (string, error) {
746746
case Resource:
747747
return r.GetRevision(), nil
748748
case ResourceMetadata:
749-
return r.GetMetadata().Revision, nil
749+
return r.GetMetadata().GetRevision(), nil
750750
}
751751
return "", trace.BadParameter("unable to determine revision from resource of type %T", v)
752752
}

lib/services/local/events.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ func (e *EventsService) NewWatcher(ctx context.Context, watch types.Watch) (type
260260
parser = newWorkloadIdentityX509RevocationParser()
261261
case types.KindHealthCheckConfig:
262262
parser = newHealthCheckConfigParser()
263+
case types.KindRecordingEncryption:
264+
parser = newRecordingEncryptionParser()
263265
default:
264266
if watch.AllowPartialSuccess {
265267
continue
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Teleport
2+
// Copyright (C) 2025 Gravitational, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package local
18+
19+
import (
20+
"context"
21+
22+
"github.com/gravitational/trace"
23+
24+
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
25+
recordingencryptionv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
26+
"github.com/gravitational/teleport/api/types"
27+
"github.com/gravitational/teleport/lib/backend"
28+
"github.com/gravitational/teleport/lib/services"
29+
"github.com/gravitational/teleport/lib/services/local/generic"
30+
)
31+
32+
const (
33+
recordingEncryptionPrefix = "recording_encryption"
34+
recordingEncryptionConfigPrefix = "config"
35+
)
36+
37+
// RecordingEncryptionService exposes backend functionality for working with the
38+
// cluster's RecordingEncryption resource.
39+
type RecordingEncryptionService struct {
40+
encryption *generic.ServiceWrapper[*recordingencryptionv1.RecordingEncryption]
41+
}
42+
43+
var _ services.RecordingEncryption = (*RecordingEncryptionService)(nil)
44+
45+
// NewRecordingEncryptionService creates a new RecordingEncryptionService.
46+
func NewRecordingEncryptionService(b backend.Backend) (*RecordingEncryptionService, error) {
47+
const pageLimit = 100
48+
encryption, err := generic.NewServiceWrapper(generic.ServiceConfig[*recordingencryptionv1.RecordingEncryption]{
49+
Backend: b,
50+
PageLimit: pageLimit,
51+
ResourceKind: types.KindRecordingEncryption,
52+
BackendPrefix: backend.NewKey(recordingEncryptionPrefix),
53+
MarshalFunc: services.MarshalProtoResource[*recordingencryptionv1.RecordingEncryption],
54+
UnmarshalFunc: services.UnmarshalProtoResource[*recordingencryptionv1.RecordingEncryption],
55+
})
56+
if err != nil {
57+
return nil, trace.Wrap(err)
58+
}
59+
60+
return &RecordingEncryptionService{
61+
encryption: encryption,
62+
}, nil
63+
}
64+
65+
// CreateRecordingEncryption creates a new RecordingEncryption in the backend.
66+
func (s *RecordingEncryptionService) CreateRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error) {
67+
if encryption.Metadata == nil {
68+
encryption.Metadata = &headerv1.Metadata{}
69+
}
70+
encryption.Metadata.Name = recordingEncryptionConfigPrefix
71+
created, err := s.encryption.CreateResource(ctx, encryption)
72+
return created, trace.Wrap(err)
73+
}
74+
75+
// UpdateRecordingEncryption replaces the RecordingEncryption resource with the given one.
76+
func (s *RecordingEncryptionService) UpdateRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error) {
77+
if encryption.Metadata == nil {
78+
encryption.Metadata = &headerv1.Metadata{}
79+
}
80+
encryption.Metadata.Name = recordingEncryptionConfigPrefix
81+
updated, err := s.encryption.ConditionalUpdateResource(ctx, encryption)
82+
return updated, trace.Wrap(err)
83+
}
84+
85+
// DeleteRecordingEncryption removes the RecordingEncryption from the cluster.
86+
func (s *RecordingEncryptionService) DeleteRecordingEncryption(ctx context.Context) error {
87+
return trace.Wrap(s.encryption.DeleteResource(ctx, recordingEncryptionConfigPrefix))
88+
}
89+
90+
// GetRecordingEncryption retrieves the RecordingEncryption for the cluster.
91+
func (s *RecordingEncryptionService) GetRecordingEncryption(ctx context.Context) (*recordingencryptionv1.RecordingEncryption, error) {
92+
encryption, err := s.encryption.GetResource(ctx, recordingEncryptionConfigPrefix)
93+
return encryption, trace.Wrap(err)
94+
}
95+
96+
type recordingEncryptionParser struct {
97+
baseParser
98+
}
99+
100+
func newRecordingEncryptionParser() *recordingEncryptionParser {
101+
return &recordingEncryptionParser{
102+
baseParser: newBaseParser(backend.NewKey(recordingEncryptionPrefix, recordingEncryptionConfigPrefix)),
103+
}
104+
}
105+
106+
func (p *recordingEncryptionParser) parse(event backend.Event) (types.Resource, error) {
107+
switch event.Type {
108+
case types.OpPut:
109+
resource, err := services.UnmarshalProtoResource[*recordingencryptionv1.RecordingEncryption](
110+
event.Item.Value,
111+
services.WithExpires(event.Item.Expires),
112+
services.WithRevision(event.Item.Revision),
113+
)
114+
if err != nil {
115+
return nil, trace.Wrap(err, "unmarshalling resource from event")
116+
}
117+
return types.Resource153ToLegacy(resource), nil
118+
case types.OpDelete:
119+
resource, err := services.UnmarshalProtoResource[*recordingencryptionv1.RecordingEncryption](
120+
event.Item.Value,
121+
services.WithExpires(event.Item.Expires),
122+
services.WithRevision(event.Item.Revision),
123+
)
124+
if err != nil {
125+
return nil, trace.Wrap(err, "unmarshalling resource from event")
126+
}
127+
return types.Resource153ToLegacy(resource), nil
128+
default:
129+
return nil, trace.BadParameter("event %v is not supported", event.Type)
130+
}
131+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Teleport
2+
// Copyright (C) 2025 Gravitational, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package local
18+
19+
import (
20+
"context"
21+
"crypto"
22+
"testing"
23+
24+
"github.com/stretchr/testify/require"
25+
26+
pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
27+
"github.com/gravitational/teleport/api/types"
28+
"github.com/gravitational/teleport/lib/backend"
29+
"github.com/gravitational/teleport/lib/backend/memory"
30+
)
31+
32+
func TestRecordingEncryption(t *testing.T) {
33+
bk, err := memory.New(memory.Config{})
34+
require.NoError(t, err)
35+
service, err := NewRecordingEncryptionService(backend.NewSanitizer(bk))
36+
require.NoError(t, err)
37+
38+
ctx := context.Background()
39+
40+
initialEncryption := pb.RecordingEncryption{
41+
Spec: &pb.RecordingEncryptionSpec{
42+
ActiveKeys: nil,
43+
},
44+
}
45+
46+
// get should fail when there's no recording encryption
47+
_, err = service.GetRecordingEncryption(ctx)
48+
require.Error(t, err)
49+
50+
created, err := service.CreateRecordingEncryption(ctx, &initialEncryption)
51+
require.NoError(t, err)
52+
53+
encryption, err := service.GetRecordingEncryption(ctx)
54+
require.NoError(t, err)
55+
56+
require.Len(t, created.Spec.ActiveKeys, 0)
57+
require.Len(t, encryption.Spec.ActiveKeys, 0)
58+
59+
encryption.Spec.ActiveKeys = []*pb.WrappedKey{
60+
{
61+
RecordingEncryptionPair: &types.EncryptionKeyPair{
62+
PrivateKey: []byte("recording encryption private"),
63+
PublicKey: []byte("recording encryption public"),
64+
Hash: 0,
65+
},
66+
KeyEncryptionPair: &types.EncryptionKeyPair{
67+
PrivateKey: []byte("key encryption private"),
68+
PublicKey: []byte("key encryption public"),
69+
Hash: uint32(crypto.SHA256),
70+
},
71+
},
72+
}
73+
74+
updated, err := service.UpdateRecordingEncryption(ctx, encryption)
75+
require.NoError(t, err)
76+
require.Len(t, updated.Spec.ActiveKeys, 1)
77+
require.EqualExportedValues(t, encryption.Spec.ActiveKeys[0], updated.Spec.ActiveKeys[0])
78+
79+
encryption, err = service.GetRecordingEncryption(ctx)
80+
require.NoError(t, err)
81+
require.Len(t, encryption.Spec.ActiveKeys, 1)
82+
require.EqualExportedValues(t, updated.Spec.ActiveKeys[0], encryption.Spec.ActiveKeys[0])
83+
84+
err = service.DeleteRecordingEncryption(ctx)
85+
require.NoError(t, err)
86+
_, err = service.GetRecordingEncryption(ctx)
87+
require.Error(t, err)
88+
}

lib/services/recording_encryption.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Teleport
2+
// Copyright (C) 2025 Gravitational, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package services
18+
19+
import (
20+
"context"
21+
22+
recordingencryptionv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/recordingencryption/v1"
23+
)
24+
25+
// RecordingEncryption handles CRUD operations for RecordingEncryption and RotatedKeys resources.
26+
type RecordingEncryption interface {
27+
// CreateRecordingEncryption creates a new RecordingEncryption in the backend if one
28+
// does not already exist.
29+
CreateRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error)
30+
// UpdateRecordingEncryption replaces the RecordingEncryption resource with the given one.
31+
UpdateRecordingEncryption(ctx context.Context, encryption *recordingencryptionv1.RecordingEncryption) (*recordingencryptionv1.RecordingEncryption, error)
32+
// DeleteRecordingEncryption removes the RecordingEncryption from the cluster.
33+
DeleteRecordingEncryption(ctx context.Context) error
34+
// GetRecordingEncryption retrieves the RecordingEncryption for the cluster.
35+
GetRecordingEncryption(ctx context.Context) (*recordingencryptionv1.RecordingEncryption, error)
36+
}

0 commit comments

Comments
 (0)