mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
Add dynamic marshalers for CertAuthorityOverride (#65858)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user