Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v17] GitHub proxy #51086

Merged
merged 15 commits into from
Jan 16, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
GitHub Proxy part 1: github integration resource (#48999)
* github integration resource

* fix lib/web

* revert withSecrets

* use static credentials

* address review comments

* fix ut
  • Loading branch information
greedy52 committed Jan 16, 2025
commit 3467219847f304801a0703960be6a0ddd42ea738
8 changes: 8 additions & 0 deletions api/proto/teleport/legacy/types/events/events.proto
Original file line number Diff line number Diff line change
@@ -4381,6 +4381,8 @@ message IntegrationMetadata {
AWSOIDCIntegrationMetadata AWSOIDC = 2 [(gogoproto.jsontag) = "aws_oidc,omitempty"];
// AzureOIDC contains metadata for Azure OIDC integrations.
AzureOIDCIntegrationMetadata AzureOIDC = 3 [(gogoproto.jsontag) = "azure_oidc,omitempty"];
// GitHub contains metadata for GitHub integrations.
GitHubIntegrationMetadata GitHub = 4 [(gogoproto.jsontag) = "github,omitempty"];
}

// AWSOIDCIntegrationMetadata contains metadata for AWS OIDC integrations.
@@ -4402,6 +4404,12 @@ message AzureOIDCIntegrationMetadata {
string ClientID = 2 [(gogoproto.jsontag) = "client_id,omitempty"];
}

// GitHubIntegrationMetadata contains metadata for GitHub integrations.
message GitHubIntegrationMetadata {
// Organization specifies the name of the organization for the GitHub integration.
string Organization = 1 [(gogoproto.jsontag) = "organization,omitempty"];
}

// PluginCreate is emitted when a plugin resource is created.
message PluginCreate {
// Metadata is a common event metadata.
20 changes: 20 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
@@ -7153,6 +7153,7 @@ message PluginStaticCredentialsSpecV1 {
string APIToken = 1;
PluginStaticCredentialsBasicAuth BasicAuth = 2;
PluginStaticCredentialsOAuthClientSecret OAuthClientSecret = 3;
PluginStaticCredentialsSSHCertAuthorities SSHCertAuthorities = 4;
}
}

@@ -7174,6 +7175,14 @@ message PluginStaticCredentialsOAuthClientSecret {
string ClientSecret = 2 [(gogoproto.jsontag) = "client_secret"];
}

// PluginStaticCredentialsSSHCertAuthorities contains the active SSH CAs used
// for the integration or plugin.
message PluginStaticCredentialsSSHCertAuthorities {
// CertAuthorities contains the active SSH CAs used for the integration or
// plugin.
repeated SSHKeyPair cert_authorities = 1;
}

// SAMLIdPServiceProviderV1 is the representation of a SAML IdP service provider.
message SAMLIdPServiceProviderV1 {
option (gogoproto.goproto_stringer) = false;
@@ -7521,7 +7530,12 @@ message IntegrationSpecV1 {
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"];
// GitHub contains the specific fields to handle the GitHub integration subkind.
GitHubIntegrationSpecV1 GitHub = 3 [(gogoproto.jsontag) = "github,omitempty"];
}

// Credentials contains credentials for the integration.
PluginCredentialsV1 credentials = 4;
}

// AWSOIDCIntegrationSpecV1 contains the spec properties for the AWS OIDC SubKind Integration.
@@ -7566,6 +7580,12 @@ message AzureOIDCIntegrationSpecV1 {
string ClientID = 2 [(gogoproto.jsontag) = "client_id,omitempty"];
}

// GitHubIntegrationSpecV1 contains the specific fields to handle the GitHub integration subkind.
message GitHubIntegrationSpecV1 {
// Organization specifies the name of the organization for the GitHub integration.
string Organization = 1 [(gogoproto.jsontag) = "organization,omitempty"];
}

// HeadlessAuthentication holds data for an ongoing headless authentication attempt.
message HeadlessAuthentication {
// Header is the resource header.
2,652 changes: 1,443 additions & 1,209 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

147 changes: 141 additions & 6 deletions api/types/integration.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,8 @@ import (
"net/url"

"github.com/gravitational/trace"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/protoadapt"

"github.com/gravitational/teleport/api/utils"
)
@@ -32,6 +34,9 @@ const (

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

// IntegrationSubKindGitHub is an integration with GitHub.
IntegrationSubKindGitHub = "github"
)

const (
@@ -61,6 +66,18 @@ type Integration interface {

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

// GetGitHubIntegrationSpec returns the GitHub spec.
GetGitHubIntegrationSpec() *GitHubIntegrationSpecV1
// SetGitHubIntegrationSpec returns the GitHub spec.
SetGitHubIntegrationSpec(*GitHubIntegrationSpecV1)

// SetCredentials updates credentials.
SetCredentials(creds PluginCredentials) error
// GetCredentials retrieves credentials.
GetCredentials() PluginCredentials
// WithoutCredentials returns a copy without credentials.
WithoutCredentials() Integration
}

var _ ResourceWithLabels = (*IntegrationV1)(nil)
@@ -107,6 +124,27 @@ func NewIntegrationAzureOIDC(md Metadata, spec *AzureOIDCIntegrationSpecV1) (*In
return ig, nil
}

// NewIntegrationGitHub returns a new `github` subkind Integration
func NewIntegrationGitHub(md Metadata, spec *GitHubIntegrationSpecV1) (*IntegrationV1, error) {
ig := &IntegrationV1{
ResourceHeader: ResourceHeader{
Metadata: md,
Kind: KindIntegration,
Version: V1,
SubKind: IntegrationSubKindGitHub,
},
Spec: IntegrationSpecV1{
SubKindSpec: &IntegrationSpecV1_GitHub{
GitHub: 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)",
@@ -168,6 +206,11 @@ func (s *IntegrationSpecV1) CheckAndSetDefaults() error {
if err != nil {
return trace.Wrap(err)
}
case *IntegrationSpecV1_GitHub:
if err := integrationSubKind.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
return nil
default:
return trace.BadParameter("unknown integration subkind: %T", integrationSubKind)
}
@@ -230,6 +273,17 @@ func (s *IntegrationSpecV1_AzureOIDC) Validate() error {
return nil
}

// CheckAndSetDefaults validates the configuration for GitHub integration subkind.
func (s *IntegrationSpecV1_GitHub) CheckAndSetDefaults() error {
if s == nil || s.GitHub == nil {
return trace.BadParameter("github spec must be set for GitHub integrations")
}
if err := ValidateGitHubOrganizationName(s.GitHub.Organization); err != nil {
return trace.Wrap(err, "invalid GitHub organization name")
}
return nil
}

// GetAWSOIDCIntegrationSpec returns the specific spec fields for `aws-oidc` subkind integrations.
func (ig *IntegrationV1) GetAWSOIDCIntegrationSpec() *AWSOIDCIntegrationSpecV1 {
return ig.Spec.GetAWSOIDC()
@@ -273,6 +327,18 @@ func (ig *IntegrationV1) GetAzureOIDCIntegrationSpec() *AzureOIDCIntegrationSpec
return ig.Spec.GetAzureOIDC()
}

// GetGitHubIntegrationSpec returns the GitHub spec.
func (ig *IntegrationV1) GetGitHubIntegrationSpec() *GitHubIntegrationSpecV1 {
return ig.Spec.GetGitHub()
}

// SetGitHubIntegrationSpec returns the GitHub spec.
func (ig *IntegrationV1) SetGitHubIntegrationSpec(spec *GitHubIntegrationSpecV1) {
ig.Spec.SubKindSpec = &IntegrationSpecV1_GitHub{
GitHub: spec,
}
}

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

@@ -322,8 +388,10 @@ func (ig *IntegrationV1) UnmarshalJSON(data []byte) error {
d := struct {
ResourceHeader `json:""`
Spec struct {
AWSOIDC json.RawMessage `json:"aws_oidc"`
AzureOIDC json.RawMessage `json:"azure_oidc"`
AWSOIDC json.RawMessage `json:"aws_oidc"`
AzureOIDC json.RawMessage `json:"azure_oidc"`
GitHub json.RawMessage `json:"github"`
Credentials json.RawMessage `json:"credentials"`
} `json:"spec"`
}{}

@@ -333,6 +401,13 @@ func (ig *IntegrationV1) UnmarshalJSON(data []byte) error {
}

integration.ResourceHeader = d.ResourceHeader
if len(d.Spec.Credentials) != 0 {
var credentials PluginCredentialsV1
if err := protojson.Unmarshal(d.Spec.Credentials, protoadapt.MessageV2Of(&credentials)); err != nil {
return trace.Wrap(err)
}
integration.Spec.Credentials = &credentials
}

switch integration.SubKind {
case IntegrationSubKindAWSOIDC:
@@ -357,6 +432,17 @@ func (ig *IntegrationV1) UnmarshalJSON(data []byte) error {

integration.Spec.SubKindSpec = subkindSpec

case IntegrationSubKindGitHub:
subkindSpec := &IntegrationSpecV1_GitHub{
GitHub: &GitHubIntegrationSpecV1{},
}

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

integration.Spec.SubKindSpec = subkindSpec

default:
return trace.BadParameter("invalid subkind %q", integration.ResourceHeader.SubKind)
}
@@ -377,30 +463,79 @@ func (ig *IntegrationV1) MarshalJSON() ([]byte, error) {
d := struct {
ResourceHeader `json:""`
Spec struct {
AWSOIDC AWSOIDCIntegrationSpecV1 `json:"aws_oidc,omitempty"`
AzureOIDC AzureOIDCIntegrationSpecV1 `json:"azure_oidc,omitempty"`
AWSOIDC AWSOIDCIntegrationSpecV1 `json:"aws_oidc,omitempty"`
AzureOIDC AzureOIDCIntegrationSpecV1 `json:"azure_oidc,omitempty"`
GitHub GitHubIntegrationSpecV1 `json:"github,omitempty"`
Credentials json.RawMessage `json:"credentials,omitempty"`
} `json:"spec"`
}{}

d.ResourceHeader = ig.ResourceHeader
if ig.Spec.Credentials != nil {
data, err := protojson.Marshal(protoadapt.MessageV2Of(ig.Spec.Credentials))
if err != nil {
return nil, trace.Wrap(err)
}
d.Spec.Credentials = json.RawMessage(data)
}

switch ig.SubKind {
case IntegrationSubKindAWSOIDC:
if ig.GetAWSOIDCIntegrationSpec() == nil {
return nil, trace.BadParameter("missing subkind data for %q subkind", ig.SubKind)
return nil, trace.BadParameter("missing spec for %q subkind", ig.SubKind)
}

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

d.Spec.AzureOIDC = *ig.GetAzureOIDCIntegrationSpec()
case IntegrationSubKindGitHub:
if ig.GetGitHubIntegrationSpec() == nil {
return nil, trace.BadParameter("missing spec for %q subkind", ig.SubKind)
}
d.Spec.GitHub = *ig.GetGitHubIntegrationSpec()
default:
return nil, trace.BadParameter("invalid subkind %q", ig.SubKind)
}

out, err := json.Marshal(d)
return out, trace.Wrap(err)
}

// SetCredentials updates credentials.
func (ig *IntegrationV1) SetCredentials(creds PluginCredentials) error {
if creds == nil {
ig.Spec.Credentials = nil
return nil
}
switch creds := creds.(type) {
case *PluginCredentialsV1:
ig.Spec.Credentials = creds
default:
return trace.BadParameter("unsupported plugin credential type %T", creds)
}
return nil
}

// GetCredentials retrieves credentials.
func (ig *IntegrationV1) GetCredentials() PluginCredentials {
// This function returns an interface so return nil explicitly.
if ig.Spec.Credentials == nil {
return nil
}
return ig.Spec.Credentials
}

// WithoutCredentials returns a copy without credentials.
func (ig *IntegrationV1) WithoutCredentials() Integration {
if ig == nil || ig.GetCredentials() == nil {
return ig
}

clone := utils.CloneProtoMsg(ig)
clone.SetCredentials(nil)
return clone
}
32 changes: 32 additions & 0 deletions api/types/integration_github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2024 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package types

import "regexp"

// validGitHubOrganizationName filters the allowed characters in GitHub
// organization name.
//
// GitHub shows the following error when inputing an invalid org name:
// The name '_' may only contain alphanumeric characters or single hyphens, and
// cannot begin or end with a hyphen.
var validGitHubOrganizationName = regexp.MustCompile(`^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$`)

// ValidateGitHubOrganizationName returns an error if a given string is not a
// valid GitHub organization name.
func ValidateGitHubOrganizationName(name string) error {
return ValidateResourceName(validGitHubOrganizationName, name)
}
Loading