From 4bd425c037664958ffa3fd26ad2bb12db08a087b Mon Sep 17 00:00:00 2001 From: Sakshyam Shah Date: Fri, 14 Nov 2025 15:16:03 -0500 Subject: [PATCH] tctl resource: convert saml_idp_service_provider handler (#61386) --- tool/tctl/common/collection.go | 22 --- tool/tctl/common/resource_command.go | 61 ------- tool/tctl/common/resources/resource.go | 1 + .../resources/saml_idp_service_provider.go | 165 ++++++++++++++++++ 4 files changed, 166 insertions(+), 83 deletions(-) create mode 100644 tool/tctl/common/resources/saml_idp_service_provider.go diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index 9ac55497014..279482180d4 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -692,28 +692,6 @@ func (l *loginRuleCollection) Resources() []types.Resource { return resources } -//nolint:revive // Because we want this to be IdP. -type samlIdPServiceProviderCollection struct { - serviceProviders []types.SAMLIdPServiceProvider -} - -func (c *samlIdPServiceProviderCollection) Resources() []types.Resource { - r := make([]types.Resource, len(c.serviceProviders)) - for i, resource := range c.serviceProviders { - r[i] = resource - } - return r -} - -func (c *samlIdPServiceProviderCollection) WriteText(w io.Writer, verbose bool) error { - t := asciitable.MakeTable([]string{"Name"}) - for _, serviceProvider := range c.serviceProviders { - t.AddRow([]string{serviceProvider.GetName()}) - } - _, err := t.AsBuffer().WriteTo(w) - return trace.Wrap(err) -} - type deviceCollection struct { devices []*devicepb.Device } diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 6b3d7c73a18..70a84043d8d 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -33,7 +33,6 @@ import ( "time" "github.com/alecthomas/kingpin/v2" - "github.com/crewjam/saml/samlsp" "github.com/gravitational/trace" kyaml "k8s.io/apimachinery/pkg/util/yaml" @@ -131,7 +130,6 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, _ *tctlcfg.Globa types.KindNetworkRestrictions: rc.createNetworkRestrictions, types.KindKubernetesCluster: rc.createKubeCluster, types.KindLoginRule: rc.createLoginRule, - types.KindSAMLIdPServiceProvider: rc.createSAMLIdPServiceProvider, types.KindDevice: rc.createDevice, types.KindOktaImportRule: rc.createOktaImportRule, types.KindIntegration: rc.createIntegration, @@ -776,46 +774,6 @@ func (rc *ResourceCommand) createLoginRule(ctx context.Context, client *authclie return nil } -func (rc *ResourceCommand) createSAMLIdPServiceProvider(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { - // Create services.SAMLIdPServiceProvider from raw YAML to extract the service provider name. - sp, err := services.UnmarshalSAMLIdPServiceProvider(raw.Raw, services.DisallowUnknown()) - if err != nil { - return trace.Wrap(err) - } - - if sp.GetEntityDescriptor() != "" { - // verify that entity descriptor parses - ed, err := samlsp.ParseMetadata([]byte(sp.GetEntityDescriptor())) - if err != nil { - return trace.BadParameter("invalid entity descriptor for SAML IdP Service Provider %q: %v", sp.GetEntityID(), err) - } - - // issue warning about unsupported ACS bindings. - if err := services.FilterSAMLEntityDescriptor(ed, false /* quiet */); err != nil { - slog.WarnContext(ctx, "Entity descriptor for SAML IdP service provider contains unsupported ACS bindings", - "entity_id", sp.GetEntityID(), - "error", err, - ) - } - } - - serviceProviderName := sp.GetName() - - exists := false - if err = client.CreateSAMLIdPServiceProvider(ctx, sp); err != nil { - if trace.IsAlreadyExists(err) { - exists = true - err = client.UpdateSAMLIdPServiceProvider(ctx, sp) - } - - if err != nil { - return trace.Wrap(err) - } - } - fmt.Printf("SAML IdP service provider %q has been %s\n", serviceProviderName, UpsertVerb(exists, rc.IsForced())) - return nil -} - func (rc *ResourceCommand) createDevice(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error { res, err := services.UnmarshalDevice(raw.Raw) if err != nil { @@ -1204,11 +1162,6 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client return trail.FromGRPC(err) } fmt.Printf("login rule %q has been deleted\n", rc.ref.Name) - case types.KindSAMLIdPServiceProvider: - if err := client.DeleteSAMLIdPServiceProvider(ctx, rc.ref.Name); err != nil { - return trace.Wrap(err) - } - fmt.Printf("SAML IdP service provider %q has been deleted\n", rc.ref.Name) case types.KindDevice: remote := client.DevicesClient() device, err := findDeviceByIDOrTag(ctx, remote, rc.ref.Name) @@ -1667,20 +1620,6 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient Name: rc.ref.Name, }) return &loginRuleCollection{[]*loginrulepb.LoginRule{rule}}, trail.FromGRPC(err) - case types.KindSAMLIdPServiceProvider: - if rc.ref.Name != "" { - serviceProvider, err := client.GetSAMLIdPServiceProvider(ctx, rc.ref.Name) - if err != nil { - return nil, trace.Wrap(err) - } - return &samlIdPServiceProviderCollection{serviceProviders: []types.SAMLIdPServiceProvider{serviceProvider}}, nil - } - - resources, err := stream.Collect(clientutils.Resources(ctx, client.ListSAMLIdPServiceProviders)) - if err != nil { - return nil, trace.Wrap(err) - } - return &samlIdPServiceProviderCollection{serviceProviders: resources}, nil case types.KindDevice: remote := client.DevicesClient() if rc.ref.Name != "" { diff --git a/tool/tctl/common/resources/resource.go b/tool/tctl/common/resources/resource.go index cd809587433..bad90ee7726 100644 --- a/tool/tctl/common/resources/resource.go +++ b/tool/tctl/common/resources/resource.go @@ -63,6 +63,7 @@ func Handlers() map[string]Handler { types.KindProxy: proxyHandler(), types.KindRole: roleHandler(), types.KindSAMLConnector: samlConnectorHandler(), + types.KindSAMLIdPServiceProvider: samlIdPServiceProviderHandler(), types.KindSessionRecordingConfig: sessionRecordingConfigHandler(), types.KindSigstorePolicy: sigstorePolicyHandler(), types.KindSPIFFEFederation: spiffeFederationHandler(), diff --git a/tool/tctl/common/resources/saml_idp_service_provider.go b/tool/tctl/common/resources/saml_idp_service_provider.go new file mode 100644 index 00000000000..83ef4d7fa04 --- /dev/null +++ b/tool/tctl/common/resources/saml_idp_service_provider.go @@ -0,0 +1,165 @@ +/* + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package resources + +import ( + "context" + "fmt" + "io" + "log/slog" + + "github.com/crewjam/saml/samlsp" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/clientutils" + "github.com/gravitational/teleport/lib/asciitable" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/itertools/stream" + "github.com/gravitational/teleport/lib/services" +) + +type samlIdPServiceProviderCollection struct { + serviceProviders []types.SAMLIdPServiceProvider +} + +// Resources returns collection of SAML IdP service provider resource. +func (c *samlIdPServiceProviderCollection) Resources() []types.Resource { + r := make([]types.Resource, len(c.serviceProviders)) + for i, resource := range c.serviceProviders { + r[i] = resource + } + return r +} + +// WriteText writes collection of SAML IdP service provider resource to [w]. +func (c *samlIdPServiceProviderCollection) WriteText(w io.Writer, verbose bool) error { + t := asciitable.MakeTable([]string{"Name"}) + for _, serviceProvider := range c.serviceProviders { + t.AddRow([]string{serviceProvider.GetName()}) + } + _, err := t.AsBuffer().WriteTo(w) + return trace.Wrap(err) +} + +func samlIdPServiceProviderHandler() Handler { + return Handler{ + getHandler: getSAMLIdPServiceProvider, + createHandler: createSAMLIdPServiceProvider, + updateHandler: updateSAMLIdPServiceProvider, + deleteHandler: deleteSAMLIdPServiceProvider, + + singleton: false, + // MFA not enforced in Auth. + mfaRequired: false, + description: "Configure service provider for the Teleport SAML IdP", + } +} + +func getSAMLIdPServiceProvider(ctx context.Context, client *authclient.Client, ref services.Ref, opts GetOpts) (Collection, error) { + if ref.Name != "" { + serviceProvider, err := client.GetSAMLIdPServiceProvider(ctx, ref.Name) + if err != nil { + return nil, trace.Wrap(err) + } + return &samlIdPServiceProviderCollection{serviceProviders: []types.SAMLIdPServiceProvider{serviceProvider}}, nil + } + + resources, err := stream.Collect(clientutils.Resources(ctx, client.ListSAMLIdPServiceProviders)) + if err != nil { + return nil, trace.Wrap(err) + } + return &samlIdPServiceProviderCollection{serviceProviders: resources}, nil +} + +func createSAMLIdPServiceProvider(ctx context.Context, client *authclient.Client, raw services.UnknownResource, opts CreateOpts) error { + // Create services.SAMLIdPServiceProvider from raw YAML to extract the service provider name. + sp, err := services.UnmarshalSAMLIdPServiceProvider(raw.Raw, services.DisallowUnknown()) + if err != nil { + return trace.Wrap(err) + } + + if sp.GetEntityDescriptor() != "" { + // verify that entity descriptor parses + ed, err := samlsp.ParseMetadata([]byte(sp.GetEntityDescriptor())) + if err != nil { + return trace.BadParameter("invalid entity descriptor for SAML IdP Service Provider %q: %v", sp.GetEntityID(), err) + } + + // issue warning about unsupported ACS bindings. + if err := services.FilterSAMLEntityDescriptor(ed, false /* quiet */); err != nil { + slog.WarnContext(ctx, "Entity descriptor for SAML IdP service provider contains unsupported ACS bindings", + "entity_id", sp.GetEntityID(), + "error", err, + ) + } + } + + exists := false + if err = client.CreateSAMLIdPServiceProvider(ctx, sp); err != nil { + if trace.IsAlreadyExists(err) { + exists = true + err = client.UpdateSAMLIdPServiceProvider(ctx, sp) + } + + if err != nil { + return trace.Wrap(err) + } + } + fmt.Printf("SAML IdP service provider %q has been %s\n", sp.GetName(), upsertVerb(exists, opts.Force)) + return nil +} + +func updateSAMLIdPServiceProvider(ctx context.Context, client *authclient.Client, raw services.UnknownResource, opts CreateOpts) error { + // Create services.SAMLIdPServiceProvider from raw YAML to extract the service provider name. + sp, err := services.UnmarshalSAMLIdPServiceProvider(raw.Raw, services.DisallowUnknown()) + if err != nil { + return trace.Wrap(err) + } + + if sp.GetEntityDescriptor() != "" { + // verify that entity descriptor parses + ed, err := samlsp.ParseMetadata([]byte(sp.GetEntityDescriptor())) + if err != nil { + return trace.BadParameter("invalid entity descriptor for SAML IdP Service Provider %q: %v", sp.GetEntityID(), err) + } + + // issue warning about unsupported ACS bindings. + if err := services.FilterSAMLEntityDescriptor(ed, false /* quiet */); err != nil { + slog.WarnContext(ctx, "Entity descriptor for SAML IdP service provider contains unsupported ACS bindings", + "entity_id", sp.GetEntityID(), + "error", err, + ) + } + } + + if err = client.UpdateSAMLIdPServiceProvider(ctx, sp); err != nil { + return trace.Wrap(err) + } + fmt.Printf("SAML IdP service provider %q has been updated\n", sp.GetName()) + return nil +} + +func deleteSAMLIdPServiceProvider(ctx context.Context, client *authclient.Client, ref services.Ref) error { + if err := client.DeleteSAMLIdPServiceProvider(ctx, ref.Name); err != nil { + return trace.Wrap(err) + } + fmt.Printf("SAML IdP service provider %q has been deleted\n", ref.Name) + return nil +}