-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Gitpod OIDC Identity Provider #16482
Changes from all commits
b1acf4a
fc25531
8e414ad
c5c401e
2acca62
c309fe2
447ebb4
2896dc4
adad568
682de90
50b314c
f334af8
c8ec88d
4265479
c566e6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,89 @@ | ||||||
// Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||||||
// Licensed under the GNU Affero General Public License (AGPL). | ||||||
// See License.AGPL.txt in the project root for license information. | ||||||
|
||||||
package cmd | ||||||
|
||||||
import ( | ||||||
"context" | ||||||
"encoding/json" | ||||||
"fmt" | ||||||
"os" | ||||||
"os/exec" | ||||||
"path/filepath" | ||||||
"time" | ||||||
|
||||||
"github.com/spf13/cobra" | ||||||
) | ||||||
|
||||||
const ( | ||||||
idpAudienceAWS = "sts.amazonaws.com" | ||||||
) | ||||||
|
||||||
var idpLoginAwsOpts struct { | ||||||
RoleARN string | ||||||
CredentialsFile string | ||||||
} | ||||||
|
||||||
var idpLoginAwsCmd = &cobra.Command{ | ||||||
Use: "aws", | ||||||
Short: "Login to AWS", | ||||||
RunE: func(cmd *cobra.Command, args []string) error { | ||||||
cmd.SilenceUsage = true | ||||||
if idpLoginAwsOpts.RoleARN == "" { | ||||||
return fmt.Errorf("missing --role-arn or IDP_AWS_ROLE_ARN env var") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) | ||||||
defer cancel() | ||||||
|
||||||
tkn, err := idpToken(ctx, []string{idpAudienceAWS}) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
awsCmd := exec.Command("aws", "sts", "assume-role-with-web-identity", "--role-arn", idpLoginAwsOpts.RoleARN, "--role-session-name", fmt.Sprintf("gitpod-%d", time.Now().Unix()), "--web-identity-token", tkn) | ||||||
out, err := awsCmd.CombinedOutput() | ||||||
if err != nil { | ||||||
return fmt.Errorf("%w: %s", err, string(out)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
var result struct { | ||||||
Credentials struct { | ||||||
AccessKeyId string | ||||||
SecretAccessKey string | ||||||
SessionToken string | ||||||
} | ||||||
} | ||||||
err = json.Unmarshal(out, &result) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
credentials := "[default]\n" | ||||||
credentials += fmt.Sprintf("aws_access_key_id=%s\n", result.Credentials.AccessKeyId) | ||||||
credentials += fmt.Sprintf("aws_secret_access_key=%s\n", result.Credentials.SecretAccessKey) | ||||||
credentials += fmt.Sprintf("aws_session_token=%s\n", result.Credentials.SessionToken) | ||||||
|
||||||
_ = os.MkdirAll(filepath.Dir(idpLoginAwsOpts.CredentialsFile), 0755) | ||||||
err = os.WriteFile(idpLoginAwsOpts.CredentialsFile, []byte(credentials), 0600) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
return nil | ||||||
}, | ||||||
} | ||||||
|
||||||
func init() { | ||||||
idpLoginCmd.AddCommand(idpLoginAwsCmd) | ||||||
|
||||||
idpLoginAwsCmd.Flags().StringVar(&idpLoginAwsOpts.RoleARN, "role-arn", os.Getenv("IDP_AWS_ROLE_ARN"), "AWS role to assume (defaults to IDP_AWS_ROLE_ARN env var)") | ||||||
|
||||||
home, err := os.UserHomeDir() | ||||||
if err != nil { | ||||||
panic(err) | ||||||
} | ||||||
idpLoginAwsCmd.Flags().StringVar(&idpLoginAwsOpts.CredentialsFile, "credentials-file", filepath.Join(home, ".aws", "credentials"), "path to the AWS credentials file") | ||||||
_ = idpLoginAwsCmd.MarkFlagFilename("credentials-file") | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,67 @@ | ||||||
// Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||||||
// Licensed under the GNU Affero General Public License (AGPL). | ||||||
// See License.AGPL.txt in the project root for license information. | ||||||
|
||||||
package cmd | ||||||
|
||||||
import ( | ||||||
"context" | ||||||
"encoding/json" | ||||||
"fmt" | ||||||
"os" | ||||||
"os/exec" | ||||||
"time" | ||||||
|
||||||
"github.com/spf13/cobra" | ||||||
) | ||||||
|
||||||
const ( | ||||||
idpAudienceVault = "vault.hashicorp.com" | ||||||
) | ||||||
|
||||||
var idpLoginVaultOpts struct { | ||||||
Role string | ||||||
} | ||||||
|
||||||
var idpLoginVaultCmd = &cobra.Command{ | ||||||
Use: "vault", | ||||||
Short: "Login to HashiCorp's Vault", | ||||||
RunE: func(cmd *cobra.Command, args []string) error { | ||||||
cmd.SilenceUsage = true | ||||||
|
||||||
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) | ||||||
defer cancel() | ||||||
|
||||||
tkn, err := idpToken(ctx, []string{idpAudienceVault}) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
// vault write auth/jwt/login role=demo jwt=$TKN -format=json | ||||||
out, err := exec.Command("vault", "write", "-format=json", "auth/jwt/login", "role="+idpLoginVaultOpts.Role, "jwt="+tkn).CombinedOutput() | ||||||
if err != nil { | ||||||
return fmt.Errorf("%w: %s", err, string(out)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
var result struct { | ||||||
Auth struct { | ||||||
ClientToken string `json:"client_token"` | ||||||
} `json:"auth"` | ||||||
} | ||||||
err = json.Unmarshal(out, &result) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
vaultCmd := exec.Command("vault", "login", result.Auth.ClientToken) | ||||||
vaultCmd.Stdout = os.Stdout | ||||||
vaultCmd.Stderr = os.Stderr | ||||||
return vaultCmd.Run() | ||||||
}, | ||||||
} | ||||||
|
||||||
func init() { | ||||||
idpLoginCmd.AddCommand(idpLoginVaultCmd) | ||||||
|
||||||
idpLoginVaultCmd.Flags().StringVar(&idpLoginVaultOpts.Role, "role", os.Getenv("IDP_VAULT_ROLE"), "Vault role to assume (defaults to IDP_VAULT_ROLE env var)") | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||
// Licensed under the GNU Affero General Public License (AGPL). | ||
// See License.AGPL.txt in the project root for license information. | ||
|
||
package cmd | ||
|
||
import ( | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var idpLoginCmd = &cobra.Command{ | ||
Use: "login", | ||
Short: "Login to a service for which trust has been established", | ||
} | ||
|
||
func init() { | ||
idpCmd.AddCommand(idpLoginCmd) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||
// Licensed under the GNU Affero General Public License (AGPL). | ||
// See License.AGPL.txt in the project root for license information. | ||
|
||
package cmd | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
connect "github.com/bufbuild/connect-go" | ||
"github.com/gitpod-io/gitpod/common-go/util" | ||
"github.com/gitpod-io/gitpod/components/public-api/go/client" | ||
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1" | ||
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/gitpod" | ||
supervisor "github.com/gitpod-io/gitpod/supervisor/api" | ||
"github.com/spf13/cobra" | ||
"golang.org/x/xerrors" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/credentials/insecure" | ||
) | ||
|
||
var idpTokenOpts struct { | ||
Audience []string | ||
} | ||
|
||
var idpTokenCmd = &cobra.Command{ | ||
Use: "token", | ||
Short: "Requests an ID token for this workspace", | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
cmd.SilenceUsage = true | ||
|
||
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) | ||
defer cancel() | ||
|
||
tkn, err := idpToken(ctx, idpTokenOpts.Audience) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fmt.Println(tkn) | ||
return nil | ||
}, | ||
} | ||
|
||
func idpToken(ctx context.Context, audience []string) (idToken string, err error) { | ||
wsInfo, err := gitpod.GetWSInfo(ctx) | ||
if err != nil { | ||
return "", err | ||
} | ||
supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) | ||
if err != nil { | ||
return "", xerrors.Errorf("failed connecting to supervisor: %w", err) | ||
} | ||
defer supervisorConn.Close() | ||
clientToken, err := supervisor.NewTokenServiceClient(supervisorConn).GetToken(ctx, &supervisor.GetTokenRequest{ | ||
Host: wsInfo.GitpodApi.Host, | ||
Kind: "gitpod", | ||
Scope: []string{ | ||
"function:getWorkspace", | ||
}, | ||
}) | ||
if err != nil { | ||
return "", xerrors.Errorf("failed getting token from supervisor: %w", err) | ||
} | ||
|
||
c, err := client.New(client.WithCredentials(clientToken.Token), client.WithURL("https://api."+wsInfo.GitpodApi.Host)) | ||
if err != nil { | ||
return "", err | ||
} | ||
tkn, err := c.IdentityProvider.GetIDToken(ctx, &connect.Request[v1.GetIDTokenRequest]{ | ||
Msg: &v1.GetIDTokenRequest{ | ||
Audience: audience, | ||
WorkspaceId: wsInfo.WorkspaceId, | ||
}, | ||
}) | ||
if err != nil { | ||
return "", err | ||
} | ||
return tkn.Msg.Token, nil | ||
} | ||
|
||
func init() { | ||
idpCmd.AddCommand(idpTokenCmd) | ||
|
||
idpTokenCmd.Flags().StringArrayVar(&idpTokenOpts.Audience, "audience", nil, "audience of the ID token") | ||
_ = idpTokenCmd.MarkFlagRequired("audience") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||
// Licensed under the GNU Affero General Public License (AGPL). | ||
// See License.AGPL.txt in the project root for license information. | ||
|
||
package cmd | ||
|
||
import ( | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var idpCmd = &cobra.Command{ | ||
Use: "idp", | ||
Short: "Interact Gitpod's OIDC identity provider", | ||
Hidden: true, | ||
} | ||
|
||
func init() { | ||
rootCmd.AddCommand(idpCmd) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check if the aws binary exists to avoid confusing errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the behaviour if the AWS binary is not available:
How should the error look like instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking of requesting to install the CLI, but that is just fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could link to the AWS Docs. Arguably though, if someone's using this feature they're deep enough into it that they'll have looked at our (yet to be written) docs which will need to state that the AWS CLI needs to be installed.