Files
teleport/lib/services/spiffe_federations.go
T
Noah Stride 91356a5eba Workload Identity: SPIFFE Federation Syncer (#45586)
* Start hacking on implementation

* Add new proto field

* More resilient change detection

* Calculate next synce time for https-web bundles

* Add validation of spiffesyncer paramaters

* Flesh out events watcher mechanism

* Start putting together test for SPIFFEFederationSyncer

* Add UpdateSPIFFEFederation to service

* Flesh out test for UpdateSPIFFEFederation

* Working test "TestSPIFFEFederationSyncer_syncFederation" for basic
functionality

* Add additional validation for SPIFFEFederation

* Add more cases to TestSPIFFEFederationSyncer_syncFederation

* Add retry mechanism to syncery

* Tidy up logging

* Add SPIFFEFederationSyncer to lib/service init

* Use buffered channel for firstrun

* Fix potential deadlock with syncing

* Tidying up logging/naming

* Tidy up retry calculation

* Expand test suite

* Appease linter

* Use `GetStatus` for nil safety

* switch `UpdateResource` -> `ConditionalUpdateResource`

* Fix unprotected map access

* Add seleect condition for w.Done

* Correct comment

* Fix nil panic

* Fix TestSPIFFEFederationService_UpdateSPIFFEFederation

* Tidy up logging of refresh hint adjustment

* Move to CheckAndSetDefaults

* Tidy up error/log on fatalish errors

* Make shouldSyncTrustDomain a method

* Fix potential race with Put and Delete

* Actually handle stopCh

* use safer proto getters

* Fix spelling to be american
2024-08-30 10:04:34 +00:00

145 lines
5.9 KiB
Go

// Teleport
// Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
package services
import (
"context"
"net/url"
"strings"
"github.com/gravitational/trace"
"github.com/spiffe/go-spiffe/v2/bundle/spiffebundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
"github.com/gravitational/teleport/api/types"
)
// SPIFFEFederations is an interface over the SPIFFEFederations service. This
// interface may also be implemented by a client to allow remote and local
// consumers to access the resource in a similar way.
type SPIFFEFederations interface {
// GetSPIFFEFederation gets a SPIFFE Federation by name.
GetSPIFFEFederation(
ctx context.Context, name string,
) (*machineidv1.SPIFFEFederation, error)
// ListSPIFFEFederations lists all SPIFFE Federations using Google style
// pagination.
ListSPIFFEFederations(
ctx context.Context, pageSize int, lastToken string,
) ([]*machineidv1.SPIFFEFederation, string, error)
// CreateSPIFFEFederation creates a new SPIFFE Federation.
CreateSPIFFEFederation(
ctx context.Context, spiffeFederation *machineidv1.SPIFFEFederation,
) (*machineidv1.SPIFFEFederation, error)
// DeleteSPIFFEFederation deletes a SPIFFE Federation by name.
DeleteSPIFFEFederation(ctx context.Context, name string) error
// UpdateSPIFFEFederation updates a SPIFFE Federation. It will not act if the resource is not found
// or where the revision does not match.
UpdateSPIFFEFederation(
ctx context.Context, spiffeFederation *machineidv1.SPIFFEFederation,
) (*machineidv1.SPIFFEFederation, error)
}
// MarshalSPIFFEFederation marshals the SPIFFEFederation object into a JSON byte
// array.
func MarshalSPIFFEFederation(object *machineidv1.SPIFFEFederation, opts ...MarshalOption) ([]byte, error) {
return MarshalProtoResource(object, opts...)
}
// UnmarshalSPIFFEFederation unmarshals the SPIFFEFederation object from a
// JSON byte array.
func UnmarshalSPIFFEFederation(
data []byte, opts ...MarshalOption,
) (*machineidv1.SPIFFEFederation, error) {
return UnmarshalProtoResource[*machineidv1.SPIFFEFederation](data, opts...)
}
// ValidateSPIFFEFederation validates the SPIFFEFederation object.
func ValidateSPIFFEFederation(s *machineidv1.SPIFFEFederation) error {
switch {
case s == nil:
return trace.BadParameter("object cannot be nil")
case s.Version != types.V1:
return trace.BadParameter("version: only %q is supported", types.V1)
case s.Kind != types.KindSPIFFEFederation:
return trace.BadParameter("kind: must be %q", types.KindSPIFFEFederation)
case s.Metadata == nil:
return trace.BadParameter("metadata: is required")
case s.Metadata.Name == "":
return trace.BadParameter("metadata.name: is required")
case s.Spec == nil:
return trace.BadParameter("spec: is required")
case s.Spec.BundleSource == nil:
return trace.BadParameter("spec.bundle_source: is required")
case s.Spec.BundleSource.HttpsWeb != nil && s.Spec.BundleSource.Static != nil:
return trace.BadParameter("spec.bundle_source: at most one of https_web or static can be set")
case s.Spec.BundleSource.HttpsWeb == nil && s.Spec.BundleSource.Static == nil:
return trace.BadParameter("spec.bundle_source: at least one of https_web or static must be set")
}
// Validate name is valid SPIFFE Trust Domain name without the "spiffe://"
name := s.Metadata.Name
if strings.HasPrefix(name, "spiffe://") {
return trace.BadParameter(
"metadata.name: must not include the spiffe:// prefix",
)
}
td, err := spiffeid.TrustDomainFromString(name)
if err != nil {
return trace.Wrap(err, "validating metadata.name")
}
// Validate Static
if s.Spec.BundleSource.Static != nil {
if s.Spec.BundleSource.Static.Bundle == "" {
return trace.BadParameter("spec.bundle_source.static.bundle: is required")
}
// Validate contents
// TODO(noah): Is this a bit intense to run on every validation?
// This could easily be moved into reconciliation...
_, err := spiffebundle.Parse(td, []byte(s.Spec.BundleSource.Static.Bundle))
if err != nil {
return trace.Wrap(err, "validating spec.bundle_source.static.bundle")
}
}
// Validate HTTPSWeb
if s.Spec.BundleSource.HttpsWeb != nil {
if s.Spec.BundleSource.HttpsWeb.BundleEndpointUrl == "" {
return trace.BadParameter("spec.bundle_source.https_web.bundle_endpoint_url: is required")
}
_, err := url.Parse(s.Spec.BundleSource.HttpsWeb.BundleEndpointUrl)
if err != nil {
return trace.Wrap(err, "validating spec.bundle_source.https_web.bundle_endpoint_url")
}
}
// Ensure that all key status fields are set if any are set. This is a safeguard against weird inconsistent states
// where some fields are set and others are not.
currentBundleSet := s.Status.GetCurrentBundle() != ""
currentBundledSyncedAtSet := s.Status.GetCurrentBundleSyncedAt() != nil
currentBundleSyncedFromSet := s.Status.GetCurrentBundleSyncedFrom() != nil
anyStatusFieldSet := currentBundleSet || currentBundledSyncedAtSet || currentBundleSyncedFromSet
allStatusFieldsSet := currentBundleSet && currentBundledSyncedAtSet && currentBundleSyncedFromSet
if anyStatusFieldSet && !allStatusFieldsSet {
return trace.BadParameter("status: all of ['current_bundle', 'current_bundle_synced_at', 'current_bundle_synced_from'] must be set if any are set")
}
return nil
}