diff --git a/lib/services/local/resource.go b/lib/services/local/resource.go index 8d8d32f3e15..cadb16fb675 100644 --- a/lib/services/local/resource.go +++ b/lib/services/local/resource.go @@ -26,6 +26,7 @@ import ( autoupdatev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1" healthcheckconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/healthcheckconfig/v1" + subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1" workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/backend" @@ -109,6 +110,8 @@ func itemsFromResource(resource types.Resource) ([]backend.Item, error) { item, err = itemFromHealthCheckConfig(r.UnwrapT()) case types.Resource153UnwrapperT[*workloadidentityv1pb.WorkloadIdentity]: item, err = itemFromWorkloadIdentity(r.UnwrapT()) + case types.Resource153UnwrapperT[*subcav1.CertAuthorityOverride]: + item, err = itemFromCertAuthorityOverride(r.UnwrapT()) default: return nil, trace.NotImplemented("cannot itemFrom resource of type %T", resource) } diff --git a/lib/services/local/subca_service.go b/lib/services/local/subca_service.go index 23e2999ab21..b7963919a03 100644 --- a/lib/services/local/subca_service.go +++ b/lib/services/local/subca_service.go @@ -262,3 +262,29 @@ func (p *certAuthorityOverrideParser) parse(event backend.Event) (types.Resource return nil, trace.BadParameter("event %v is not supported", event.Type) } } + +// itemFromCertAuthorityOverride is used by CreateResources. +func itemFromCertAuthorityOverride(resource *subcav1.CertAuthorityOverride) (*backend.Item, error) { + if _, err := subca.ValidateAndParseCAOverride(resource); err != nil { + return nil, trace.Wrap(err) + } + value, err := services.MarshalCertAuthorityOverride(resource) + if err != nil { + return nil, trace.Wrap(err) + } + expires, err := types.GetExpiry(resource) + if err != nil { + return nil, trace.Wrap(err) + } + + key := newCAOverridesPrefix().AppendKey(backend.NewKey( + resource.Metadata.Name, + resource.SubKind, + )) + return &backend.Item{ + Key: key, + Value: value, + Expires: expires, + Revision: resource.Metadata.Revision, + }, nil +} diff --git a/lib/services/local/subca_service_test.go b/lib/services/local/subca_service_test.go index dcb7fe9abfb..7b9ad33d745 100644 --- a/lib/services/local/subca_service_test.go +++ b/lib/services/local/subca_service_test.go @@ -27,6 +27,7 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/backend" @@ -369,3 +370,54 @@ func TestSubCAService_GetDeleteNotFoundError(t *testing.T) { assert.ErrorContains(t, err, wantErr) }) } + +func TestCreateResource_CertAuthorityOverride(t *testing.T) { + t.Parallel() + + env := subcaenv.New(t, subcaenv.EnvParams{ + SkipExternalRoot: true, + }) + be := env.Backend + service := env.SubCA + + t.Run("invalid", func(t *testing.T) { + t.Parallel() + + // An empty resource is not valid. + r := types.Resource153ToLegacy(&subcav1.CertAuthorityOverride{}) + + assert.ErrorAs(t, + local.CreateResources(t.Context(), be, r), + new(*trace.BadParameterError), + "CreateResources error mismatch") + }) + + t.Run("ok", func(t *testing.T) { + t.Parallel() + + want := &subcav1.CertAuthorityOverride{ + Kind: types.KindCertAuthorityOverride, + SubKind: string(types.DatabaseClientCA), + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: env.ClusterName, + }, + Spec: &subcav1.CertAuthorityOverrideSpec{}, + } + + // CreateResources. + r := types.Resource153ToLegacy(want) + require.NoError(t, + local.CreateResources(t.Context(), be, r), + "CreateResources errored") + + // Verify resource via service read. + got, err := service.GetCertAuthorityOverride( + t.Context(), local.CertAuthorityOverrideIDFromResource(want)) + require.NoError(t, err) + want.Metadata.Revision = got.Metadata.Revision + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("CertAuthorityOverride mismatch (-want +got)\n%s", diff) + } + }) +} diff --git a/lib/services/resource.go b/lib/services/resource.go index ca11481fc50..0fec07c0aa4 100644 --- a/lib/services/resource.go +++ b/lib/services/resource.go @@ -35,10 +35,12 @@ import ( headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" healthcheckconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/healthcheckconfig/v1" machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" + subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1" workloadidentityv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" scopedaccess "github.com/gravitational/teleport/lib/scopes/access" + "github.com/gravitational/teleport/lib/subca" "github.com/gravitational/teleport/lib/utils" ) @@ -823,6 +825,40 @@ func init() { } return types.Resource153ToLegacy(wid), nil }) + + // Gate these behind the feature flag, as they have an effect on user-visible product surface + // (ie, "tctl get all"). + if subca.Enabled() { + initSubCA() + } +} + +func initSubCA() { + // TODO(codingllama): Remove this method and inline calls on init() once the + // feature flag is no more. + + RegisterResourceMarshaler(types.KindCertAuthorityOverride, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) { + unwrapper, ok := resource.(types.Resource153UnwrapperT[*subcav1.CertAuthorityOverride]) + if !ok { + return nil, trace.BadParameter("expected wrapped CertAuthorityOverride resource, got %T", resource) + } + caOverride := unwrapper.UnwrapT() + if caOverride == nil { + return nil, trace.BadParameter("nil CertAuthorityOverride resource") + } + bytes, err := MarshalCertAuthorityOverride(caOverride, opts...) + if err != nil { + return nil, trace.Wrap(err) + } + return bytes, nil + }) + RegisterResourceUnmarshaler(types.KindCertAuthorityOverride, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) { + caOverride, err := UnmarshalCertAuthorityOverride(bytes, opts...) + if err != nil { + return nil, trace.Wrap(err) + } + return types.ProtoResource153ToLegacy(caOverride), nil + }) } // CheckAndSetDefaults calls [r.CheckAndSetDefaults] if r implements the method. diff --git a/lib/services/subca_test.go b/lib/services/subca_test.go index 675416dd3de..7678186d22a 100644 --- a/lib/services/subca_test.go +++ b/lib/services/subca_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package services_test +package services import ( "testing" @@ -26,10 +26,11 @@ import ( headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/services" ) -func TestMarshalCertAuthOverrideRoundtrip(t *testing.T) { +func TestMarshalCertAuthorityOverrideRoundtrip(t *testing.T) { + t.Parallel() + want := &subcav1.CertAuthorityOverride{ Kind: types.KindCertAuthorityOverride, SubKind: string(types.DatabaseClientCA), @@ -41,13 +42,33 @@ func TestMarshalCertAuthOverrideRoundtrip(t *testing.T) { } t.Run("ok", func(t *testing.T) { - val, err := services.MarshalCertAuthorityOverride(want) + t.Parallel() + + val, err := MarshalCertAuthorityOverride(want) require.NoError(t, err) - got, err := services.UnmarshalCertAuthorityOverride(val) + got, err := UnmarshalCertAuthorityOverride(val) require.NoError(t, err) if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { t.Errorf("CAOverride mismatch (-want +got)\n%s", diff) } }) + + t.Run("dynamic", func(t *testing.T) { + t.Parallel() + initSubCA() + initSubCA() // check that a duplicate call is harmless + + val, err := MarshalResource(types.Resource153ToLegacy(want)) + require.NoError(t, err) + + wrapped, err := UnmarshalResource(types.KindCertAuthorityOverride, val) + require.NoError(t, err) + unwrapper, ok := wrapped.(types.Resource153UnwrapperT[*subcav1.CertAuthorityOverride]) + require.True(t, ok, "Wrapped resource has unexpected type: %T", wrapped) + got := unwrapper.UnwrapT() + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("CAOverride mismatch (-want +got)\n%s", diff) + } + }) } diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 2981ba5bba6..f812318da5f 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -277,6 +277,15 @@ func (rc *ResourceCommand) Get(ctx context.Context, client *authclient.Client) e } func (rc *ResourceCommand) GetMany(ctx context.Context, client *authclient.Client) error { + const skipNotSupported = false + return trace.Wrap(rc.getMany(ctx, client, skipNotSupported)) +} + +func (rc *ResourceCommand) getMany( + ctx context.Context, + client *authclient.Client, + skipNotSupported bool, +) error { if rc.format != teleport.YAML { return trace.BadParameter("mixed resource types only support YAML formatting") } @@ -285,6 +294,9 @@ func (rc *ResourceCommand) GetMany(ctx context.Context, client *authclient.Clien for _, ref := range rc.refs { rc.ref = ref collection, err := rc.getCollection(ctx, client) + if skipNotSupported && errors.As(err, new(*errNotSupported)) { + continue + } if err != nil { return trace.Wrap(err) } @@ -307,7 +319,12 @@ func (rc *ResourceCommand) GetAll(ctx context.Context, client *authclient.Client allRefs = append(allRefs, ref) } rc.refs = services.Refs(allRefs) - return rc.GetMany(ctx, client) + + // This lets OSS query Enterprise-only kinds without failing when the + // corresponding RPCs return "NotImplemented". + const skipNotSupported = true + + return rc.getMany(ctx, client, skipNotSupported) } // Create updates or inserts one or many resources @@ -842,7 +859,7 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient coll, err := handler.Get(ctx, client, rc.ref, resources.GetOpts{WithSecrets: rc.withSecrets}) if err != nil { if trace.IsNotImplemented(err) { - return nil, trace.BadParameter("getting %q is not supported", rc.ref.String()) + return nil, &errNotSupported{trace.BadParameter("getting %q is not supported", rc.ref.String())} } return nil, trace.Wrap(err, "getting resource %q of type %q", rc.ref.Name, rc.ref.Kind) } @@ -1154,6 +1171,20 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient return nil, trace.BadParameter("getting %q is not supported", rc.ref.String()) } +// errNotSupported is used to mark NotImplemented errors that were transformed +// into BadParameter, so they can later be identified. +type errNotSupported struct { + cause error +} + +func (e *errNotSupported) Error() string { + return e.cause.Error() +} + +func (e *errNotSupported) Unwrap() error { + return e.cause +} + // UpsertVerb generates the correct string form of a verb based on the action taken func UpsertVerb(exists bool, force bool) string { if !force && exists {