Skip to content

Commit

Permalink
feat: support SSO (Single Sign-On) (#1010)
Browse files Browse the repository at this point in the history
* feat(authenticate): support pkce

* refactor: authentication

* fix: acquire session token from api endpoint

* feat(undocumented): support debug output

* fix(app): avoid auth for config command

* fix: acquire api token

* feat(auth): implement refresh behaviour

* refactor: move FastlyAPIClient out of app package

* refactor: variable naming

* refactor: display profile name

* refactor: clarify tokenless commands

* doc(testing): explain delve debugging

* tests: fix

* refactor(app): move kingpin config

* refactor(app): small doc clean-ups

* refactor: support flag/env-var override for accounts endpoint

* refactor: move token exchange to separate function

* refactor: move auth logic to separate functions

* fix(config): update config version

* refactor(app): make prompt less scary

* test: fix whoami test

* fix: support skipping OAuth

* refactor(app): move more logic from Run function

* refactor(app): move more logic to separate functions + support new env var

* fix(profile/create): support OAuth token flow

* refactor(authenticate): move new default logic to separate function

* fix: allow profile override for authenticate command + skip auth prompt

* fix: allow profile override for profile update command

* refactor(profile/update): move static token flow to separate function

* fix(profile/update): support OAuth flow

* fix(app): move endpoint display before token validation

* fix: avoid breaking change by switching SSO to be opt-in

* refactor(app): group token processing logic

* refactor(profile/create): update prompt description

* fix: profile logic

* fix: resolve semgrep concern for modified variable

* refactor(authenticate): move profile logic to separate function

* fix(app): wrap prompt in flag checks

* doc(authenticate): explain prompt skip

* doc(authenticate): explain processProfiles logic

* refactor(authenticate): move case conditionals to separate functions

* doc(profile/update): clarify else if conditional

* fix: set Default correctly when invoking authenticate command directly

* refactor: clean-up profile.Get interface

* fix: refactor profile.Default interface

* refactor: rename OAuth to SSO

* refactor: inject browser opener behaviour

* refactor: inject auth server

* test(authenticate): add validation for authenticate command

* doc(app): clarify env var usage

* fix: vcl/conditions test after rebase from main

* refactor(app): hide token message with --quiet

* feat: add Important text header

* feat(profile): add SetADefault

* test(authenticate): add more test assertions

* refactor: support profile arg to authenticate command

* refactor: rename authenticate command to sso

* doc: rename DEVELOP to DEVELOPMENT

* fix: split command name to avoid arguments being included

* fix: use correct profile name after authentication

* refactor: rename all instances of authenticate to sso

* refactor(app): reword instructions for SSO opt-in

* refactor(profile/update): avoid excessive config writes

* refactor(sso): change text function depending on how command is invoked

* feat(sso): validate azp/aud claims

* fix: resolve linter feedback

* refactor:  rename testcase field

* test: add more tests to validate pre-command token processing

* refactor: move INFO output related to expired tokens to verbose mode

* fix: set Authorization header alongside Fastly-Key

* doc: warn callers of memory concern with undocumented.Call()

* refactor(whoami): replace manual request with existing abstraction

* feat: FASTLY_DEBUG_MODE

* refactor: remove unnecessary profile check

* fix(undocumented): print correct output

* refactor: deduplicate JWT validation and token logic

* fix(profile/update): ensure SetDefault() is called

* fix: avoid breaking flow in profile commands

* fix(profile/update): put back the flow to how it was prior to sso

* fix(testutil): correct annotations

* fix(undocumented): move response output to before error check

* style(profile/update): add line breaks to info output

* fix(config): remove duplicate key

* fix(app): move verbose flag out of signature

* fix(app): add compute metadata to the no token switch

* fix(tests): remove extra line break

* fix(undocumented): remove unnecessary Authorization header

* fix(config): bump config_version

* refactor: rename Endpoint to APIEndpoint for clarity

* style(app): add line breaks

* doc(auth): add well-known path

* style: tweak line breaks

* fix(sso): hide command until GA

* remove: SSO messaging until GA

* refactor(global): rename constants

* refactor(profile): hide sso flag until GA

* fix(auth): check type assert

* refactor(main): rename s to authServer

* feat: store .well-known inside of auth.Server

* refactor: store well-known as struct not bytes

* refactor: rename Account to AccountEndpoint

* refactor: move main logic to custom init

* refactor: correct some linter items

* refactor: all the things

* fix: move functions inside auth server + set api endpoint

* remove(testutil): SetAccountEndpoint mock method

* fix: stop processing if user doesn't want to continue

* test: add DontWantOutput for TestSSO

* feat: add --enable-sso flag

* fix: don't os.Exit(1) for Yes/No

* style(app): remove line break

* fix(app): don't auto SSO if no profiles

* refactor: auth flow

* refactor: naming of variables

* feat: support account endpoint override
  • Loading branch information
Integralist authored Nov 15, 2023
1 parent 4e9fe46 commit 60e53e3
Show file tree
Hide file tree
Showing 400 changed files with 6,234 additions and 4,218 deletions.
3 changes: 2 additions & 1 deletion .fastly/config.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
config_version = 4
config_version = 5

[fastly]
account_endpoint = "https://accounts.fastly.com"
api_endpoint = "https://api.fastly.com"

[wasm-metadata]
Expand Down
5 changes: 0 additions & 5 deletions .tmpl/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ type CreateCommand struct {

// Exec invokes the application logic for the command.
func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error {
_, s := c.Globals.Token()
if s == config.SourceUndefined {
return errors.ErrNoToken
}

serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{
AutoCloneFlag: c.autoClone,
Client: c.Globals.Client,
Expand Down
5 changes: 0 additions & 5 deletions .tmpl/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ type DeleteCommand struct {

// Exec invokes the application logic for the command.
func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error {
_, s := c.Globals.Token()
if s == config.SourceUndefined {
return errors.ErrNoToken
}

serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{
AutoCloneFlag: c.autoClone,
Client: c.Globals.Client,
Expand Down
5 changes: 0 additions & 5 deletions .tmpl/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ type DescribeCommand struct {

// Exec invokes the application logic for the command.
func (c *DescribeCommand) Exec(in io.Reader, out io.Writer) error {
_, s := c.Globals.Token()
if s == config.SourceUndefined {
return errors.ErrNoToken
}

serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{
AllowActiveLocked: true,
Client: c.Globals.Client,
Expand Down
5 changes: 0 additions & 5 deletions .tmpl/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,6 @@ type ListCommand struct {

// Exec invokes the application logic for the command.
func (c *ListCommand) Exec(in io.Reader, out io.Writer) error {
_, s := c.Globals.Token()
if s == config.SourceUndefined {
return errors.ErrNoToken
}

serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{
AllowActiveLocked: true,
Client: c.Globals.Client,
Expand Down
5 changes: 0 additions & 5 deletions .tmpl/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,6 @@ type UpdateCommand struct {

// Exec invokes the application logic for the command.
func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error {
_, s := c.Globals.Token()
if s == config.SourceUndefined {
return errors.ErrNoToken
}

serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{
AutoCloneFlag: c.autoClone,
Client: c.Globals.Client,
Expand Down
File renamed without changes.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
</div>

## Quick links

- [Installation](https://developer.fastly.com/learning/tools/cli#installing)
- [Shell auto-completion](https://developer.fastly.com/learning/tools/cli#shell-auto-completion)
- [Configuring](https://developer.fastly.com/learning/tools/cli#configuring)
- [Commands](https://developer.fastly.com/reference/cli/#command-groups)
- [Development](DEVELOP.md)
- [Development](DEVELOPMENT.md)
- [Testing](TESTING.md)
- [Documentation](DOCUMENTATION.md)

Expand All @@ -27,8 +28,6 @@ Refer to [CONTRIBUTING.md](./CONTRIBUTING.md)
If you encounter any non-security-related bug or unexpected behavior, please [file an issue][bug]
using the bug report template.

[bug]: https://github.com/fastly/cli/issues/new?labels=bug&template=bug_report.md

Please also check the [CHANGELOG](./CHANGELOG.md) for any breaking-changes or migration guidance.

### Security issues
Expand All @@ -38,3 +37,5 @@ Please see our [SECURITY.md](SECURITY.md) for guidance on reporting security-rel
## License

[Apache 2.0](LICENSE).

[bug]: https://github.com/fastly/cli/issues/new?labels=bug&template=bug_report.md
18 changes: 18 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,21 @@ TEST_COMPUTE_INIT=1 TEST_COMPUTE_BUILD=1 TEST_COMPUTE_DEPLOY=1 TEST_COMMAND=gote
```

> **NOTE**: `TEST_COMMAND` is optional and allows the use of https://github.com/rakyll/gotest to improve test output.
### Debugging

To debug failing tests you can use [Delve](<>).

Essentially, `cd` into a package directory (where the `_test.go` file is you want to run) and then execute...

```
TEST_COMPUTE_BUILD=1 dlv test -- -test.v -test.run TestNameGoesHere
```

Once that is done, you can set breakpoints. For example:

```
break ../../app/run.go:152
```

> **NOTE:** The path is relative to the package directory you're running the test file.
134 changes: 12 additions & 122 deletions cmd/fastly/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,148 +3,38 @@ package main

import (
"errors"
"io"
"net/http"
"os"
"time"

"github.com/fastly/go-fastly/v8/fastly"
"github.com/fatih/color"

"github.com/fastly/cli/pkg/api"
"github.com/fastly/cli/pkg/app"
"github.com/fastly/cli/pkg/commands/compute"
"github.com/fastly/cli/pkg/config"
"github.com/fastly/cli/pkg/env"
fsterr "github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/github"
"github.com/fastly/cli/pkg/manifest"
"github.com/fastly/cli/pkg/sync"
"github.com/fastly/cli/pkg/text"
)

func main() {
// Parse the arguments provided by the user via the command-line interface.
args := os.Args[1:]

// Define a HTTP client that will be used for making arbitrary HTTP requests.
httpClient := &http.Client{Timeout: time.Minute * 2}

// Define the standard input/output streams.
var (
in io.Reader = os.Stdin
out io.Writer = sync.NewWriter(color.Output)
)

// Read relevant configuration options from the user's environment.
var e config.Environment
e.Read(env.Parse(os.Environ()))

// Identify verbose flag early (before Kingpin parser has executed) so we can
// print additional output related to the CLI configuration.
var verboseOutput bool
for _, seg := range args {
if seg == "-v" || seg == "--verbose" {
verboseOutput = true
}
}

// Identify auto-yes/non-interactive flag early (before Kingpin parser has
// executed) so we can handle the interactive prompts appropriately with
// regards to processing the CLI configuration.
var autoYes, nonInteractive bool
for _, seg := range args {
if seg == "-y" || seg == "--auto-yes" {
autoYes = true
}
if seg == "-i" || seg == "--non-interactive" {
nonInteractive = true
if err := app.Run(os.Args, os.Stdin); err != nil {
if skipExit := processErr(err, os.Args); skipExit {
return
}
}

// Extract a subset of configuration options from the local app directory.
var cfg config.File
cfg.SetAutoYes(autoYes)
cfg.SetNonInteractive(nonInteractive)
// The CLI relies on a valid configuration, otherwise we can't continue.
err := cfg.Read(config.FilePath, in, out, fsterr.Log, verboseOutput)
if err != nil {
fsterr.Deduce(err).Print(color.Error)
// WARNING: os.Exit will exit, and any `defer` calls will not be run.
os.Exit(1)
}
}

// Extract user's project configuration from the fastly.toml manifest.
var md manifest.Data
md.File.Args = args
md.File.SetErrLog(fsterr.Log)
md.File.SetOutput(out)

// NOTE: We skip handling the error because not all commands relate to Compute.
_ = md.File.Read(manifest.Filename)

// The `main` function is a shim for calling `app.Run()`.
err = app.Run(app.RunOpts{
APIClient: func(token, endpoint string, debugMode bool) (api.Interface, error) {
client, err := fastly.NewClientForEndpoint(token, endpoint)
if debugMode {
client.DebugMode = true
}
return client, err
},
Args: args,
ConfigFile: cfg,
ConfigPath: config.FilePath,
Env: e,
ErrLog: fsterr.Log,
ExecuteWasmTools: compute.ExecuteWasmTools,
HTTPClient: httpClient,
Manifest: &md,
Stdin: in,
Stdout: out,
Versioners: app.Versioners{
CLI: github.New(github.Opts{
HTTPClient: httpClient,
Org: "fastly",
Repo: "cli",
Binary: "fastly",
}),
Viceroy: github.New(github.Opts{
HTTPClient: httpClient,
Org: "fastly",
Repo: "viceroy",
Binary: "viceroy",
Version: md.File.LocalServer.ViceroyVersion,
}),
WasmTools: github.New(github.Opts{
HTTPClient: httpClient,
Org: "bytecodealliance",
Repo: "wasm-tools",
Binary: "wasm-tools",
External: true,
Nested: true,
}),
},
})

// processErr persists the error log to disk and deduces the error type.
func processErr(err error, args []string) (skipExit bool) {
// NOTE: We persist any error log entries to disk before attempting to handle
// a possible error response from app.Run as there could be errors recorded
// during the execution flow but were otherwise handled without bubbling an
// error back the call stack, and so if the user still experiences something
// unexpected we will have a record of any errors that happened along the way.
logErr := fsterr.Log.Persist(fsterr.LogPath, args)
logErr := fsterr.Log.Persist(fsterr.LogPath, args[1:])
if logErr != nil {
fsterr.Deduce(logErr).Print(color.Error)
}
if err != nil {
text.Break(out)
fsterr.Deduce(err).Print(color.Error)
exitError := fsterr.SkipExitError{}
if errors.As(err, &exitError) {
if exitError.Skip {
return // skip returning an error for 'help' output
}
}
os.Exit(1)
exitError := fsterr.SkipExitError{}
if errors.As(err, &exitError) {
return exitError.Skip
}
fsterr.Deduce(err).Print(color.Error)
return false
}
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ require (

require (
github.com/fastly/go-fastly/v8 v8.6.4
github.com/hashicorp/cap v0.3.4
github.com/kennygrant/sanitize v1.2.4
github.com/mholt/archiver v3.1.1+incompatible
github.com/otiai10/copy v1.14.0
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/theckman/yacspin v0.13.12
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
Expand All @@ -47,23 +49,34 @@ require (

require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/coreos/go-oidc/v3 v3.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/jsonapi v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.4.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/klauspost/compress v1.16.6 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/nwaples/rardecode v1.1.2 // indirect
github.com/peterhellberg/link v1.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.5.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/text v0.14.0
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 60e53e3

Please sign in to comment.