From 653207943795e7e3ee3a4ec3f9a17154c9d3132c Mon Sep 17 00:00:00 2001 From: Dan Pantry Date: Tue, 7 Nov 2023 12:40:42 -0800 Subject: [PATCH] Squashed 'keyconjurer-v2/' changes from be45b54..69d489f 69d489f Apply formatting changes 249bab4 Add missing articles df88087 Add Linux usage fb27555 Update MacUsage.md (#102) 5450ae6 Add new frontend with new instructions (#101) bdf33e5 Don't parse the updated_at field 9270394 Open a browser by default when logging in 6cabb2c Ensure account iteration order is stable 51b6b06 Return a HTTP 500 if Okta has a problem 8913eaa Improve keyconjurer accounts output 85f224a YubiKey support (#98) REVERT: be45b54 Remove unused return value REVERT: b66a276 Run go test REVERT: 6aa2105 Remove unnecessary refs that are breaking PR builds REVERT: 10156de Conform to gorevives suggestions REVERT: ba36b5b Strip symbols REVERT: 68ff0dd Add the kitchensink module REVERT: c31df25 terraform fmt REVERT: 4d853f5 Move frontend to its own module REVERT: b5b0040 Remove KMS policy REVERT: 3531287 Add a load balancer module REVERT: 4162a4f Oops REVERT: 8fe40c3 Correct CLI_TARGETS REVERT: e939fc0 Reduce copying REVERT: 8f5d0fe Remove unnecessary quote REVERT: 89d03e8 Fold client/makefile into root REVERT: dee9d34 simplify Makefile a bit REVERT: d3a8858 Tidy up Makefile REVERT: 202b371 Fold frontend makefile into root REVERT: 27883e5 Fix up Frontend Makefile REVERT: a5ba274 Remove broken target REVERT: a4cd0f6 Rename Makefile REVERT: f467a1d Merge terraform makefile into root REVERT: ef97642 Remove unused value REVERT: 18296d1 Move Lambda function into its own module REVERT: 9ec3b05 Use a temporary dir for building this target REVERT: 4e2436e Remove note on legacy API REVERT: 4d85d5c fixup! Move lambda function to lambda/ REVERT: d42f0d5 Remove unused function REVERT: d26e60c Consolidate further REVERT: 904f522 Fold cloud.go into cli/ folder REVERT: d45aa3d Move lambda function to lambda/ REVERT: 4188119 Remove unused code REVERT: 31655c2 Go mod tidy REVERT: 764843e Consolidate pkg/ dir into internal/ directory REVERT: b7f7235 Remove local copy of the coreos-oidc dependency REVERT: 25fec03 Remove old cloud.Provider struct REVERT: 371d092 Remove legacy auth providers REVERT: f5dac64 Remove logrus REVERT: 6494834 Modernize settings REVERT: 6745a6f Remove the legacy API REVERT: 849ba0a Allow users to provide flags through KEYCONJURERFLAGS REVERT: 8159c2e Update README REVERT: dd16f4b Use a single const for the build timestamp REVERT: fd0649d Remove OneLogin test REVERT: 1928a93 Only require env variables for build target REVERT: 4f0dbf5 Don't rely on the context for the config & path REVERT: c0c49ef Refactor get REVERT: 4bb065d oops REVERT: b89ad28 Don't use configinfo outside of context REVERT: 655973b Remove appname REVERT: 2e258c2 Remove notes REVERT: 2980f1c Use buffers in test REVERT: 7bd0158 Add function for resolving credentials file REVERT: fa00d5c Simplify section managemnet REVERT: b23eac8 Remove ~/.aws/config management REVERT: f60f16f Use filepath instead of fmt Sprintf REVERT: ecdb9d1 use t.Setenv for environment variables REVERT: d5d3c90 Remove pipe REVERT: 64f0529 Store the type of credentials within the creds REVERT: 584b7e3 allow user to only output the URL REVERT: ea246e6 Provide a better error message if Okta screws up REVERT: 43cfa6a add server address const REVERT: ebbf97b add the correct ID for the application REVERT: c34d56a fix bypassing cache REVERT: 5d6788a Correctly set headers REVERT: b97ed75 Write errors in ServeJSON to log REVERT: 3180045 Fix a bug where headers werent canonized REVERT: a4e17a8 Assume headers are lowercase REVERT: 2e80e39 apply the Oauth2 header ourselves REVERT: 69bd103 Ensure a status code is set REVERT: ded014f use http.Header.Get() rather than accessing the map REVERT: e135a86 squash more bugs REVERT: a717af6 add status messages REVERT: 4ff8b00 add listener rule REVERT: 13128ca add a trailing slash REVERT: 9773a92 correct typo REVERT: 997e0b1 fix broken xargs instruction REVERT: d737cbf no longer support legacy ids REVERT: 6c7646d Fix a bug where executing a command would brick existing config REVERT: 4fc7636 Add the ability to bypass account cache REVERT: 4374fdd Don't use global variables for flags REVERT: c6bccbd Remove global quiet variable REVERT: 983c5b8 Pass configuration in context REVERT: 36c368e Simplify get REVERT: bc065ff Use AWS provider directly REVERT: e4068d6 Remove OneLogin REVERT: d045ae4 Don't use global variables for new flags REVERT: 742936c Immediately exit if --no-refresh specified REVERT: 03ce54b Move RequestAttrs to http REVERT: 177f761 Move serverless function to the serverless functions file REVERT: b199a29 Don't pre-allocate a size REVERT: 78138f4 All target must be first REVERT: 8d270e4 Correctly upload all files REVERT: fe124d8 Provide secrets to the new Lambda function REVERT: ac30889 Tag new API with lambda.norpc REVERT: a0e3b9d Use provided.al2 for the new function REVERT: 7e61d24 Modernize Makefile REVERT: c7910b9 Move new endpoint to distinguish it from legacy ones REVERT: e00f286 use CSV REVERT: 7859f5a Only enumerate Tencent or Amazon apps REVERT: e80025e Correctly list applinks for user REVERT: a3ee0ff Correctly fetch the username from the token REVERT: 1322b31 Oops REVERT: dad0fc0 fix visual errors with error reporting REVERT: c47f278 Reduce nesting REVERT: fe0e1e4 Call the KeyConjurer API instead of the Okta one REVERT: a29f435 Allow user to disable refreshing of accounts REVERT: d350649 Rename config path REVERT: e205e49 Improve logging and add to testserver REVERT: 9ccb6bd Add implementation of the new function REVERT: 7b48b69 Move Lambdaify out of main REVERT: 97c7beb add lambda <-> net/http translation layer REVERT: b4d9398 Add skeleton for new required endpoint REVERT: f7d2ddc Update Port REVERT: 66cbb19 move fetching accounts into a function REVERT: b2677bf Remove legacy accommodations REVERT: 5e085c1 Correct set the timeout REVERT: 2510385 Remove QR code device flow REVERT: 216d1d7 Use context for timeouts REVERT: aaf3f64 Add new versino of Cobra REVERT: a8c8706 Add CLIENT_ID and OIDC_DOMAIN to Makefile REVERT: 9c0a16c Simplify version flags REVERT: d1ec64b Remove host flag REVERT: 7a26205 Use consts file for client ID and OIDC domain REVERT: 3a1dea7 Modify download command to use http.Client for non windows systems REVERT: b38aeef We no longer store credentials REVERT: df593b6 add a working roles command REVERT: 639879d Implement Role & Provider finding REVERT: 3251c26 Reinstate alias resolution REVERT: efacea6 add logging to all requests we issue REVERT: fd0a05e Add basic request monitoring REVERT: dec08fe add slog REVERT: cecf8b7 Add HTTP logging middleware REVERT: 245af7a Remove base package REVERT: 2183f57 Remove unused flag REVERT: c1d4a9a Remove references to the API project from the client REVERT: 1cb0884 Fix warnings REVERT: 9068fa6 Remove unsupported roles command REVERT: 7a2b62c Remove deprecated API calls REVERT: a9ad856 Move cloud handlers to internal REVERT: a549fd8 Correctly exchange the SAML assertion for tokens REVERT: 56ae358 Return the session token when acquired REVERT: ceb95e0 Add "hidden" scope which enables the token exchange endpoint REVERT: b7be128 add some notes REVERT: 01dd811 fixup! WIP add access token exchange REVERT: 2e71142 WIP add access token exchange REVERT: 905f278 Move oauth2 stuff to.. oauth2 REVERT: 936c3b0 Remove old method of interacting with Okta here REVERT: 5ab579a Catch token expiry REVERT: 072801b Remove bad check REVERT: 884f6b1 Go mod tidy REVERT: afe05bf Display a QR code to the user REVERT: bf84d14 add qr terminal REVERT: 7372508 Correctly render accounts REVERT: 2c2ec53 Add helper method to identify that tokens have expired REVERT: 39256f2 Correctly load and store the oauth tokens REVERT: 9e13414 Move OIDC stuff to its own package REVERT: 13976e7 Go mod tidy REVERT: 665bc58 Correctly implement the device flow for Okta REVERT: 3a292fc Add oauth2device REVERT: 3690d08 Remove list providers command REVERT: d399cc0 Move OAuth2 functionality to oauth2.go REVERT: f665295 Split Login functionality into its own method REVERT: eab9cb7 Correctly generate random values REVERT: 2bc0394 Implement callback handling REVERT: b53e4c1 Add the OAuth2 flow skeleton for logging in REVERT: 45466db add oidc library REVERT: 5099552 Add Oauth2 library git-subtree-dir: keyconjurer-v2 git-subtree-split: 69d489f1cd2d3c2131e5cf79128a6161c7edb090 --- .gitignore | 2 + Makefile | 5 +- cli/accounts.go | 10 +- cli/config.go | 29 +- cli/get.go | 16 +- cli/login.go | 51 +- cli/oauth2.go | 17 +- cli/roles.go | 4 +- cli/saml.go | 4 + cli/saml_test.go | 2 +- example.env | 1 - frontend/config/env.js | 104 + frontend/config/getHttpsConfig.js | 66 + frontend/config/jest/babelTransform.js | 29 + frontend/config/jest/cssTransform.js | 14 + frontend/config/jest/fileTransform.js | 40 + frontend/config/modules.js | 134 + frontend/config/paths.js | 77 + frontend/config/webpack.config.js | 760 + .../persistentCache/createEnvironmentHash.js | 9 + frontend/config/webpackDevServer.config.js | 127 + frontend/package-lock.json | 19510 ++++++++++++++-- frontend/package.json | 115 +- frontend/scripts/build.js | 217 + frontend/scripts/start.js | 154 + frontend/scripts/test.js | 52 + frontend/src/App.css | 32 - frontend/src/App.module.css | 45 + frontend/src/App.tsx | 117 +- frontend/src/actions.ts | 147 - frontend/src/articles/LinuxUsage.md | 46 + frontend/src/articles/MacUsage.md | 56 + frontend/src/articles/UsageTemplate.md | 35 + frontend/src/articles/WSLUsage.md | 1 + frontend/src/articles/WindowsUsage.md | 1 + frontend/src/components/Header.tsx | 11 - frontend/src/components/History.tsx | 47 - frontend/src/components/KeyCard.tsx | 48 - .../src/components/KeyRequestForm.test.tsx | 122 - frontend/src/components/KeyRequestForm.tsx | 263 - frontend/src/components/LoginForm.tsx | 127 - .../src/components/TroubleshootingCard.tsx | 16 - frontend/src/logo.svg | 7 - frontend/src/stores.ts | 102 - frontend/src/types.d.ts | 10 + go.mod | 2 + go.sum | 5 + internal/api/okta.go | 38 +- internal/api/serverless_functions.go | 8 + internal/api/settings.go | 6 +- terraform/modules/list_applications/lambda.tf | 2 +- 51 files changed, 19846 insertions(+), 2997 deletions(-) create mode 100644 frontend/config/env.js create mode 100644 frontend/config/getHttpsConfig.js create mode 100644 frontend/config/jest/babelTransform.js create mode 100644 frontend/config/jest/cssTransform.js create mode 100644 frontend/config/jest/fileTransform.js create mode 100644 frontend/config/modules.js create mode 100644 frontend/config/paths.js create mode 100644 frontend/config/webpack.config.js create mode 100644 frontend/config/webpack/persistentCache/createEnvironmentHash.js create mode 100644 frontend/config/webpackDevServer.config.js create mode 100644 frontend/scripts/build.js create mode 100644 frontend/scripts/start.js create mode 100644 frontend/scripts/test.js delete mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.module.css delete mode 100644 frontend/src/actions.ts create mode 100644 frontend/src/articles/LinuxUsage.md create mode 100644 frontend/src/articles/MacUsage.md create mode 100644 frontend/src/articles/UsageTemplate.md create mode 100644 frontend/src/articles/WSLUsage.md create mode 100644 frontend/src/articles/WindowsUsage.md delete mode 100644 frontend/src/components/Header.tsx delete mode 100644 frontend/src/components/History.tsx delete mode 100644 frontend/src/components/KeyCard.tsx delete mode 100644 frontend/src/components/KeyRequestForm.test.tsx delete mode 100644 frontend/src/components/KeyRequestForm.tsx delete mode 100644 frontend/src/components/LoginForm.tsx delete mode 100644 frontend/src/components/TroubleshootingCard.tsx delete mode 100644 frontend/src/logo.svg delete mode 100644 frontend/src/stores.ts diff --git a/.gitignore b/.gitignore index 19cbb4d7..ae1d78dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# G # Manual additions *.auto.tfvars dev.env @@ -277,3 +278,4 @@ $RECYCLE.BIN/ cli/keyconjurer-darwin* cli/keyconjurer-linux* cli/keyconjurer-windows.exe +vendor diff --git a/Makefile b/Makefile index 89f9e5f6..cb87ac4f 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ RELEASE ?= dev VERSION ?= $(shell git rev-parse --short HEAD) +# This runs on Linux machines. Mac users should override TIMESTAMP. +TIMESTAMP ?= $(shell date --iso-8601=minutes) ## Standard targets for all Makefiles in our team @@ -33,7 +35,6 @@ frontend/build/index.html: frontend/node_modules cd frontend && \ REACT_APP_VERSION='$$(git rev-parse --short HEAD)-$(RELEASE)' \ REACT_APP_API_URL=${API_URL} \ - REACT_APP_BINARY_NAME=${BINARY_NAME} \ REACT_APP_DOCUMENTATION_URL=${REACT_APP_DOCUMENTATION_URL} \ REACT_APP_CLIENT=webUI npm run-script build @@ -66,7 +67,7 @@ cli/keyconjurer: -X main.Version=$(shell git rev-parse --short HEAD)-$(RELEASE) \ -X main.ClientID=$(CLIENT_ID) \ -X main.OIDCDomain=$(OIDC_DOMAIN) \ - -X main.BuildTimestamp='$(shell date --iso-8601=minutes)' \ + -X main.BuildTimestamp='$(TIMESTAMP)' \ -X main.ServerAddress=$(SERVER_ADDRESS)" \ -o $(BUILD_TARGET) diff --git a/cli/accounts.go b/cli/accounts.go index 39445186..1b25610d 100644 --- a/cli/accounts.go +++ b/cli/accounts.go @@ -23,7 +23,6 @@ var ( func init() { accountsCmd.Flags().Bool(FlagNoRefresh, false, "Indicate that the account list should not be refreshed when executing this command. This is useful if you're not able to reach the account server.") - // TODO: Replace the address accountsCmd.Flags().String(FlagServerAddress, ServerAddress, "The address of the account server. This does not usually need to be changed or specified.") } @@ -34,11 +33,14 @@ var accountsCmd = &cobra.Command{ config := ConfigFromCommand(cmd) stdOut := cmd.OutOrStdout() noRefresh, _ := cmd.Flags().GetBool(FlagNoRefresh) + loud := !ShouldUseMachineOutput(cmd.Flags()) if noRefresh { - config.DumpAccounts(stdOut) - if q, _ := cmd.Flags().GetBool(FlagQuiet); !q { + config.DumpAccounts(stdOut, loud) + + if loud { cmd.PrintErrf("--%s was specified - these results may be out of date, and you may not have access to accounts in this list.\n", FlagNoRefresh) } + return nil } @@ -70,7 +72,7 @@ var accountsCmd = &cobra.Command{ } config.UpdateAccounts(accounts) - config.DumpAccounts(stdOut) + config.DumpAccounts(stdOut, loud) return nil }, } diff --git a/cli/config.go b/cli/config.go index bc1c8392..57fcc5a3 100644 --- a/cli/config.go +++ b/cli/config.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "sort" "time" "strings" @@ -73,8 +74,18 @@ func generateDefaultAlias(name string) string { } func (a *accountSet) ForEach(f func(id string, account Account, alias string)) { - for id, acc := range a.accounts { - f(id, *acc, acc.Alias) + // Golang does not maintain the order of maps, so we create a slice which is sorted instead. + var accounts []*Account + for _, acc := range a.accounts { + accounts = append(accounts, acc) + } + + sort.SliceStable(accounts, func(i, j int) bool { + return accounts[i].Name < accounts[j].Name + }) + + for _, acc := range accounts { + f(acc.ID, *acc, acc.Alias) } } @@ -169,12 +180,18 @@ func (a *accountSet) ReplaceWith(other []Account) { } } -func (a accountSet) WriteTable(w io.Writer) { +func (a accountSet) WriteTable(w io.Writer, withHeaders bool) { tbl := csv.NewWriter(w) - tbl.Write([]string{"id,name,alias"}) + tbl.Comma = '\t' + + if withHeaders { + tbl.Write([]string{"id", "name", "alias"}) + } + a.ForEach(func(id string, acc Account, alias string) { tbl.Write([]string{id, acc.Name, alias}) }) + tbl.Flush() } @@ -290,8 +307,8 @@ func (c *Config) UpdateAccounts(entries []Account) { c.Accounts.ReplaceWith(entries) } -func (c *Config) DumpAccounts(w io.Writer) { - c.Accounts.WriteTable(w) +func (c *Config) DumpAccounts(w io.Writer, withHeaders bool) { + c.Accounts.WriteTable(w, withHeaders) } func EnsureConfigFileExists(fp string) (io.ReadWriteCloser, error) { diff --git a/cli/get.go b/cli/get.go index b5824b6d..c59231a4 100644 --- a/cli/get.go +++ b/cli/get.go @@ -5,7 +5,6 @@ import ( "os" "time" - "github.com/RobotsAndPencils/go-saml" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sts" @@ -109,11 +108,12 @@ A role must be specified when using this command through the --role flag. You ma return nil } - if roleName == "" && account.MostRecentRole != "" { + if roleName == "" { + if account.MostRecentRole == "" { + cmd.PrintErrln("You must specify the --role flag with this command") + return nil + } roleName = account.MostRecentRole - } else { - cmd.PrintErrln("You must specify the --role flag with this command") - return nil } if config.TimeRemaining != 0 && timeRemaining == DefaultTimeRemaining { @@ -143,14 +143,14 @@ A role must be specified when using this command through the --role flag. You ma return nil } - assertionBytes, err := ExchangeWebSSOTokenForSAMLAssertion(cmd.Context(), client, oidcDomain, tok) + assertion, err := ExchangeWebSSOTokenForSAMLAssertion(cmd.Context(), client, oidcDomain, tok) if err != nil { cmd.PrintErrf("failed to fetch SAML assertion: %s\n", err) return nil } - assertionStr := string(assertionBytes) - samlResponse, err := saml.ParseEncodedResponse(assertionStr) + assertionStr := string(assertion) + samlResponse, err := ParseBase64EncodedSAMLResponse(assertionStr) if err != nil { cmd.PrintErrf("could not parse assertion: %s\n", err) return nil diff --git a/cli/login.go b/cli/login.go index 26651263..92c9683a 100644 --- a/cli/login.go +++ b/cli/login.go @@ -2,17 +2,24 @@ package main import ( "context" + "fmt" "os" + "github.com/pkg/browser" "github.com/spf13/cobra" "github.com/spf13/pflag" + "golang.org/x/exp/slog" "golang.org/x/oauth2" ) -var FlagURLOnly = "url-only" +var ( + FlagURLOnly = "url-only" + FlagNoBrowser = "no-browser" +) func init() { loginCmd.Flags().BoolP(FlagURLOnly, "u", false, "Print only the URL to visit rather than a user-friendly message") + loginCmd.Flags().BoolP(FlagNoBrowser, "b", false, "Do not open a browser window, printing the URL instead") } // ShouldUseMachineOutput indicates whether or not we should write to standard output as if the user is a machine. @@ -38,8 +45,17 @@ var loginCmd = &cobra.Command{ oidcDomain, _ := cmd.Flags().GetString(FlagOIDCDomain) clientID, _ := cmd.Flags().GetString(FlagClientID) urlOnly, _ := cmd.Flags().GetBool(FlagURLOnly) - isMachineOutput := ShouldUseMachineOutput(cmd.Flags()) || urlOnly - token, err := Login(cmd.Context(), oidcDomain, clientID, isMachineOutput) + + var outputMode LoginOutputMode = LoginOutputModeBrowser{} + if noBrowser, _ := cmd.Flags().GetBool(FlagNoBrowser); noBrowser { + if ShouldUseMachineOutput(cmd.Flags()) || urlOnly { + outputMode = LoginOutputModeURLOnly{} + } else { + outputMode = LoginOutputModeHumanFriendlyMessage{} + } + } + + token, err := Login(cmd.Context(), oidcDomain, clientID, outputMode) if err != nil { return err } @@ -48,7 +64,7 @@ var loginCmd = &cobra.Command{ }, } -func Login(ctx context.Context, domain, clientID string, machineOutput bool) (*oauth2.Token, error) { +func Login(ctx context.Context, domain, clientID string, outputMode LoginOutputMode) (*oauth2.Token, error) { oauthCfg, err := DiscoverOAuth2Config(ctx, domain, clientID) if err != nil { return nil, err @@ -64,5 +80,30 @@ func Login(ctx context.Context, domain, clientID string, machineOutput bool) (*o return nil, err } - return RedirectionFlow(ctx, oauthCfg, state, codeChallenge, codeVerifier, machineOutput) + return RedirectionFlow(ctx, oauthCfg, state, codeChallenge, codeVerifier, outputMode) +} + +type LoginOutputMode interface { + PrintURL(url string) error +} + +type LoginOutputModeBrowser struct{} + +func (LoginOutputModeBrowser) PrintURL(url string) error { + slog.Debug("trying to open browser window", slog.String("url", url)) + return browser.OpenURL(url) +} + +type LoginOutputModeURLOnly struct{} + +func (LoginOutputModeURLOnly) PrintURL(url string) error { + fmt.Fprintln(os.Stdout, url) + return nil +} + +type LoginOutputModeHumanFriendlyMessage struct{} + +func (LoginOutputModeHumanFriendlyMessage) PrintURL(url string) error { + fmt.Printf("Visit the following link in your terminal: %s\n", url) + return nil } diff --git a/cli/oauth2.go b/cli/oauth2.go index 7307033c..dee77baa 100644 --- a/cli/oauth2.go +++ b/cli/oauth2.go @@ -19,6 +19,10 @@ import ( "golang.org/x/oauth2" ) +// stateBufSize is the size of the buffer used to generate the state parameter. +// 43 is a magic number - It generates states that are not too short or long for Okta's validation. +const stateBufSize = 43 + func NewHTTPClient() *http.Client { // Some Darwin systems require certs to be loaded from the system certificate store or attempts to verify SSL certs on internal websites may fail. tr := http.DefaultTransport @@ -136,7 +140,7 @@ func (e OAuth2Error) Error() string { } func GenerateCodeVerifierAndChallenge() (string, string, error) { - codeVerifierBuf := make([]byte, 43) + codeVerifierBuf := make([]byte, stateBufSize) rand.Read(codeVerifierBuf) codeVerifier := base64.RawURLEncoding.EncodeToString(codeVerifierBuf) codeChallengeHash := sha256.Sum256([]byte(codeVerifier)) @@ -145,12 +149,12 @@ func GenerateCodeVerifierAndChallenge() (string, string, error) { } func GenerateState() (string, error) { - stateBuf := make([]byte, 43) + stateBuf := make([]byte, stateBufSize) rand.Read(stateBuf) return base64.URLEncoding.EncodeToString([]byte(stateBuf)), nil } -func RedirectionFlow(ctx context.Context, oauthCfg *oauth2.Config, state, codeChallenge, codeVerifier string, machineOutput bool) (*oauth2.Token, error) { +func RedirectionFlow(ctx context.Context, oauthCfg *oauth2.Config, state, codeChallenge, codeVerifier string, outputMode LoginOutputMode) (*oauth2.Token, error) { listener := NewOAuth2Listener() go listener.Listen(ctx) oauthCfg.RedirectURL = "http://localhost:57468" @@ -159,10 +163,9 @@ func RedirectionFlow(ctx context.Context, oauthCfg *oauth2.Config, state, codeCh oauth2.SetAuthURLParam("code_challenge", codeChallenge), ) - if machineOutput { - fmt.Println(url) - } else { - fmt.Printf("Visit the following link in your terminal: %s\n", url) + if err := outputMode.PrintURL(url); err != nil { + // This is unlikely to ever happen + return nil, fmt.Errorf("failed to display link: %w", err) } code, err := listener.WaitForAuthorizationCode(ctx, state) diff --git a/cli/roles.go b/cli/roles.go index 9f291ec8..59ea49e2 100644 --- a/cli/roles.go +++ b/cli/roles.go @@ -1,7 +1,6 @@ package main import ( - "github.com/RobotsAndPencils/go-saml" "github.com/spf13/cobra" ) @@ -44,8 +43,7 @@ var rolesCmd = cobra.Command{ return nil } - assertionStr := string(assertionBytes) - samlResponse, err := saml.ParseEncodedResponse(assertionStr) + samlResponse, err := ParseBase64EncodedSAMLResponse(string(assertionBytes)) if err != nil { cmd.PrintErrf("could not parse assertion: %s\n", err) return nil diff --git a/cli/saml.go b/cli/saml.go index 0b973a44..575e980e 100644 --- a/cli/saml.go +++ b/cli/saml.go @@ -96,3 +96,7 @@ func getARN(value string) RoleProviderPair { } return p } + +func ParseBase64EncodedSAMLResponse(xml string) (*saml.Response, error) { + return saml.ParseEncodedResponse(xml) +} diff --git a/cli/saml_test.go b/cli/saml_test.go index 3797a1e7..904b8f9a 100644 --- a/cli/saml_test.go +++ b/cli/saml_test.go @@ -8,7 +8,7 @@ import ( ) func TestAwsFindRoleDoesntBreakIfYouHaveMultipleRoles(t *testing.T) { - resp := saml.Response{} + var resp saml.Response resp.AddAttribute("https://aws.amazon.com/SAML/Attributes/Role", "arn:cloud:iam::1234:saml-provider/Okta,arn:cloud:iam::1234:role/Admin") resp.AddAttribute("https://aws.amazon.com/SAML/Attributes/Role", "arn:cloud:iam::1234:saml-provider/Okta,arn:cloud:iam::1234:role/Power") pair, err := FindRoleInSAML("Power", &resp) diff --git a/example.env b/example.env index 6d9a449e..80829838 100644 --- a/example.env +++ b/example.env @@ -11,7 +11,6 @@ export S3_TF_BUCKET_NAME='' export S3_TF_BUCKET_TAGS="TagSet=[{Key=Name,Value=keyconjurer}]" export S3_FRONTEND_BUCKET='' -export BINARY_NAME='keyconjurer' export LOGSTASH_ENDPOINT='' export SECRETS_RETRIEVER='kms_blob' diff --git a/frontend/config/env.js b/frontend/config/env.js new file mode 100644 index 00000000..ffa7e496 --- /dev/null +++ b/frontend/config/env.js @@ -0,0 +1,104 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); + +// Make sure that including paths.js after env.js will read .env variables. +delete require.cache[require.resolve('./paths')]; + +const NODE_ENV = process.env.NODE_ENV; +if (!NODE_ENV) { + throw new Error( + 'The NODE_ENV environment variable is required but was not specified.' + ); +} + +// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +const dotenvFiles = [ + `${paths.dotenv}.${NODE_ENV}.local`, + // Don't include `.env.local` for `test` environment + // since normally you expect tests to produce the same + // results for everyone + NODE_ENV !== 'test' && `${paths.dotenv}.local`, + `${paths.dotenv}.${NODE_ENV}`, + paths.dotenv, +].filter(Boolean); + +// Load environment variables from .env* files. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. Variable expansion is supported in .env files. +// https://github.com/motdotla/dotenv +// https://github.com/motdotla/dotenv-expand +dotenvFiles.forEach(dotenvFile => { + if (fs.existsSync(dotenvFile)) { + require('dotenv-expand')( + require('dotenv').config({ + path: dotenvFile, + }) + ); + } +}); + +// We support resolving modules according to `NODE_PATH`. +// This lets you use absolute paths in imports inside large monorepos: +// https://github.com/facebook/create-react-app/issues/253. +// It works similar to `NODE_PATH` in Node itself: +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders +// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. +// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. +// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 +// We also resolve them to make sure all tools using them work consistently. +const appDirectory = fs.realpathSync(process.cwd()); +process.env.NODE_PATH = (process.env.NODE_PATH || '') + .split(path.delimiter) + .filter(folder => folder && !path.isAbsolute(folder)) + .map(folder => path.resolve(appDirectory, folder)) + .join(path.delimiter); + +// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be +// injected into the application via DefinePlugin in webpack configuration. +const REACT_APP = /^REACT_APP_/i; + +function getClientEnvironment(publicUrl) { + const raw = Object.keys(process.env) + .filter(key => REACT_APP.test(key)) + .reduce( + (env, key) => { + env[key] = process.env[key]; + return env; + }, + { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + NODE_ENV: process.env.NODE_ENV || 'development', + // Useful for resolving the correct path to static assets in `public`. + // For example, . + // This should only be used as an escape hatch. Normally you would put + // images into the `src` and `import` them in code to get their paths. + PUBLIC_URL: publicUrl, + // We support configuring the sockjs pathname during development. + // These settings let a developer run multiple simultaneous projects. + // They are used as the connection `hostname`, `pathname` and `port` + // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` + // and `sockPort` options in webpack-dev-server. + WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, + WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, + WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, + // Whether or not react-refresh is enabled. + // It is defined here so it is available in the webpackHotDevClient. + FAST_REFRESH: process.env.FAST_REFRESH !== 'false', + } + ); + // Stringify all values so we can feed into webpack DefinePlugin + const stringified = { + 'process.env': Object.keys(raw).reduce((env, key) => { + env[key] = JSON.stringify(raw[key]); + return env; + }, {}), + }; + + return { raw, stringified }; +} + +module.exports = getClientEnvironment; diff --git a/frontend/config/getHttpsConfig.js b/frontend/config/getHttpsConfig.js new file mode 100644 index 00000000..013d493c --- /dev/null +++ b/frontend/config/getHttpsConfig.js @@ -0,0 +1,66 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const chalk = require('react-dev-utils/chalk'); +const paths = require('./paths'); + +// Ensure the certificate and key provided are valid and if not +// throw an easy to debug error +function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { + let encrypted; + try { + // publicEncrypt will throw an error with an invalid cert + encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); + } catch (err) { + throw new Error( + `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` + ); + } + + try { + // privateDecrypt will throw an error with an invalid key + crypto.privateDecrypt(key, encrypted); + } catch (err) { + throw new Error( + `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ + err.message + }` + ); + } +} + +// Read file and throw an error if it doesn't exist +function readEnvFile(file, type) { + if (!fs.existsSync(file)) { + throw new Error( + `You specified ${chalk.cyan( + type + )} in your env, but the file "${chalk.yellow(file)}" can't be found.` + ); + } + return fs.readFileSync(file); +} + +// Get the https config +// Return cert files if provided in env, otherwise just true or false +function getHttpsConfig() { + const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; + const isHttps = HTTPS === 'true'; + + if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { + const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); + const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); + const config = { + cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), + key: readEnvFile(keyFile, 'SSL_KEY_FILE'), + }; + + validateKeyAndCerts({ ...config, keyFile, crtFile }); + return config; + } + return isHttps; +} + +module.exports = getHttpsConfig; diff --git a/frontend/config/jest/babelTransform.js b/frontend/config/jest/babelTransform.js new file mode 100644 index 00000000..5b391e40 --- /dev/null +++ b/frontend/config/jest/babelTransform.js @@ -0,0 +1,29 @@ +'use strict'; + +const babelJest = require('babel-jest').default; + +const hasJsxRuntime = (() => { + if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { + return false; + } + + try { + require.resolve('react/jsx-runtime'); + return true; + } catch (e) { + return false; + } +})(); + +module.exports = babelJest.createTransformer({ + presets: [ + [ + require.resolve('babel-preset-react-app'), + { + runtime: hasJsxRuntime ? 'automatic' : 'classic', + }, + ], + ], + babelrc: false, + configFile: false, +}); diff --git a/frontend/config/jest/cssTransform.js b/frontend/config/jest/cssTransform.js new file mode 100644 index 00000000..8f651148 --- /dev/null +++ b/frontend/config/jest/cssTransform.js @@ -0,0 +1,14 @@ +'use strict'; + +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process() { + return 'module.exports = {};'; + }, + getCacheKey() { + // The output is always the same. + return 'cssTransform'; + }, +}; diff --git a/frontend/config/jest/fileTransform.js b/frontend/config/jest/fileTransform.js new file mode 100644 index 00000000..aab67618 --- /dev/null +++ b/frontend/config/jest/fileTransform.js @@ -0,0 +1,40 @@ +'use strict'; + +const path = require('path'); +const camelcase = require('camelcase'); + +// This is a custom Jest transformer turning file imports into filenames. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.svg$/)) { + // Based on how SVGR generates a component name: + // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 + const pascalCaseFilename = camelcase(path.parse(filename).name, { + pascalCase: true, + }); + const componentName = `Svg${pascalCaseFilename}`; + return `const React = require('react'); + module.exports = { + __esModule: true, + default: ${assetFilename}, + ReactComponent: React.forwardRef(function ${componentName}(props, ref) { + return { + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: ref, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }; + }), + };`; + } + + return `module.exports = ${assetFilename};`; + }, +}; diff --git a/frontend/config/modules.js b/frontend/config/modules.js new file mode 100644 index 00000000..d63e41d7 --- /dev/null +++ b/frontend/config/modules.js @@ -0,0 +1,134 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); +const chalk = require('react-dev-utils/chalk'); +const resolve = require('resolve'); + +/** + * Get additional module paths based on the baseUrl of a compilerOptions object. + * + * @param {Object} options + */ +function getAdditionalModulePaths(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return ''; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + // We don't need to do anything if `baseUrl` is set to `node_modules`. This is + // the default behavior. + if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { + return null; + } + + // Allow the user set the `baseUrl` to `appSrc`. + if (path.relative(paths.appSrc, baseUrlResolved) === '') { + return [paths.appSrc]; + } + + // If the path is equal to the root directory we ignore it here. + // We don't want to allow importing from the root directly as source files are + // not transpiled outside of `src`. We do allow importing them with the + // absolute path (e.g. `src/Components/Button.js`) but we set that up with + // an alias. + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return null; + } + + // Otherwise, throw an error. + throw new Error( + chalk.red.bold( + "Your project's `baseUrl` can only be set to `src` or `node_modules`." + + ' Create React App does not support other values at this time.' + ) + ); +} + +/** + * Get webpack aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getWebpackAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + src: paths.appSrc, + }; + } +} + +/** + * Get jest aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getJestAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + '^src/(.*)$': '/src/$1', + }; + } +} + +function getModules() { + // Check if TypeScript is setup + const hasTsConfig = fs.existsSync(paths.appTsConfig); + const hasJsConfig = fs.existsSync(paths.appJsConfig); + + if (hasTsConfig && hasJsConfig) { + throw new Error( + 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' + ); + } + + let config; + + // If there's a tsconfig.json we assume it's a + // TypeScript project and set up the config + // based on tsconfig.json + if (hasTsConfig) { + const ts = require(resolve.sync('typescript', { + basedir: paths.appNodeModules, + })); + config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; + // Otherwise we'll check if there is jsconfig.json + // for non TS projects. + } else if (hasJsConfig) { + config = require(paths.appJsConfig); + } + + config = config || {}; + const options = config.compilerOptions || {}; + + const additionalModulePaths = getAdditionalModulePaths(options); + + return { + additionalModulePaths: additionalModulePaths, + webpackAliases: getWebpackAliases(options), + jestAliases: getJestAliases(options), + hasTsConfig, + }; +} + +module.exports = getModules(); diff --git a/frontend/config/paths.js b/frontend/config/paths.js new file mode 100644 index 00000000..f0a6cd9c --- /dev/null +++ b/frontend/config/paths.js @@ -0,0 +1,77 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); + +// Make sure any symlinks in the project folder are resolved: +// https://github.com/facebook/create-react-app/issues/637 +const appDirectory = fs.realpathSync(process.cwd()); +const resolveApp = relativePath => path.resolve(appDirectory, relativePath); + +// We use `PUBLIC_URL` environment variable or "homepage" field to infer +// "public path" at which the app is served. +// webpack needs to know it to put the right