Skip to content

Commit

Permalink
[v15] Entra ID integration (#42555)
Browse files Browse the repository at this point in the history
* Entra ID reconciler: directory reconciler prerequisites (#40778)

* Add Entra ID resource origin

* Ignore ID and Revision from `header` in cmp

* Add e_imports for MS Graph SDK

* Entra ID integration: add proto definitions (#40997)

* Entra ID integration boilerplate (#40998)

* Add e imports for MS Graph SDK

* Add ability to sign Entra ID OIDC JWTs, rework KID handling

- Synthesize Key IDs for our JWT keys. For backwards compatibility, also
  include the same keys with an empty `kid` in JWKS.
- Sign AWS OIDC tokens with a `kid=""` header claim,
  rather than omitting the `kid` claim altogether.
  See comment for details.

* Add validation for Entra ID plugin

* Fix typo in assertion function name

* Update the OIDC JWKS test to expect the same key twice

* Add Entra ID plugin type constant

* go mod tidy

* Fix expected JWKS size in integration test

* Add basic tests for KeyID

* Move Azure auth settings from Plugin to Integration

* Address review comments

* Add a unit test to ensure KeyID compatibility

* Add license header to token_generator.go

* Rename validation function per new conventions

* Access Graph: sync AWS identity providers  (#41368)

* Add AWSSAMLProviderV1 to access graph proto

* Access Graph: sync AWS SAML Providers

* Parse SAML entity descriptor before sending to TAG

* Add protos for AWS OIDC providers

* Fetch AWS OIDC providers

* Fetch signing certificates for AWS SAML providers

* Deflake identity provider fetch test

The concrete implementation of IAM mock uses a map,
resulting in non-deterministic iteration order.
Sort the results before comparing to alleviate.

* Update lib/srv/discovery/fetchers/aws-sync/iam_test.go

Co-authored-by: Jakub Nyckowski <jakub.nyckowski@goteleport.com>

---------

Co-authored-by: Jakub Nyckowski <jakub.nyckowski@goteleport.com>

* Access Graph: Entra ID application sync prerequisites (#41650)

* Add access graph settings to Entra ID plugin

* Move Entra ID labels to OSS

* Add Entra resources and RPC to Access Graph proto

* Add azure-oidc integration to web.

Current code assumes that Integration is always either AwsOidc,
or an external audit storage integration

* Change app sso cache to a repeated field

* Entra ID integration: add onboarding script (#41811)

* Add Entra ID integration onboarding script

* Adapt after proto update

* Validate names in azure script handler, add test

* Add license headers

* Update Entra plugin test with SSO connector field

* Fix lint

* Remove leftover panics

* Adjust success message

* Downgrade log message level

* Expect exactly 1 SP for MS Graph, improve errors

* Properly extract hostname for enterprise app name

* Comment on assuming the first subscription

* Address review nits

* Factor out sso info fetch into a function

* fixup refactor

* Add retry logic to app role assignment

* Make godoc conventional

* Entra ID integration: integration script updates and web onboarding prerequisites (#42172)

* Remove integration name validation from web script

Not used by the script. It is validated by the "plugins/validate"
endpoint.

* Add required frontend constants for Entra ID

* Support Azure/Entra integrations in the list

* Add IsPolicyEnabled to web config

* Allow custom URL for ButtonLockedFeature

* Add CTA_ENTRA_ID event type

* Expose TAGInfoCache for use in e

* Add LackingIgs option

* Add Entra ID icon

* Add Entra ID plugin to storybook

* Bump e for dev build

* Return underlying error in getPrivateAPIToken

* Find default Azure subscription instead of the first one

* Require user to re-login when provisioning Azure OIDC

* Update prehog protos with Entra ID values

From https://github.com/gravitational/cloud/pull/9111

* Suppress verbose warnings / information from az

* Add an additional message after successful auth

Lets user know that `az login` has completed
and `teleport` is continuing its work.

* Move EntraId constant to the bottom

* Revert unintended changes to usageevents

CTA is 1-to-1 with prehog, but IntegrationEnrollKind is not.

* Remove integrationName validation asserts from test

This parameter is no longer accepted by the endpoint

* Revert "Bump e for dev build"

This reverts commit fc747a0.

* `go mod tidy` secondary modules

---------

Co-authored-by: Jakub Nyckowski <jakub.nyckowski@goteleport.com>
  • Loading branch information
justinas and jakule authored Jun 7, 2024
1 parent 27bc869 commit bb5706e
Show file tree
Hide file tree
Showing 63 changed files with 8,271 additions and 3,367 deletions.
2 changes: 2 additions & 0 deletions api/client/webclient/webconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ type WebConfig struct {
IsTeam bool `json:"isTeam"`
// IsIGSEnabled is true if [Features.IdentityGovernance] = true
IsIGSEnabled bool `json:"isIgsEnabled"`
// IsPolicyEnabled is true if [Features.Policy] = true
IsPolicyEnabled bool `json:"isPolicyEnabled"`
// featureLimits define limits for features.
// Typically used with feature teasers if feature is not enabled for the
// product type eg: Team product contains teasers to upgrade to Enterprise.
Expand Down
609 changes: 308 additions & 301 deletions api/gen/proto/go/usageevents/v1/usageevents.pb.go

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5773,6 +5773,8 @@ message PluginSpecV1 {
PluginServiceNowSettings serviceNow = 10;
// Settings for the Gitlab plugin.
PluginGitlabSettings gitlab = 12;
// Settings for the Entra ID plugin
PluginEntraIDSettings entra_id = 13;
// Settings for the SCIM plugin
PluginSCIMSettings scim = 14;
}
Expand Down Expand Up @@ -5953,6 +5955,53 @@ message PluginDiscordSettings {
map<string, DiscordChannels> role_to_recipients = 1;
}

// PluginEntraIDSettings defines settings for the Entra ID sync plugin
message PluginEntraIDSettings {
option (gogoproto.equal) = true;

// SyncSettings controls the user and access list sync settings for EntraID.
PluginEntraIDSyncSettings sync_settings = 1;

// AccessGraphSettings controls settings for syncing access graph specific data.
// When this is null, Entra ID integration with Access Graph is disabled.
PluginEntraIDAccessGraphSettings access_graph_settings = 2;
}

// Defines settings for syncing users and access lists from Entra ID.
message PluginEntraIDSyncSettings {
option (gogoproto.equal) = true;

// DefaultOwners are the default owners for all imported access lists.
repeated string default_owners = 1;

// SSOConnectorID is the name of the Teleport SSO connector created and used by the Entra ID plugin
string sso_connector_id = 2;
}

// AccessGraphSettings controls settings for syncing access graph specific data.
message PluginEntraIDAccessGraphSettings {
option (gogoproto.equal) = true;

// AppSsoSettingsCache is an array of single sign-on settings for Entra enterprise applications.
//
// This data is stored here because it is not available through traditional methods (MS Graph API).
// Instead, it is fetched once during the plugin's set up using the user's credentials to connect to Azure's private API.
repeated PluginEntraIDAppSSOSettings app_sso_settings_cache = 1;
}

// PluginEntraIDAppSSOSettings is a container for a single Entra ID enterprise application's
// cached SSO settings.
// As this data is only parsed by TAG, each value is stored as an opaque JSON blob.
message PluginEntraIDAppSSOSettings {
option (gogoproto.equal) = true;

// AppID is the `AppID` property of Entra application.
string app_id = 1;

// FederatedSSOV2 contains the cached, gzip-compressed payload from the /ApplicationSso/{servicePrincipalId}/FederatedSSOV2 endpoint.
bytes federated_sso_v2 = 2;
}

// PluginSCIMSettings defines the settings for a SCIM integration plugin
message PluginSCIMSettings {
option (gogoproto.equal) = true;
Expand Down Expand Up @@ -6422,6 +6471,8 @@ message IntegrationSpecV1 {
oneof SubKindSpec {
// AWSOIDC contains the specific fields to handle the AWS OIDC Integration subkind
AWSOIDCIntegrationSpecV1 AWSOIDC = 1 [(gogoproto.jsontag) = "aws_oidc,omitempty"];
// AzureOIDC contains the specific fields to handle the Azure OIDC Integration subkind
AzureOIDCIntegrationSpecV1 AzureOIDC = 2 [(gogoproto.jsontag) = "azure_oidc,omitempty"];
}
}

Expand All @@ -6440,6 +6491,17 @@ message AWSOIDCIntegrationSpecV1 {
string IssuerS3URI = 2 [(gogoproto.jsontag) = "issuer_s3_uri,omitempty"];
}

// AzureOIDCIntegrationSpecV1 contains the spec properties for the Azure OIDC SubKind Integration.
message AzureOIDCIntegrationSpecV1 {
// TenantID specifies the ID of Entra Tenant (Directory)
// that this plugin integrates with.
string TenantID = 1 [(gogoproto.jsontag) = "tenant_id,omitempty"];

// ClientID specifies the ID of Azure enterprise application (client)
// that corresponds to this plugin.
string ClientID = 2 [(gogoproto.jsontag) = "client_id,omitempty"];
}

// HeadlessAuthentication holds data for an ongoing headless authentication attempt.
message HeadlessAuthentication {
// Header is the resource header.
Expand Down
2 changes: 2 additions & 0 deletions api/proto/teleport/usageevents/v1/usageevents.proto
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ enum CTA {
CTA_ACCESS_MONITORING = 9;
CTA_EXTERNAL_AUDIT_STORAGE = 10;
CTA_OKTA_USER_SYNC = 11;
CTA_ENTRA_ID = 12;
}

// UIDiscoverDeployServiceEvent is emitted after the user installs a Teleport Agent.
Expand Down Expand Up @@ -563,6 +564,7 @@ enum IntegrationEnrollKind {
INTEGRATION_ENROLL_KIND_MACHINE_ID_JENKINS = 16;
INTEGRATION_ENROLL_KIND_MACHINE_ID_ANSIBLE = 17;
INTEGRATION_ENROLL_KIND_SERVICENOW = 18;
INTEGRATION_ENROLL_KIND_ENTRA_ID = 19;
}

// IntegrationEnrollMetadata contains common metadata
Expand Down
5 changes: 5 additions & 0 deletions api/types/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const (
// OriginDiscoveryKubernetes indicates that the resource was imported
// from kubernetes cluster by discovery service.
OriginDiscoveryKubernetes = "discovery-kubernetes"

// OriginEntraID indicates that the resource was imported
// from the Entra ID directory.
OriginEntraID = "entra-id"
)

// OriginValues lists all possible origin values.
Expand All @@ -77,4 +81,5 @@ var OriginValues = []string{
OriginOkta,
OriginSCIM,
OriginDiscoveryKubernetes,
OriginEntraID,
}
20 changes: 20 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,10 @@ const (
// from kubernetes cluster by discovery service.
OriginDiscoveryKubernetes = common.OriginDiscoveryKubernetes

// OriginEntraID indicates that the resource was imported
// from the Entra ID directory.
OriginEntraID = common.OriginEntraID

// IntegrationLabel is a resource metadata label name used to identify the integration name that created the resource.
IntegrationLabel = TeleportNamespace + "/integration"

Expand Down Expand Up @@ -974,6 +978,22 @@ const (

// OktaRoleNameLabel is the human readable name for a role sourced from Okta.
OktaRoleNameLabel = TeleportInternalLabelPrefix + "okta-role-name"

// EntraTenantIDLabel is the label for the Entra tenant ID.
EntraTenantIDLabel = TeleportInternalLabelPrefix + "entra-tenant"

// EntraUniqueIDLabel is the label for the unique identifier of the object in the Entra ID directory.
EntraUniqueIDLabel = TeleportInternalLabelPrefix + "entra-unique-id"

// EntraUPNLabel is the label for the user principal name in Entra ID.
EntraUPNLabel = TeleportInternalLabelPrefix + "entra-upn"

// EntraDisplayNameLabel is the label for the display name of the object in the Entra ID directory.
// The display name may not be unique.
EntraDisplayNameLabel = TeleportInternalLabelPrefix + "entra-display-name"

// EntraSAMAccountNameLabel is the label for user's on-premises sAMAccountName.
EntraSAMAccountNameLabel = TeleportInternalLabelPrefix + "entra-sam-account-name"
)

const (
Expand Down
77 changes: 74 additions & 3 deletions api/types/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import (
const (
// IntegrationSubKindAWSOIDC is an integration with AWS that uses OpenID Connect as an Identity Provider.
IntegrationSubKindAWSOIDC = "aws-oidc"

// IntegrationSubKindAzureOIDC is an integration with Azure that uses OpenID Connect as an Identity Provider.
IntegrationSubKindAzureOIDC = "azure-oidc"
)

// Integration specifies is a connection configuration between Teleport and a 3rd party system.
Expand All @@ -47,6 +50,9 @@ type Integration interface {
// SetAWSOIDCIssuerS3URI sets the IssuerS3URI of the AWS OIDC Spec.
// Eg, s3://my-bucket/my-prefix
SetAWSOIDCIssuerS3URI(string)

// GetAzureOIDCIntegrationSpec returns the `azure-oidc` spec fields.
GetAzureOIDCIntegrationSpec() *AzureOIDCIntegrationSpecV1
}

var _ ResourceWithLabels = (*IntegrationV1)(nil)
Expand All @@ -72,6 +78,27 @@ func NewIntegrationAWSOIDC(md Metadata, spec *AWSOIDCIntegrationSpecV1) (*Integr
return ig, nil
}

// NewIntegrationAzureOIDC returns a new `azure-oidc` subkind Integration
func NewIntegrationAzureOIDC(md Metadata, spec *AzureOIDCIntegrationSpecV1) (*IntegrationV1, error) {
ig := &IntegrationV1{
ResourceHeader: ResourceHeader{
Metadata: md,
Kind: KindIntegration,
Version: V1,
SubKind: IntegrationSubKindAzureOIDC,
},
Spec: IntegrationSpecV1{
SubKindSpec: &IntegrationSpecV1_AzureOIDC{
AzureOIDC: spec,
},
},
}
if err := ig.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return ig, nil
}

// String returns the integration string representation.
func (ig *IntegrationV1) String() string {
return fmt.Sprintf("IntegrationV1(Name=%v, SubKind=%s, Labels=%v)",
Expand Down Expand Up @@ -128,14 +155,19 @@ func (s *IntegrationSpecV1) CheckAndSetDefaults() error {
if err != nil {
return trace.Wrap(err)
}
case *IntegrationSpecV1_AzureOIDC:
err := integrationSubKind.Validate()
if err != nil {
return trace.Wrap(err)
}
default:
return trace.BadParameter("unknown integration subkind: %T", integrationSubKind)
}

return nil
}

// CheckAndSetDefaults validates an agent mesh integration.
// CheckAndSetDefaults validates the configuration for AWS OIDC integration subkind.
func (s *IntegrationSpecV1_AWSOIDC) CheckAndSetDefaults() error {
if s == nil || s.AWSOIDC == nil {
return trace.BadParameter("aws_oidc is required for %q subkind", IntegrationSubKindAWSOIDC)
Expand All @@ -160,6 +192,21 @@ func (s *IntegrationSpecV1_AWSOIDC) CheckAndSetDefaults() error {
return nil
}

// Validate validates the configuration for Azure OIDC integration subkind.
func (s *IntegrationSpecV1_AzureOIDC) Validate() error {
if s == nil || s.AzureOIDC == nil {
return trace.BadParameter("azure_oidc is required for %q subkind", IntegrationSubKindAzureOIDC)
}
if s.AzureOIDC.TenantID == "" {
return trace.BadParameter("tenant_id must be set")
}
if s.AzureOIDC.ClientID == "" {
return trace.BadParameter("client_id must be set")
}

return nil
}

// GetAWSOIDCIntegrationSpec returns the specific spec fields for `aws-oidc` subkind integrations.
func (ig *IntegrationV1) GetAWSOIDCIntegrationSpec() *AWSOIDCIntegrationSpecV1 {
return ig.Spec.GetAWSOIDC()
Expand Down Expand Up @@ -198,6 +245,11 @@ func (ig *IntegrationV1) SetAWSOIDCIssuerS3URI(issuerS3URI string) {
}
}

// GetAzureOIDCIntegrationSpec returns the specific spec fields for `azure-oidc` subkind integrations.
func (ig *IntegrationV1) GetAzureOIDCIntegrationSpec() *AzureOIDCIntegrationSpecV1 {
return ig.Spec.GetAzureOIDC()
}

// Integrations is a list of Integration resources.
type Integrations []Integration

Expand Down Expand Up @@ -247,7 +299,8 @@ func (ig *IntegrationV1) UnmarshalJSON(data []byte) error {
d := struct {
ResourceHeader `json:""`
Spec struct {
AWSOIDC json.RawMessage `json:"aws_oidc"`
AWSOIDC json.RawMessage `json:"aws_oidc"`
AzureOIDC json.RawMessage `json:"azure_oidc"`
} `json:"spec"`
}{}

Expand All @@ -270,6 +323,17 @@ func (ig *IntegrationV1) UnmarshalJSON(data []byte) error {

integration.Spec.SubKindSpec = subkindSpec

case IntegrationSubKindAzureOIDC:
subkindSpec := &IntegrationSpecV1_AzureOIDC{
AzureOIDC: &AzureOIDCIntegrationSpecV1{},
}

if err := json.Unmarshal(d.Spec.AzureOIDC, subkindSpec.AzureOIDC); err != nil {
return trace.Wrap(err)
}

integration.Spec.SubKindSpec = subkindSpec

default:
return trace.BadParameter("invalid subkind %q", integration.ResourceHeader.SubKind)
}
Expand All @@ -290,7 +354,8 @@ func (ig *IntegrationV1) MarshalJSON() ([]byte, error) {
d := struct {
ResourceHeader `json:""`
Spec struct {
AWSOIDC AWSOIDCIntegrationSpecV1 `json:"aws_oidc"`
AWSOIDC AWSOIDCIntegrationSpecV1 `json:"aws_oidc,omitempty"`
AzureOIDC AzureOIDCIntegrationSpecV1 `json:"azure_oidc,omitempty"`
} `json:"spec"`
}{}

Expand All @@ -303,6 +368,12 @@ func (ig *IntegrationV1) MarshalJSON() ([]byte, error) {
}

d.Spec.AWSOIDC = *ig.GetAWSOIDCIntegrationSpec()
case IntegrationSubKindAzureOIDC:
if ig.GetAzureOIDCIntegrationSpec() == nil {
return nil, trace.BadParameter("missing subkind data for %q subkind", ig.SubKind)
}

d.Spec.AzureOIDC = *ig.GetAzureOIDCIntegrationSpec()
default:
return nil, trace.BadParameter("invalid subkind %q", ig.SubKind)
}
Expand Down
Loading

0 comments on commit bb5706e

Please sign in to comment.