pax_global_header00006660000000000000000000000064143377126540014526gustar00rootroot0000000000000052 comment=403c6c1e6be4ec6c5255d5b5942143d94c769792 golang-step-cli-utils-0.7.5+ds/000077500000000000000000000000001433771265400162645ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/.github/000077500000000000000000000000001433771265400176245ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/.github/workflows/000077500000000000000000000000001433771265400216615ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/.github/workflows/test.yml000066400000000000000000000036611433771265400233710ustar00rootroot00000000000000name: Lint, Test on: push: pull_request: jobs: lint: name: lint runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: 'latest' # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: golangci-lint command line arguments. args: --timeout=30m # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true # Optional: if set to true then the action will use pre-installed Go. # skip-go-installation: true # Optional: if set to true then the action don't cache or restore ~/go/pkg. # skip-pkg-cache: true # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. # skip-build-cache: true backwardCompatTest: name: test runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 - name: Setup Go uses: actions/setup-go@v2 with: go-version: 1.16 - name: Test id: test run: V=1 make ci test: name: test runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 - name: Setup Go uses: actions/setup-go@v2 with: go-version: 1.17 - name: Test id: test run: V=1 make ci - name: Codecov uses: codecov/codecov-action@v1.2.1 with: file: ./coverage.out # optional name: codecov-umbrella # optional fail_ci_if_error: true # optional (default = false) golang-step-cli-utils-0.7.5+ds/.gitignore000066400000000000000000000004251433771265400202550ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ .envrc golang-step-cli-utils-0.7.5+ds/.golangci.yml000066400000000000000000000031721433771265400206530ustar00rootroot00000000000000linters-settings: govet: check-shadowing: true settings: printf: funcs: - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf golint: min-confidence: 0 gocyclo: min-complexity: 10 maligned: suggest-new: true dupl: threshold: 100 goconst: min-len: 2 min-occurrences: 2 depguard: list-type: blacklist packages: # logging is allowed only by logutils.Log, logrus # is allowed to use only in logutils package - github.com/sirupsen/logrus misspell: locale: US lll: line-length: 140 goimports: local-prefixes: github.com/golangci/golangci-lint gocritic: enabled-tags: - performance - style - experimental - diagnostic disabled-checks: - commentFormatting - commentedOutCode - hugeParam - octalLiteral - rangeValCopy - tooManyResultsChecker - unnamedResult linters: disable-all: true enable: - deadcode - gocritic - gofmt - gosimple - govet - ineffassign - misspell - revive - staticcheck - structcheck - unused run: skip-dirs: - pkg issues: exclude: - declaration of "err" shadows declaration at line - should have a package comment, unless it's in another file for this package - func `CLICommand. - error strings should not be capitalized or end with punctuation or a newline golang-step-cli-utils-0.7.5+ds/LICENSE000066400000000000000000000261351433771265400173000ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. golang-step-cli-utils-0.7.5+ds/Makefile000066400000000000000000000013461433771265400177300ustar00rootroot00000000000000# Set V to 1 for verbose output from the Makefile Q=$(if $V,,@) all: lint test ci: test .PHONY: all ci ######################################### # Bootstrapping ######################################### bootstrap: $Q GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.30.0 .PHONY: bootstrap ######################################### # Test ######################################### test: $Q $(GOFLAGS) go test -coverprofile=coverage.out ./... race: $Q $(GOFLAGS) go test -race ./... .PHONY: test race ######################################### # Linting ######################################### fmt: $Q gofmt -l -w $(SRC) lint: $Q LOG_LEVEL=error golangci-lint run --timeout=30m .PHONY: lint fmt golang-step-cli-utils-0.7.5+ds/README.md000066400000000000000000000017071433771265400175500ustar00rootroot00000000000000# cli-utils [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Build Status](https://travis-ci.com/smallstep/crypto.svg?branch=master)](https://travis-ci.com/smallstep/cli-utils) [![Documentation](https://godoc.org/go.step.sm/crypto?status.svg)](https://pkg.go.dev/mod/go.step.sm/cli-utils) Cli-utils is a collection of packages used in [smallstep](https://smallstep.com) products. See: * [step](https://github.com/smallstep/cli): A zero trust swiss army knife for working with X509, OAuth, JWT, OATH OTP, etc. * [step-ca](https://github.com/smallstep/certificates): A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH. > ⚠️: Other projects should not use this package. The API can change at any time. ## Usage To add this to a project just run: ```sh go get go.step.sm/cli-utils ``` golang-step-cli-utils-0.7.5+ds/command/000077500000000000000000000000001433771265400177025ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/command/command.go000066400000000000000000000016031433771265400216470ustar00rootroot00000000000000package command import ( "os" "github.com/urfave/cli" "go.step.sm/cli-utils/step" "go.step.sm/cli-utils/usage" ) var cmds []cli.Command var currentContext *cli.Context func init() { os.Unsetenv(step.IgnoreEnvVar) cmds = []cli.Command{ usage.HelpCommand(), } } // Register adds the given command to the global list of commands. // It sets recursively the command Flags environment variables. func Register(c cli.Command) { step.SetEnvVar(&c) cmds = append(cmds, c) } // Retrieve returns all commands func Retrieve() []cli.Command { return cmds } // ActionFunc returns a cli.ActionFunc that stores the context. func ActionFunc(fn cli.ActionFunc) cli.ActionFunc { return func(ctx *cli.Context) error { currentContext = ctx return fn(ctx) } } // IsForce returns if the force flag was passed func IsForce() bool { return currentContext != nil && currentContext.Bool("force") } golang-step-cli-utils-0.7.5+ds/command/version/000077500000000000000000000000001433771265400213675ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/command/version/version.go000066400000000000000000000011171433771265400234030ustar00rootroot00000000000000package version import ( "fmt" "github.com/urfave/cli" "go.step.sm/cli-utils/command" "go.step.sm/cli-utils/step" ) func init() { cmd := cli.Command{ Name: "version", Usage: "display the current version of the cli", UsageText: "**step version**", Description: `**step version** prints the version of the cli.`, Action: Command, } command.Register(cmd) } // Command prints out the current version of the tool func Command(c *cli.Context) error { fmt.Printf("%s\n", step.Version()) fmt.Printf("Release Date: %s\n", step.ReleaseDate()) return nil } golang-step-cli-utils-0.7.5+ds/errs/000077500000000000000000000000001433771265400172375ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/errs/errs.go000066400000000000000000000267421433771265400205540ustar00rootroot00000000000000package errs import ( "fmt" "os" "strings" "errors" pkge "github.com/pkg/errors" "github.com/urfave/cli" ) // NewError returns a new Error for the given format and arguments func NewError(format string, args ...interface{}) error { return fmt.Errorf(format, args...) } // NewExitError returns an error that the urfave/cli package will handle and // will show the given error and exit with the given code. func NewExitError(err error, exitCode int) error { return cli.NewExitError(err, exitCode) } // Wrap returns a new error wrapped by the given error with the given message. // If the given error implements the errors.Cause interface, the base error is // used. If the given error is wrapped by a package name, the error wrapped // will be the string after the last colon. // // TODO: this should be refactored to use Go's wrap facilities. func Wrap(err error, format string, args ...interface{}) error { if err == nil { return nil } cause := pkge.Cause(err) if cause == err { str := err.Error() if i := strings.LastIndexByte(str, ':'); i >= 0 { str = strings.TrimSpace(str[i:]) return pkge.Wrapf(errors.New(str), format, args...) } } return pkge.Wrapf(cause, format, args...) } // InsecureCommand returns an error with a message saying that the current // command requires the insecure flag. func InsecureCommand(ctx *cli.Context) error { return fmt.Errorf("'%s %s' requires the '--insecure' flag", ctx.App.Name, ctx.Command.Name) } // EqualArguments returns an error saying that the given positional arguments // cannot be equal. func EqualArguments(ctx *cli.Context, arg1, arg2 string) error { return fmt.Errorf("positional arguments <%s> and <%s> cannot be equal in '%s'", arg1, arg2, usage(ctx)) } // MissingArguments returns an error with a missing arguments message for the // given positional argument names. func MissingArguments(ctx *cli.Context, argNames ...string) error { switch len(argNames) { case 0: return fmt.Errorf("missing positional arguments in '%s'", usage(ctx)) case 1: return fmt.Errorf("missing positional argument <%s> in '%s'", argNames[0], usage(ctx)) default: args := make([]string, len(argNames)) for i, name := range argNames { args[i] = "<" + name + ">" } return fmt.Errorf("missing positional arguments %s in '%s'", strings.Join(args, " "), usage(ctx)) } } // NumberOfArguments returns nil if the number of positional arguments is // equal to the required one. It will return an appropriate error if they are // not. func NumberOfArguments(ctx *cli.Context, required int) error { n := ctx.NArg() switch { case n < required: return TooFewArguments(ctx) case n > required: return TooManyArguments(ctx) default: return nil } } // MinMaxNumberOfArguments returns nil if the number of positional arguments // between the min/max range. It will return an appropriate error if they are // not. func MinMaxNumberOfArguments(ctx *cli.Context, min, max int) error { n := ctx.NArg() switch { case n < min: return TooFewArguments(ctx) case n > max: return TooManyArguments(ctx) default: return nil } } // TooFewArguments returns an error with a few arguments were provided message. func TooFewArguments(ctx *cli.Context) error { return fmt.Errorf("not enough positional arguments were provided in '%s'", usage(ctx)) } // TooManyArguments returns an error with a too many arguments were provided // message. func TooManyArguments(ctx *cli.Context) error { return fmt.Errorf("too many positional arguments were provided in '%s'", usage(ctx)) } // InsecureArgument returns an error with the given argument requiring the // --insecure flag. func InsecureArgument(ctx *cli.Context, name string) error { return fmt.Errorf("positional argument <%s> requires the '--insecure' flag", name) } // FlagValueInsecure returns an error with the given flag and value requiring // the --insecure flag. func FlagValueInsecure(ctx *cli.Context, flag, value string) error { return fmt.Errorf("flag '--%s %s' requires the '--insecure' flag", flag, value) } // InvalidFlagValue returns an error with the given value being missing or // invalid for the given flag. Optionally it lists the given formatted options // at the end. func InvalidFlagValue(ctx *cli.Context, flag, value, options string) error { if options != "" { options = fmt.Sprintf("options are %s", options) } return InvalidFlagValueMsg(ctx, flag, value, options) } // InvalidFlagValueMsg returns an error with the given value being missing or // invalid for the given flag. Optionally it returns an error message to aid // in debugging. func InvalidFlagValueMsg(ctx *cli.Context, flag, value, msg string) error { var format string if value == "" { format = fmt.Sprintf("missing value for flag '--%s'", flag) } else { format = fmt.Sprintf("invalid value '%s' for flag '--%s'", value, flag) } if msg == "" { return errors.New(format) } return errors.New(format + "; " + msg) } // IncompatibleFlag returns an error with the flag being incompatible with the // given value. func IncompatibleFlag(ctx *cli.Context, flag, value string) error { return fmt.Errorf("flag '--%s' is incompatible with '%s'", flag, value) } // IncompatibleFlagWithFlag returns an error with the flag being incompatible with the // given value. func IncompatibleFlagWithFlag(ctx *cli.Context, flag, withFlag string) error { return fmt.Errorf("flag '--%s' is incompatible with '--%s'", flag, withFlag) } // IncompatibleFlagValue returns an error with the flag being incompatible with the // given value. func IncompatibleFlagValue(ctx *cli.Context, flag, incompatibleWith, incompatibleWithValue string) error { return fmt.Errorf("flag '--%s' is incompatible with flag '--%s %s'", flag, incompatibleWith, incompatibleWithValue) } // IncompatibleFlagValues returns an error with the flag being incompatible with the // given value. func IncompatibleFlagValues(ctx *cli.Context, flag, value, incompatibleWith, incompatibleWithValue string) error { return IncompatibleFlagValueWithFlagValue(ctx, flag, value, incompatibleWith, incompatibleWithValue, "") } // IncompatibleFlagValueWithFlagValue returns an error with the given value // being missing or invalid for the given flag. Optionally it lists the given // formatted options at the end. func IncompatibleFlagValueWithFlagValue(ctx *cli.Context, flag, value, withFlag, withValue, options string) error { format := fmt.Sprintf("flag '--%s %s' is incompatible with flag '--%s %s'", flag, value, withFlag, withValue) if options == "" { return errors.New(format) } // TODO: check whether double space before options is intended return fmt.Errorf("%s\n\n Option(s): --%s %s", format, withFlag, options) } // RequiredFlag returns an error with the required flag message. func RequiredFlag(ctx *cli.Context, flag string) error { return fmt.Errorf("'%s %s' requires the '--%s' flag", ctx.App.HelpName, ctx.Command.Name, flag) } // RequiredWithFlag returns an error with the required flag message with another flag. func RequiredWithFlag(ctx *cli.Context, flag, required string) error { return fmt.Errorf("flag '--%s' requires the '--%s' flag", flag, required) } // RequiredWithFlagValue returns an error with the required flag message. func RequiredWithFlagValue(ctx *cli.Context, flag, value, required string) error { return fmt.Errorf("'--%s %s' requires the '--%s' flag", flag, value, required) } // RequiredWithProvisionerTypeFlag returns an error with the required flag message. func RequiredWithProvisionerTypeFlag(ctx *cli.Context, provisionerType, required string) error { return fmt.Errorf("provisioner type '%s' requires the '--%s' flag", provisionerType, required) } // RequiredInsecureFlag returns an error with the given flag requiring the // insecure flag message. func RequiredInsecureFlag(ctx *cli.Context, flag string) error { return fmt.Errorf("flag '--%s' requires the '--insecure' flag", flag) } // RequiredSubtleFlag returns an error with the given flag requiring the // subtle flag message.. func RequiredSubtleFlag(ctx *cli.Context, flag string) error { return fmt.Errorf("flag '--%s' requires the '--subtle' flag", flag) } // RequiredUnlessInsecureFlag returns an error with the required flag message unless // the insecure flag is used. func RequiredUnlessInsecureFlag(ctx *cli.Context, flag string) error { return RequiredUnlessFlag(ctx, flag, "insecure") } // RequiredUnlessFlag returns an error with the required flag message unless // the specified flag is used. func RequiredUnlessFlag(ctx *cli.Context, flag, unlessFlag string) error { return fmt.Errorf("flag '--%s' is required unless the '--%s' flag is provided", flag, unlessFlag) } // RequiredUnlessSubtleFlag returns an error with the required flag message unless // the subtle flag is used. func RequiredUnlessSubtleFlag(ctx *cli.Context, flag string) error { return RequiredUnlessFlag(ctx, flag, "subtle") } // RequiredOrFlag returns an error with a list of flags being required messages. func RequiredOrFlag(ctx *cli.Context, flags ...string) error { params := make([]string, len(flags)) for i, flag := range flags { params[i] = "--" + flag } return fmt.Errorf("one of flag %s is required", strings.Join(params, " or ")) } // RequiredWithOrFlag returns an error with a list of flags at least one of which // is required in conjunction with the last flag in the list. func RequiredWithOrFlag(ctx *cli.Context, withFlag string, flags ...string) error { params := make([]string, len(flags)) for i := 0; i < len(flags); i++ { params[i] = "--" + flags[i] } return fmt.Errorf("one of flag %s is required with flag --%s", strings.Join(params, " or "), withFlag) } // MinSizeFlag returns an error with a greater or equal message message for // the given flag and size. func MinSizeFlag(ctx *cli.Context, flag, size string) error { return fmt.Errorf("flag '--%s' must be greater than or equal to %s", flag, size) } // MinSizeInsecureFlag returns an error with a requiring --insecure flag // message with the given flag an size. func MinSizeInsecureFlag(ctx *cli.Context, flag, size string) error { return fmt.Errorf("flag '--%s' requires at least %s unless '--insecure' flag is provided", flag, size) } // MutuallyExclusiveFlags returns an error with mutually exclusive message for // the given flags. func MutuallyExclusiveFlags(ctx *cli.Context, flag1, flag2 string) error { return fmt.Errorf("flag '--%s' and flag '--%s' are mutually exclusive", flag1, flag2) } // UnsupportedFlag returns an error with a message saying that the given flag is // not yet supported. func UnsupportedFlag(ctx *cli.Context, flag string) error { return fmt.Errorf("flag '--%s' is not yet supported", flag) } // usage returns the command usage text if set or a default usage string. func usage(ctx *cli.Context) string { if ctx.Command.UsageText == "" { return fmt.Sprintf("%s %s [command options]", ctx.App.HelpName, ctx.Command.Name) } // keep just the first line and remove markdown lines := strings.Split(ctx.Command.UsageText, "\n") return strings.ReplaceAll(lines[0], "**", "") } // FileError is a wrapper for errors of the os package. func FileError(err error, filename string) error { if err == nil { return nil } switch e := err.(type) { case *os.PathError: return fmt.Errorf("%s %s failed: %w", e.Op, e.Path, e.Err) case *os.LinkError: return fmt.Errorf("%s %s %s failed: %w", e.Op, e.Old, e.New, e.Err) case *os.SyscallError: return fmt.Errorf("%s failed: %w", e.Syscall, e.Err) default: return Wrap(err, "unexpected error on %s", filename) } } // FriendlyError is an interface for returning friendly error messages to the user. type FriendlyError interface { Message() string } golang-step-cli-utils-0.7.5+ds/errs/errs_test.go000066400000000000000000000220511433771265400216000ustar00rootroot00000000000000package errs import ( "errors" "flag" "io" "os" "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli" ) func TestNewError(t *testing.T) { err := NewError("error was: %w", io.EOF) assert.EqualError(t, err, `error was: EOF`) assert.True(t, errors.Is(err, io.EOF)) } func TestNewExitError(t *testing.T) { err := NewExitError(assert.AnError, 12) assert.EqualError(t, err, assert.AnError.Error()) var ee *cli.ExitError require.True(t, errors.As(err, &ee)) assert.Equal(t, 12, ee.ExitCode()) } func TestInsecureCommand(t *testing.T) { const exp = `'app cmd' requires the '--insecure' flag` ctx := newTestCLI(t) assert.EqualError(t, InsecureCommand(ctx), exp) } func TestEqualArguments(t *testing.T) { const exp = `positional arguments and cannot be equal in 'app cmd [command options]'` ctx := newTestCLI(t) assert.EqualError(t, EqualArguments(ctx, "arg1", "arg2"), exp) } func TestMissingArguments(t *testing.T) { cases := []struct { args []string exp string }{ 0: { exp: "missing positional arguments in 'app cmd [command options]'", }, 1: { args: []string{"arg1"}, exp: "missing positional argument in 'app cmd [command options]'", }, 2: { args: []string{"arg1", "arg2"}, exp: "missing positional arguments in 'app cmd [command options]'", }, } for caseIndex, kase := range cases { t.Run(strconv.Itoa(caseIndex), func(t *testing.T) { ctx := newTestCLI(t, kase.args...) assert.EqualError(t, MissingArguments(ctx, kase.args...), kase.exp) }) } } func TestNumberOfArguments(t *testing.T) { ctx := newTestCLI(t, "arg1", "arg2") cases := map[int]string{ 0: "too many positional arguments were provided in 'app cmd [command options]'", 1: "too many positional arguments were provided in 'app cmd [command options]'", 2: "", 3: "not enough positional arguments were provided in 'app cmd [command options]'", } for n := range cases { exp := cases[n] t.Run(strconv.Itoa(n), func(t *testing.T) { if exp == "" { assert.NoError(t, NumberOfArguments(ctx, n)) } else { assert.EqualError(t, NumberOfArguments(ctx, n), exp) } }) } } func TestMinMaxNumberOfArguments(t *testing.T) { ctx := newTestCLI(t, "arg1", "arg2") cases := []struct { min int max int exp string }{ 0: {0, 1, "too many positional arguments were provided in 'app cmd [command options]'"}, 1: {1, 3, ""}, 2: {3, 4, "not enough positional arguments were provided in 'app cmd [command options]'"}, } for caseIndex := range cases { kase := cases[caseIndex] t.Run(strconv.Itoa(caseIndex), func(t *testing.T) { got := MinMaxNumberOfArguments(ctx, kase.min, kase.max) if kase.exp == "" { assert.NoError(t, got) } else { assert.EqualError(t, got, kase.exp) } }) } } func TestInsecureArgument(t *testing.T) { const exp = `positional argument requires the '--insecure' flag` ctx := newTestCLI(t) assert.EqualError(t, InsecureArgument(ctx, "arg"), exp) } func TestFlagValueInsecure(t *testing.T) { const exp = `flag '--flag1 value2' requires the '--insecure' flag` ctx := newTestCLI(t) assert.EqualError(t, FlagValueInsecure(ctx, "flag1", "value2"), exp) } func TestInvalidFlagValue(t *testing.T) { ctx := newTestCLI(t) cases := []struct { value string options string exp string }{ 0: { exp: `missing value for flag '--myflag'`, }, 1: { value: "val2", exp: `invalid value 'val2' for flag '--myflag'`, }, 2: { options: `'val3'`, exp: `missing value for flag '--myflag'; options are 'val3'`, }, 3: { value: "val2", options: `'val3', 'val4'`, exp: `invalid value 'val2' for flag '--myflag'; options are 'val3', 'val4'`, }, } for caseIndex := range cases { kase := cases[caseIndex] t.Run(strconv.Itoa(caseIndex), func(t *testing.T) { got := InvalidFlagValue(ctx, "myflag", kase.value, kase.options) assert.EqualError(t, got, kase.exp) }) } } func TestIncompatibleFlag(t *testing.T) { const exp = `flag '--flag1' is incompatible with '--flag2'` ctx := newTestCLI(t) assert.EqualError(t, IncompatibleFlag(ctx, "flag1", "--flag2"), exp) } func TestIncompatibleFlagWithFlag(t *testing.T) { const exp = `flag '--flag1' is incompatible with '--flag2'` ctx := newTestCLI(t) assert.EqualError(t, IncompatibleFlagWithFlag(ctx, "flag1", "flag2"), exp) } func TestIncompatibleFlagValue(t *testing.T) { const exp = `flag '--flag1' is incompatible with flag '--with2 value3'` ctx := newTestCLI(t) assert.EqualError(t, IncompatibleFlagValue(ctx, "flag1", "with2", "value3"), exp) } func TestIncompatibleFlagValues(t *testing.T) { const exp = `flag '--flag1 value2' is incompatible with flag '--with2 value4'` ctx := newTestCLI(t) assert.EqualError(t, IncompatibleFlagValues(ctx, "flag1", "value2", "with2", "value4"), exp) } func TestIncompatibleFlagValueWithFlagValue(t *testing.T) { const exp = `flag '--flag1 value2' is incompatible with flag '--with2 value4' Option(s): --with2 opt` ctx := newTestCLI(t) assert.EqualError(t, IncompatibleFlagValueWithFlagValue(ctx, "flag1", "value2", "with2", "value4", "opt"), exp) } func TestRequiredFlag(t *testing.T) { const exp = `'app cmd' requires the '--f1' flag` ctx := newTestCLI(t) assert.EqualError(t, RequiredFlag(ctx, "f1"), exp) } func TestRequiredWithFlag(t *testing.T) { const exp = `flag '--f1' requires the '--f2' flag` ctx := newTestCLI(t) assert.EqualError(t, RequiredWithFlag(ctx, "f1", "f2"), exp) } func TestRequiredWithFlagValue(t *testing.T) { const exp = `'--f1 v1' requires the '--f2' flag` ctx := newTestCLI(t) assert.EqualError(t, RequiredWithFlagValue(ctx, "f1", "v1", "f2"), exp) } func TestRequiredWithProvisionerTypeFlag(t *testing.T) { const exp = `provisioner type 'p1' requires the '--f1' flag` ctx := newTestCLI(t) assert.EqualError(t, RequiredWithProvisionerTypeFlag(ctx, "p1", "f1"), exp) } func TestRequiredInsecureFlag(t *testing.T) { const exp = `flag '--f1' requires the '--insecure' flag` ctx := newTestCLI(t) assert.EqualError(t, RequiredInsecureFlag(ctx, "f1"), exp) } func TestRequiredSubtleFlag(t *testing.T) { const exp = `flag '--f1' requires the '--subtle' flag` ctx := newTestCLI(t) assert.EqualError(t, RequiredSubtleFlag(ctx, "f1"), exp) } func TestRequiredUnlessInsecureFlag(t *testing.T) { const exp = `flag '--f1' is required unless the '--insecure' flag is provided` ctx := newTestCLI(t) assert.EqualError(t, RequiredUnlessInsecureFlag(ctx, "f1"), exp) } func TestRequiredUnlessSubtleFlag(t *testing.T) { const exp = `flag '--f1' is required unless the '--subtle' flag is provided` ctx := newTestCLI(t) assert.EqualError(t, RequiredUnlessSubtleFlag(ctx, "f1"), exp) } func TestRequiredOrFlag(t *testing.T) { const exp = `one of flag --f1 or --f2 or --f3 is required` ctx := newTestCLI(t) assert.EqualError(t, RequiredOrFlag(ctx, "f1", "f2", "f3"), exp) } func TestRequiredWithOrFlag(t *testing.T) { const exp = `one of flag --f1 or --f2 or --f3 is required with flag --f4` ctx := newTestCLI(t) assert.EqualError(t, RequiredWithOrFlag(ctx, "f4", "f1", "f2", "f3"), exp) } func TestMinSizeFlag(t *testing.T) { const exp = `flag '--f1' must be greater than or equal to 10` ctx := newTestCLI(t) assert.EqualError(t, MinSizeFlag(ctx, "f1", "10"), exp) } func TestMinSizeInsecureFlag(t *testing.T) { const exp = `flag '--f1' requires at least 10 unless '--insecure' flag is provided` ctx := newTestCLI(t) assert.EqualError(t, MinSizeInsecureFlag(ctx, "f1", "10"), exp) } func TestMutuallyExclusiveFlags(t *testing.T) { const exp = `flag '--f1' and flag '--f2' are mutually exclusive` ctx := newTestCLI(t) assert.EqualError(t, MutuallyExclusiveFlags(ctx, "f1", "f2"), exp) } func TestUnsupportedFlag(t *testing.T) { const exp = `flag '--f1' is not yet supported` ctx := newTestCLI(t) assert.EqualError(t, UnsupportedFlag(ctx, "f1"), exp) } func TestFileError(t *testing.T) { tests := []struct { err error expected string }{ { err: os.NewSyscallError("open", errors.New("out of file descriptors")), expected: "open failed: out of file descriptors", }, { err: func() error { _, err := os.ReadFile("im-fairly-certain-this-file-doesnt-exist") require.Error(t, err) return err }(), expected: "open im-fairly-certain-this-file-doesnt-exist failed", }, { err: func() error { err := os.Link("im-fairly-certain-this-file-doesnt-exist", "neither-does-this") require.Error(t, err) return err }(), expected: "link im-fairly-certain-this-file-doesnt-exist neither-does-this failed", }, } for _, tt := range tests { err := FileError(tt.err, "myfile") require.Error(t, err) require.Contains(t, err.Error(), tt.expected) } } func newTestCLI(t *testing.T, args ...string) *cli.Context { t.Helper() fs := flag.NewFlagSet("cmd", flag.ContinueOnError) require.NoError(t, fs.Parse(args)) app := cli.NewApp() app.Name = "app" app.HelpName = "app" app.Writer = io.Discard app.ErrWriter = io.Discard ctx := cli.NewContext(app, fs, nil) ctx.Command = cli.Command{ Name: "cmd", } return ctx } golang-step-cli-utils-0.7.5+ds/fileutil/000077500000000000000000000000001433771265400201015ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/fileutil/file.go000066400000000000000000000057361433771265400213620ustar00rootroot00000000000000package fileutil import ( "os" "github.com/pkg/errors" ) // File represents a wrapper on os.File that supports read, write, seek and // close methods, but they won't be called if an error occurred before. type File struct { File *os.File err error } // OpenFile calls os.OpenFile method and returns the os.File wrapped. func OpenFile(name string, flag int, perm os.FileMode) (*File, error) { f, err := os.OpenFile(name, flag, perm) if err != nil { return nil, FileError(err, name) } return &File{ File: f, }, nil } // error writes f.err if it's not set and returns f.err. func (f *File) error(err error) error { if f.err == nil && err != nil { f.err = FileError(err, f.File.Name()) } return f.err } // Close wraps `func (*os.File) Close` it will always call Close but the error // return will be the first error thrown if any. func (f *File) Close() error { return f.error(f.File.Close()) } // Read wraps `func (*os.File) Read` but doesn't perform the operation if a // previous error was thrown. func (f *File) Read(b []byte) (n int, err error) { if f.err != nil { return 0, f.err } n, err = f.File.Read(b) return n, f.error(err) } // ReadAt wraps `func (*os.File) ReadAt` but doesn't perform the operation if a // previous error was thrown. func (f *File) ReadAt(b []byte, off int64) (n int, err error) { if f.err != nil { return 0, f.err } n, err = f.File.ReadAt(b, off) return n, f.error(err) } // Seek wraps `func (*os.File) Seek` but doesn't perform the operation if a // previous error was thrown. func (f *File) Seek(offset int64, whence int) (ret int64, err error) { if f.err != nil { return 0, f.err } ret, err = f.File.Seek(offset, whence) return ret, f.error(err) } // Write wraps `func (*os.File) Write` but doesn't perform the operation if a // previous error was thrown. func (f *File) Write(b []byte) (n int, err error) { if f.err != nil { return 0, f.err } n, err = f.File.Write(b) return n, f.error(err) } // WriteAt wraps `func (*os.File) WriteAt` but doesn't perform the operation if // a previous error was thrown. func (f *File) WriteAt(b []byte, off int64) (n int, err error) { if f.err != nil { return 0, f.err } n, err = f.File.WriteAt(b, off) return n, f.error(err) } // WriteString wraps `func (*os.File) WriteString` but doesn't perform the // operation if a previous error was thrown. func (f *File) WriteString(s string) (n int, err error) { if f.err != nil { return 0, f.err } n, err = f.File.WriteString(s) return n, f.error(err) } // FileError is a wrapper for errors of the os package. func FileError(err error, filename string) error { if err == nil { return nil } switch e := err.(type) { case *os.PathError: return errors.Errorf("%s %s failed: %v", e.Op, e.Path, e.Err) case *os.LinkError: return errors.Errorf("%s %s %s failed: %v", e.Op, e.Old, e.New, e.Err) case *os.SyscallError: return errors.Errorf("%s failed: %v", e.Syscall, e.Err) default: return errors.Wrapf(err, "unexpected error on %s", filename) } } golang-step-cli-utils-0.7.5+ds/fileutil/write.go000066400000000000000000000135341433771265400215700ustar00rootroot00000000000000package fileutil import ( "bufio" "bytes" "fmt" "io" "os" "strings" "time" "github.com/pkg/errors" "go.step.sm/cli-utils/command" "go.step.sm/cli-utils/ui" ) var ( // ErrFileExists is the error returned if a file exists. ErrFileExists = errors.New("file exists") // ErrIsDir is the error returned if the file is a directory. ErrIsDir = errors.New("file is a directory") // SnippetHeader is the header of a step generated snippet in a // configuration file. SnippetHeader = "# autogenerated by step" // SnippetFooter is the header of a step generated snippet in a // configuration file. SnippetFooter = "# end" ) // WriteFile wraps ioutil.WriteFile with a prompt to overwrite a file if // the file exists. It returns ErrFileExists if the user picks to not overwrite // the file. If force is set to true, the prompt will not be presented and the // file if exists will be overwritten. func WriteFile(filename string, data []byte, perm os.FileMode) error { if command.IsForce() { return os.WriteFile(filename, data, perm) } st, err := os.Stat(filename) if err != nil { if os.IsNotExist(err) { return os.WriteFile(filename, data, perm) } return errors.Wrapf(err, "error reading information for %s", filename) } if st.IsDir() { return ErrIsDir } str, err := ui.Prompt(fmt.Sprintf("Would you like to overwrite %s [y/n]", filename), ui.WithValidateYesNo()) if err != nil { return err } switch strings.ToLower(strings.TrimSpace(str)) { case "y", "yes": case "n", "no": return ErrFileExists } return os.WriteFile(filename, data, perm) } // AppendNewLine appends the given data at the end of the file. If the last // character of the file does not contain an LF it prepends it to the data. func AppendNewLine(filename string, data []byte, perm os.FileMode) error { f, err := OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, perm) if err != nil { return err } // Read last character if st, err := f.File.Stat(); err == nil && st.Size() != 0 { last := make([]byte, 1) f.Seek(-1, 2) f.Read(last) if last[0] != '\n' { f.WriteString("\n") } } f.Write(data) return f.Close() } func writeChunk(filename string, data []byte, hasHeaderFooter bool, header, footer string, perm os.FileMode) error { // Get file permissions if st, err := os.Stat(filename); err == nil { perm = st.Mode() } else if !os.IsNotExist(err) { return FileError(err, filename) } // Read file contents b, err := os.ReadFile(filename) if err != nil && !os.IsNotExist(err) { return FileError(err, filename) } // Detect previous configuration _, start, end := findConfiguration(bytes.NewReader(b), header, footer) // Replace previous configuration f, err := OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perm) if err != nil { return FileError(err, filename) } if len(b) > 0 { f.Write(b[:start]) if start == end { f.WriteString("\n") } } if !hasHeaderFooter { f.WriteString(fmt.Sprintf("%s @ %s\n", header, time.Now().UTC().Format(time.RFC3339))) } f.Write(data) if !bytes.HasSuffix(data, []byte("\n")) { f.WriteString("\n") } if !hasHeaderFooter { f.WriteString(footer + "\n") } if len(b) > 0 { f.Write(b[end:]) } return f.Close() } // WriteSnippet writes the given data into the given filename. It surrounds the // data with a default header and footer, and it will replace the previous one. func WriteSnippet(filename string, data []byte, perm os.FileMode) error { return writeChunk(filename, data, false, SnippetHeader, SnippetFooter, perm) } // PrependLine prepends the given line into the given filename and removes // other instances of the line in the file. func PrependLine(filename string, data []byte, perm os.FileMode) error { // Get file permissions if st, err := os.Stat(filename); err == nil { perm = st.Mode() } else if !os.IsNotExist(err) { return FileError(err, filename) } // Read file contents b, err := os.ReadFile(filename) if err != nil && !os.IsNotExist(err) { return FileError(err, filename) } line := string(data) result := []string{string(data)} for _, l := range strings.Split(string(b), "\n") { if l != "" && !strings.HasPrefix(l, line) { result = append(result, l) } } if err := os.WriteFile(filename, []byte(strings.Join(result, "\n")), perm); err != nil { return FileError(err, filename) } return nil } // RemoveLine removes a single line which contains the given substring from the // given file. func RemoveLine(filename, substr string) error { var perm os.FileMode // Get file permissions st, err := os.Stat(filename) switch { case os.IsNotExist(err): return nil case err != nil: return FileError(err, filename) default: perm = st.Mode() } // Read file contents b, err := os.ReadFile(filename) if err != nil && !os.IsNotExist(err) { return FileError(err, filename) } old := strings.Split(string(b), "\n") for i, l := range old { if !strings.Contains(l, substr) { continue } if err := os.WriteFile(filename, []byte(strings.Join(append(old[:i], old[i+1:]...), "\n")), perm); err != nil { return FileError(err, filename) } break } return nil } type offsetCounter struct { offset int64 } func (o *offsetCounter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { advance, token, err = bufio.ScanLines(data, atEOF) o.offset += int64(advance) return } func findConfiguration(r io.Reader, header, footer string) (lines []string, start, end int64) { var inConfig bool counter := new(offsetCounter) scanner := bufio.NewScanner(r) scanner.Split(counter.ScanLines) for scanner.Scan() { line := scanner.Text() switch { case !inConfig && strings.HasPrefix(line, header): inConfig = true start = counter.offset - int64(len(line)+1) case inConfig && strings.HasPrefix(line, footer): return lines, start, counter.offset case inConfig: lines = append(lines, line) } } return lines, counter.offset, counter.offset } golang-step-cli-utils-0.7.5+ds/go.mod000066400000000000000000000016471433771265400174020ustar00rootroot00000000000000module go.step.sm/cli-utils go 1.16 require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/google/go-cmp v0.5.2 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.11 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/pkg/errors v0.9.1 github.com/shurcooL/sanitized_anchor_name v1.0.0 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 github.com/stretchr/testify v1.5.1 github.com/urfave/cli v1.22.2 go.step.sm/crypto v0.9.0 golang.org/x/net v0.0.0-20200822124328-c89045814202 golang.org/x/sys v0.0.0-20211031064116-611d5d643895 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) //replace go.step.sm/crypto => ../crypto golang-step-cli-utils-0.7.5+ds/go.sum000066400000000000000000000170471433771265400174300ustar00rootroot00000000000000github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= go.step.sm/crypto v0.9.0 h1:q2AllTSnVj4NRtyEPkGW2ohArLmbGbe6ZAL/VIOKDzA= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211031064116-611d5d643895 h1:iaNpwpnrgL5jzWS0vCNnfa8HqzxveCFpFx3uC/X4Tps= golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= golang-step-cli-utils-0.7.5+ds/step/000077500000000000000000000000001433771265400172375ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/step/config.go000066400000000000000000000141721433771265400210400ustar00rootroot00000000000000package step import ( "fmt" "log" "os" "os/user" "path/filepath" "runtime" "strings" "time" ) // PathEnv defines the name of the environment variable that can overwrite // the default configuration path. const PathEnv = "STEPPATH" // HomeEnv defines the name of the environment variable that can overwrite the // default home directory. const HomeEnv = "HOME" var ( // version and buildTime are filled in during build by the Makefile name = "Smallstep CLI" buildTime = "N/A" version = "N/A" // stepBasePath will be populated in init() with the proper STEPPATH. stepBasePath string // homePath will be populated in init() with the proper HOME. homePath string ) func init() { l := log.New(os.Stderr, "", 0) // Get home path from environment or from the user object. homePath = os.Getenv(HomeEnv) if homePath == "" { usr, err := user.Current() if err == nil && usr.HomeDir != "" { homePath = usr.HomeDir } else { l.Fatalf("Error obtaining home directory, please define environment variable %s.", HomeEnv) } } // Get step path from environment or relative to home. stepBasePath = os.Getenv(PathEnv) if stepBasePath == "" { stepBasePath = filepath.Join(homePath, ".step") } // cleanup homePath = filepath.Clean(homePath) stepBasePath = filepath.Clean(stepBasePath) // Check for presence or attempt to create it if necessary. // // Some environments (e.g. third party docker images) might fail creating // the directory, so this should not panic if it can't. if fi, err := os.Stat(stepBasePath); err != nil { os.MkdirAll(stepBasePath, 0700) } else if !fi.IsDir() { l.Fatalf("File '%s' is not a directory.", stepBasePath) } // Initialize context state. Contexts().Init() } // BasePath returns the base path for the step configuration directory. func BasePath() string { return stepBasePath } // Path returns the path for the step configuration directory. // // 1. If the base step path has a current context configured, then this method // returns the path to the authority configured in the context. // 2. If the base step path does not have a current context configured this // method returns the value defined by the environment variable STEPPATH, OR // 3. If no environment variable is set, this method returns `$HOME/.step`. func Path() string { c := Contexts().GetCurrent() if c == nil { return BasePath() } return filepath.Join(BasePath(), "authorities", c.Authority) } // ProfilePath returns the path for the currently selected profile path. // // 1. If the base step path has a current context configured, then this method // returns the path to the profile configured in the context. // 2. If the base step path does not have a current context configured this // method returns the value defined by the environment variable STEPPATH, OR // 3. If no environment variable is set, this method returns `$HOME/.step`. func ProfilePath() string { c := Contexts().GetCurrent() if c == nil { return BasePath() } return filepath.Join(BasePath(), "profiles", c.Profile) } // IdentityPath returns the location of the identity directory. func IdentityPath() string { return filepath.Join(Path(), "identity") } // IdentityFile returns the location of the identity file. func IdentityFile() string { return filepath.Join(Path(), "config", "identity.json") } // DefaultsFile returns the location of the defaults file at the base of the // authority path. func DefaultsFile() string { return filepath.Join(Path(), "config", "defaults.json") } // ProfileDefaultsFile returns the location of the defaults file at the base // of the profile path. func ProfileDefaultsFile() string { return filepath.Join(ProfilePath(), "config", "defaults.json") } // ConfigPath returns the location of the $(step path)/config directory. func ConfigPath() string { return filepath.Join(Path(), "config") } // ProfileConfigPath returns the location of the $(step path --profile)/config directory. func ProfileConfigPath() string { return filepath.Join(ProfilePath(), "config") } // CaConfigFile returns the location of the ca.json file -- configuration for // connecting to the CA. func CaConfigFile() string { return filepath.Join(Path(), "config", "ca.json") } // ContextsFile returns the location of the config file. func ContextsFile() string { return filepath.Join(BasePath(), "contexts.json") } // CurrentContextFile returns the path to the file containing the current context. func CurrentContextFile() string { return filepath.Join(BasePath(), "current-context.json") } // Home returns the user home directory using the environment variable HOME or // the os/user package. func Home() string { return homePath } // Abs returns the given path relative to the STEPPATH if it's not an // absolute path, relative to the home directory using the special string "~/", // or relative to the working directory using "./" // // Relative paths like 'certs/root_ca.crt' will be converted to // '$STEPPATH/certs/root_ca.crt', but paths like './certs/root_ca.crt' will be // relative to the current directory. Home relative paths like // ~/certs/root_ca.crt will be converted to '$HOME/certs/root_ca.crt'. And // absolute paths like '/certs/root_ca.crt' will remain the same. func Abs(path string) string { if filepath.IsAbs(path) { return path } // Windows accept both \ and / slashed := filepath.ToSlash(path) switch { case strings.HasPrefix(slashed, "~/"): return filepath.Join(homePath, path[2:]) case strings.HasPrefix(slashed, "./"), strings.HasPrefix(slashed, "../"): if abs, err := filepath.Abs(path); err == nil { return abs } return path default: return filepath.Join(Path(), path) } } // Set updates the name, version, and build time func Set(n, v, t string) { name = n version = v buildTime = t } // Version returns the current version of the binary func Version() string { out := version if version == "N/A" { out = "0000000-dev" } return fmt.Sprintf("%s/%s (%s/%s)", name, out, runtime.GOOS, runtime.GOARCH) } // ReleaseDate returns the time of when the binary was built func ReleaseDate() string { out := buildTime if buildTime == "N/A" { out = time.Now().UTC().Format("2006-01-02 15:04 MST") } return out } golang-step-cli-utils-0.7.5+ds/step/context.go000066400000000000000000000322271433771265400212600ustar00rootroot00000000000000package step import ( "encoding/json" "fmt" "os" "path/filepath" "reflect" "sort" "strings" "github.com/pkg/errors" "github.com/urfave/cli" "go.step.sm/cli-utils/errs" "go.step.sm/cli-utils/ui" ) // IgnoreEnvVar is a value added to a flag EnvVar to avoid the use of // environment variables or configuration files. const IgnoreEnvVar = "STEP_IGNORE_ENV_VAR" // Context represents a Step Path configuration context. A context is the // combination of a profile and an authority. type Context struct { Name string `json:"-"` Profile string `json:"profile"` Authority string `json:"authority"` config map[string]interface{} } // Validate validates a context and returns an error if invalid. func (c *Context) Validate() error { suffix := "; check your $STEPPATH/contexts.json file" if c == nil { return errors.Errorf("context cannot be nil%s", suffix) } if c.Authority == "" { return errors.Errorf("context cannot have an empty authority value%s", suffix) } if c.Profile == "" { return errors.Errorf("context cannot have an empty profile value%s", suffix) } return nil } // Path return the base path relative to the context. func (c *Context) Path() string { return filepath.Join(BasePath(), "authorities", c.Authority) } // ProfilePath return the profile base path relative to the context. func (c *Context) ProfilePath() string { return filepath.Join(BasePath(), "profiles", c.Profile) } // DefaultsFile returns the location of the defaults file for the context. func (c *Context) DefaultsFile() string { return filepath.Join(c.Path(), "config", "defaults.json") } // ProfileDefaultsFile returns the location of the defaults file at the base // of the profile path. func (c *Context) ProfileDefaultsFile() string { return filepath.Join(c.ProfilePath(), "config", "defaults.json") } // Load loads the configuration for the given context. func (c *Context) Load() error { c.config = map[string]interface{}{} for _, f := range []string{c.DefaultsFile(), c.ProfileDefaultsFile()} { b, err := os.ReadFile(f) if os.IsNotExist(err) { continue } else if err != nil { return errs.FileError(err, f) } values := make(map[string]interface{}) if err := json.Unmarshal(b, &values); err != nil { return errors.Wrapf(err, "error parsing %s", f) } for k, v := range values { c.config[k] = v } } attributesBannedFromConfig := []string{ "context", "profile", "authority", } for _, attr := range attributesBannedFromConfig { if _, ok := c.config[attr]; ok { ui.Printf("cannot set '%s' attribute in config files\n", attr) delete(c.config, attr) } } return nil } // ContextMap represents the map of available Contexts that is stored // at the base of the Step Path. type ContextMap map[string]*Context type storedCurrent struct { Context string `json:"context"` } // CtxState is the type that manages context state for the cli. type CtxState struct { current *Context contexts ContextMap config map[string]interface{} } var ctxState = &CtxState{} // Init initializes the context map and current context state. func (cs *CtxState) Init() error { if err := cs.initMap(); err != nil { return err } if err := cs.initCurrent(); err != nil { return err } if err := cs.load(); err != nil { return err } return nil } func (cs *CtxState) initMap() error { contextsFile := ContextsFile() b, err := os.ReadFile(contextsFile) if os.IsNotExist(err) { return nil } else if err != nil { return errs.FileError(err, contextsFile) } cs.contexts = ContextMap{} if err := json.Unmarshal(b, &cs.contexts); err != nil { return errors.Wrap(err, "error unmarshaling context map") } for k, ctx := range cs.contexts { if err := ctx.Validate(); err != nil { return errors.Wrapf(err, "error in context '%s'", k) } ctx.Name = k } return nil } func (cs *CtxState) initCurrent() error { currentCtxFile := CurrentContextFile() b, err := os.ReadFile(currentCtxFile) if os.IsNotExist(err) { return nil } else if err != nil { return errs.FileError(err, currentCtxFile) } var sc storedCurrent if err := json.Unmarshal(b, &sc); err != nil { return errors.Wrap(err, "error unmarshaling current context") } return cs.SetCurrent(sc.Context) } func (cs *CtxState) load() error { if cs.Enabled() && cs.GetCurrent() != nil { return cs.GetCurrent().Load() } cs.LoadVintage("") return nil } // LoadVintage loads context configuration from the vintage (non-context) path. func (cs *CtxState) LoadVintage(f string) error { if f == "" { f = DefaultsFile() } b, err := os.ReadFile(f) if os.IsNotExist(err) { return nil } else if err != nil { return errs.FileError(err, f) } cs.config = make(map[string]interface{}) if err := json.Unmarshal(b, &cs.config); err != nil { return errors.Wrapf(err, "error parsing %s", f) } return nil } // GetConfig returns the current context configuration. func (cs *CtxState) GetConfig() (map[string]interface{}, error) { if cs.Enabled() { cur := cs.GetCurrent() if cur == nil { return nil, errors.New("cannot get context configuration; no current context set") } return cur.config, nil } return cs.config, nil } // SetCurrent sets the current context or returns an error if a context // with the given name does not exist. func (cs *CtxState) SetCurrent(name string) error { var ok bool cs.current, ok = cs.contexts[name] if !ok { return errors.Errorf("could not load context '%s'", name) } if len(cs.current.config) == 0 { if err := cs.current.Load(); err != nil { return err } } return nil } type contextSelect struct { Name string Context *Context } // PromptContext gets user input to select a context. func (cs *CtxState) PromptContext() error { var items []*contextSelect for _, context := range cs.List() { items = append(items, &contextSelect{ Name: context.Name, Context: context, }) } var ctxStr string if len(items) == 1 { if err := ui.PrintSelected("Context", items[0].Name); err != nil { return err } ctxStr = items[0].Name } else { i, _, err := ui.Select("Select a context for 'step':", items, ui.WithSelectTemplates(ui.NamedSelectTemplates("Context"))) if err != nil { return err } ctxStr = items[i].Name } if err := cs.SetCurrent(ctxStr); err != nil { return err } return cs.SaveCurrent(ctxStr) } // Enabled returns true if one of the following is true: // - there is a current context configured // - the context map is (list of available contexts) is not empty. func (cs *CtxState) Enabled() bool { return cs.current != nil || len(cs.contexts) > 0 } // Contexts returns an object that enables context management. func Contexts() *CtxState { return ctxState } // Add adds a new context to the context map. If current context is not // set then store the new context as the current context for future commands. func (cs *CtxState) Add(ctx *Context) error { if err := ctx.Validate(); err != nil { return errors.Wrapf(err, "error adding context") } if cs.contexts == nil { cs.contexts = map[string]*Context{ctx.Name: ctx} } else { cs.contexts[ctx.Name] = ctx } b, err := json.MarshalIndent(cs.contexts, "", " ") if err != nil { return err } cf := ContextsFile() if err := os.MkdirAll(filepath.Dir(cf), 0700); err != nil { return errs.FileError(err, cf) } if err := os.WriteFile(cf, b, 0600); err != nil { return errs.FileError(err, cf) } if cs.current == nil { if err := cs.SaveCurrent(ctx.Name); err != nil { return err } } return nil } // GetCurrent returns the current context. func (cs *CtxState) GetCurrent() *Context { return cs.current } // Get returns the context with the given name. func (cs *CtxState) Get(name string) (ctx *Context, ok bool) { ctx, ok = cs.contexts[name] return } // Remove removes a context from the context state. func (cs *CtxState) Remove(name string) error { if _, ok := cs.contexts[name]; !ok { return errors.Errorf("context '%s' not found", name) } if cs.current != nil && cs.current.Name == name { return errors.New("cannot remove current context; use 'step context select' to switch contexts") } delete(cs.contexts, name) b, err := json.MarshalIndent(cs.contexts, "", " ") if err != nil { return err } if err := os.WriteFile(ContextsFile(), b, 0600); err != nil { return err } return nil } // List returns a list of all contexts. func (cs *CtxState) List() []*Context { l := make([]*Context, 0, len(cs.contexts)) for _, v := range cs.contexts { l = append(l, v) } return l } // ListAlphabetical returns a case-insensitive, alphabetically // sorted list of all contexts. func (cs *CtxState) ListAlphabetical() []*Context { l := cs.List() sort.Slice(l, func(i, j int) bool { return strings.ToLower(l[i].Name) < strings.ToLower(l[j].Name) }) return l } // SaveCurrent stores the given context name as the selected default context for // future commands. func (cs *CtxState) SaveCurrent(name string) error { if _, ok := Contexts().Get(name); !ok { return errors.Errorf("context '%s' not found", name) } b, err := json.Marshal(storedCurrent{Context: name}) if err != nil { return err } if err = os.WriteFile(CurrentContextFile(), b, 0644); err != nil { return errs.FileError(err, CurrentContextFile()) } return nil } // Apply the current context configuration to the command line environment. func (cs *CtxState) Apply(ctx *cli.Context) error { cfg, err := cs.GetConfig() if err != nil { return err } for _, f := range ctx.Command.Flags { // Skip if EnvVar == IgnoreEnvVar if getFlagEnvVar(f) == IgnoreEnvVar { continue } for _, name := range strings.Split(f.GetName(), ",") { name = strings.TrimSpace(name) if ctx.IsSet(name) { break } // Set the flag for the first key that matches. if v, ok := cfg[name]; ok { ctx.Set(name, fmt.Sprintf("%v", v)) break } } } return nil } // getEnvVar generates the environment variable for the given flag name. func getEnvVar(name string) string { parts := strings.Split(name, ",") name = strings.TrimSpace(parts[0]) name = strings.ReplaceAll(name, "-", "_") return "STEP_" + strings.ToUpper(name) } // getFlagEnvVar returns the value of the EnvVar field of a flag. func getFlagEnvVar(f cli.Flag) string { v := reflect.ValueOf(f) if v.Kind() == reflect.Ptr { v = v.Elem() } if v.Kind() == reflect.Struct { envVar := v.FieldByName("EnvVar") if envVar.IsValid() { return envVar.String() } } return "" } // SetEnvVar sets the the EnvVar element to each flag recursively. func SetEnvVar(c *cli.Command) { if c == nil { return } // Enable getting the flags from a json file if c.Before == nil && c.Action != nil { c.Before = getConfigVars } // Enable getting the flags from environment variables for i := range c.Flags { envVar := getEnvVar(c.Flags[i].GetName()) switch f := c.Flags[i].(type) { case cli.BoolFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.BoolTFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.DurationFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.Float64Flag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.GenericFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.Int64Flag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.IntFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.IntSliceFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.Int64SliceFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.StringFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.StringSliceFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.Uint64Flag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } case cli.UintFlag: if f.EnvVar == "" { f.EnvVar = envVar c.Flags[i] = f } } } for i := range c.Subcommands { SetEnvVar(&c.Subcommands[i]) } } // GetConfigVars load the defaults.json file and sets the flags if they are not // already set or the EnvVar is set to IgnoreEnvVar. // // TODO(mariano): right now it only supports parameters at first level. func getConfigVars(ctx *cli.Context) (err error) { if ctx.Bool("no-context") { return nil } cs := Contexts() // Load the the current context into memory: // - If contexts enabled then make sure a current context is selected // and loaded. // - If vintage context then check if overwritten by --config flag. if cs.Enabled() { if ctx.IsSet("context") { err = cs.SetCurrent(ctx.String("context")) } else if cs.Enabled() && cs.GetCurrent() == nil { err = cs.PromptContext() } if err != nil { return err } } else if ctx.GlobalString("config") != "" { // Re-load the vintage context configuration if `--config` flag supplied. cs.LoadVintage(ctx.GlobalString("config")) } // TODO: a mock detail because of "add detail/assignee to this TODO/FIXME/BUG comment" lint issue fullCommandName := strings.ToLower(strings.TrimSpace(ctx.Command.FullName())) if strings.EqualFold(fullCommandName, "ca bootstrap-helper") { return nil } if err := cs.Apply(ctx); err != nil { return err } return nil } golang-step-cli-utils-0.7.5+ds/step/context_test.go000066400000000000000000000031061433771265400223110ustar00rootroot00000000000000package step import ( "reflect" "testing" "github.com/pkg/errors" "github.com/smallstep/assert" ) func TestContextValidate(t *testing.T) { type test struct { name string context *Context err error } tests := []test{ {name: "fail/nil", context: nil, err: errors.New("context cannot be nil")}, {name: "fail/empty-authority", context: &Context{}, err: errors.New("context cannot have an empty authority value")}, {name: "fail/empty-profile", context: &Context{Authority: "foo"}, err: errors.New("context cannot have an empty profile value")}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if err := tc.context.Validate(); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { assert.Nil(t, tc.err) } }) } } func TestCtxState_ListAlphabetical(t *testing.T) { aContext := &Context{Name: "A"} bContext := &Context{Name: "b"} cContext := &Context{Name: "C"} type fields struct { contexts ContextMap } tests := []struct { name string fields fields want []*Context }{ { name: "ok", fields: fields{ contexts: ContextMap{ "1": cContext, "2": bContext, "3": aContext, }, }, want: []*Context{ aContext, bContext, cContext, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cs := &CtxState{ contexts: tt.fields.contexts, } if got := cs.ListAlphabetical(); !reflect.DeepEqual(got, tt.want) { t.Errorf("CtxState.ListAlphabetical() = %v, want %v", got, tt.want) } }) } } golang-step-cli-utils-0.7.5+ds/token/000077500000000000000000000000001433771265400174045ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/token/options.go000066400000000000000000000157521433771265400214400ustar00rootroot00000000000000package token import ( "crypto/sha256" "encoding/hex" "time" "github.com/pkg/errors" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" ) // Options is a function that set claims. type Options func(c *Claims) error // WithClaim is an Options function that adds a custom claim to the JWT. func WithClaim(name string, value interface{}) Options { return func(c *Claims) error { if name == "" { return errors.New("name cannot be empty") } c.Set(name, value) return nil } } // WithRootCA returns an Options function that calculates the SHA256 of the // given root certificate to be used in the token claims. If this method it's // not used the default root certificate in the $STEPPATH secrets directory will // be used. func WithRootCA(path string) Options { return func(c *Claims) error { cert, err := pemutil.ReadCertificate(path) if err != nil { return err } sum := sha256.Sum256(cert.Raw) c.Set(RootSHAClaim, hex.EncodeToString(sum[:])) return nil } } // WithSHA returns an Options function that sets the SHA claim to the given // value. func WithSHA(sum string) Options { return func(c *Claims) error { c.Set(RootSHAClaim, sum) return nil } } // WithSANS returns an Options function that sets the list of required SANs // in the token claims. func WithSANS(sans []string) Options { return func(c *Claims) error { c.Set(SANSClaim, sans) return nil } } // WithStep returns an Options function that sets the step claim in the payload. func WithStep(v interface{}) Options { return func(c *Claims) error { c.Set(StepClaim, v) return nil } } // WithSSH returns an Options function that sets the step claim with the ssh // property in the value. func WithSSH(v interface{}) Options { return WithStep(map[string]interface{}{ "ssh": v, }) } // WithIssuedAt sets the 'iat' (IssuedAt) claim. func WithIssuedAt(issuedAt time.Time) Options { return func(c *Claims) error { c.IssuedAt = jose.NewNumericDate(issuedAt) return nil } } // WithValidity validates boundary inputs and sets the 'nbf' (NotBefore) and // 'exp' (expiration) options. If notBefore or expiration are zero time they // will not be set. func WithValidity(notBefore, expiration time.Time) Options { return func(c *Claims) error { now := timeNowUTC() if !notBefore.IsZero() { requestedDelay := notBefore.Sub(now) if requestedDelay > MaxValidityDelay { return errors.Errorf("requested validity delay is too long: 'requested validity delay'=%v, 'max validity delay'=%v", requestedDelay, MaxValidityDelay) } } if !expiration.IsZero() { if !notBefore.IsZero() { if expiration.Before(notBefore) { return errors.Errorf("nbf < exp: nbf=%v, exp=%v", notBefore, expiration) } requestedValidity := expiration.Sub(notBefore) if requestedValidity < MinValidity { return errors.Errorf("requested token validity is too short: 'requested token validity'=%v, 'minimum token validity'=%v", requestedValidity, MinValidity) } else if requestedValidity > MaxValidity { return errors.Errorf("requested token validity is too long: 'requested token validity'=%v, 'maximum token validity'=%v", requestedValidity, MaxValidity) } } } // Zero time will set nbf and exp as nil c.NotBefore = jose.NewNumericDate(notBefore) c.Expiry = jose.NewNumericDate(expiration) return nil } } // WithIssuer returns an Options function that sets the issuer to use in the // token claims. If Issuer is not used the default issuer will be used. func WithIssuer(s string) Options { return func(c *Claims) error { if s == "" { return errors.New("issuer cannot be empty") } c.Issuer = s return nil } } // WithSubject returns an Options that sets the subject to use in the token // claims. func WithSubject(s string) Options { return func(c *Claims) error { if s == "" { return errors.New("subject cannot be empty") } c.Subject = s return nil } } // WithAudience returns a Options that sets the audience to use in the token // claims. If Audience is not used the default audience will be used. func WithAudience(s string) Options { return func(c *Claims) error { if s == "" { return errors.New("audience cannot be empty") } c.Audience = append(jose.Audience{}, s) return nil } } // WithJWTID returns a Options that sets the jwtID to use in the token // claims. If WithJWTID is not used a random identifier will be used. func WithJWTID(s string) Options { return func(c *Claims) error { if s == "" { return errors.New("jwtID cannot be empty") } c.ID = s return nil } } // WithKid returns a Options that sets the header kid claims. // If WithKid is not used a thumbprint using SHA256 will be used. func WithKid(s string) Options { return func(c *Claims) error { if s == "" { return errors.New("kid cannot be empty") } c.SetHeader("kid", s) return nil } } // WithX5CFile returns a Options that sets the header x5c claims. func WithX5CFile(certFile string, key interface{}) Options { return func(c *Claims) error { certs, err := pemutil.ReadCertificateBundle(certFile) if err != nil { return errors.Wrap(err, "error reading certificate") } certStrs, err := jose.ValidateX5C(certs, key) if err != nil { return errors.Wrap(err, "error validating x5c certificate chain and key for use in x5c header") } return WithX5CCerts(certStrs)(c) } } // WithX5CCerts returns a Options that sets the header x5c claims. This method // does not do any validation. CertStrs is a list (chain) where each index // is the base64 encoding of the raw cert. Use WithX5cFile if your code does not // require input of the input parameters. func WithX5CCerts(certStrs []string) Options { return func(c *Claims) error { c.SetHeader("x5c", certStrs) return nil } } // WithX5CInsecureFile returns a Options that sets the header x5cAllowInvalid claims. // The `x5c` claims can only be accessed by running a method on the jose Token // which validates the certificate chain before returning it. This option serves // a use case where the user would prefer not to validate the certificate chain // before returning it. Presumably the user would then perform their own validation. // NOTE: here be dragons. Use WithX5CFile unless you know what you are doing. func WithX5CInsecureFile(certFile string, key interface{}) Options { return func(c *Claims) error { certs, err := pemutil.ReadCertificateBundle(certFile) if err != nil { return errors.Wrap(err, "error reading certificate") } certStrs, err := jose.ValidateX5C(certs, key) if err != nil { return errors.Wrap(err, "error validating x5c certificate chain and key for use in x5c header") } c.SetHeader(jose.X5cInsecureKey, certStrs) return nil } } // WithSSHPOPFile returns a Options that sets the header sshpop claims. func WithSSHPOPFile(certFile string, key interface{}) Options { return func(c *Claims) error { certStrs, err := jose.ValidateSSHPOP(certFile, key) if err != nil { return errors.Wrap(err, "error validating SSH certificate and key for use in sshpop header") } c.SetHeader("sshpop", certStrs) return nil } } golang-step-cli-utils-0.7.5+ds/token/options_test.go000066400000000000000000000114731433771265400224730ustar00rootroot00000000000000package token import ( "encoding/base64" "reflect" "testing" "time" "github.com/smallstep/assert" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" ) func TestOptions(t *testing.T) { var ( empty = new(Claims) now = time.Now() ) temp := timeNowUTC timeNowUTC = now.UTC t.Cleanup(func() { timeNowUTC = temp }) certs, err := pemutil.ReadCertificateBundle("./testdata/foo.crt") assert.FatalError(t, err) certStrs := make([]string, len(certs)) for i, cert := range certs { certStrs[i] = base64.StdEncoding.EncodeToString(cert.Raw) } x5cKey, err := pemutil.Read("./testdata/foo.key") assert.FatalError(t, err) tests := []struct { name string option Options want *Claims wantErr bool }{ {"WithClaim ok", WithClaim("name", "foo"), &Claims{ExtraClaims: map[string]interface{}{"name": "foo"}}, false}, {"WithClaim fail", WithClaim("", "foo"), empty, true}, {"WithRootCA ok", WithRootCA("testdata/ca.crt"), &Claims{ExtraClaims: map[string]interface{}{"sha": "6908751f68290d4573ae0be39a98c8b9b7b7d4e8b2a6694b7509946626adfe98"}}, false}, {"WithRootCA fail", WithRootCA("not-exists"), empty, true}, {"WithIssuedAt", WithIssuedAt(now), &Claims{Claims: jose.Claims{IssuedAt: jose.NewNumericDate(now)}}, false}, {"WithIssuedAt zero", WithIssuedAt(time.Time{}), &Claims{Claims: jose.Claims{IssuedAt: nil}}, false}, {"WithValidity ok", WithValidity(now, now.Add(5*time.Minute)), &Claims{Claims: jose.Claims{NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute))}}, false}, {"WithValidity zero nbf", WithValidity(time.Time{}, now), &Claims{Claims: jose.Claims{NotBefore: nil, Expiry: jose.NewNumericDate(now)}}, false}, {"WithValidity zero exp", WithValidity(now, time.Time{}), &Claims{Claims: jose.Claims{NotBefore: jose.NewNumericDate(now), Expiry: nil}}, false}, {"WithValidity zero", WithValidity(time.Time{}, time.Time{}), &Claims{Claims: jose.Claims{NotBefore: nil, Expiry: nil}}, false}, {"WithValidity fail expired", WithValidity(now, now.Add(-time.Second)), &Claims{Claims: jose.Claims{}}, true}, {"WithValidity fail delay validity", WithValidity(now.Add(MaxValidityDelay+1), now.Add(MaxValidityDelay+5*time.Minute)), &Claims{Claims: jose.Claims{}}, true}, {"WithValidity fail min validity", WithValidity(now, now.Add(MinValidity-1)), &Claims{Claims: jose.Claims{}}, true}, {"WithValidity fail max validity", WithValidity(now, now.Add(MaxValidity+1)), &Claims{Claims: jose.Claims{}}, true}, {"WithRootCA expired", WithValidity(now, now.Add(-1*time.Second)), empty, true}, {"WithRootCA long delay", WithValidity(now.Add(MaxValidityDelay+time.Minute), now.Add(MaxValidityDelay+10*time.Minute)), empty, true}, {"WithRootCA min validity ok", WithValidity(now, now.Add(MinValidity)), &Claims{Claims: jose.Claims{NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(MinValidity))}}, false}, {"WithRootCA min validity fail", WithValidity(now, now.Add(MinValidity-time.Second)), empty, true}, {"WithRootCA max validity ok", WithValidity(now, now.Add(MaxValidity)), &Claims{Claims: jose.Claims{NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(MaxValidity))}}, false}, {"WithRootCA max validity fail", WithValidity(now, now.Add(MaxValidity+time.Second)), empty, true}, {"WithIssuer ok", WithIssuer("value"), &Claims{Claims: jose.Claims{Issuer: "value"}}, false}, {"WithIssuer fail", WithIssuer(""), empty, true}, {"WithSubject ok", WithSubject("value"), &Claims{Claims: jose.Claims{Subject: "value"}}, false}, {"WithSubject fail", WithSubject(""), empty, true}, {"WithAudience ok", WithAudience("value"), &Claims{Claims: jose.Claims{Audience: jose.Audience{"value"}}}, false}, {"WithAudience fail", WithAudience(""), empty, true}, {"WithJWTID ok", WithJWTID("value"), &Claims{Claims: jose.Claims{ID: "value"}}, false}, {"WithJWTID fail", WithJWTID(""), empty, true}, {"WithKid ok", WithKid("value"), &Claims{ExtraHeaders: map[string]interface{}{"kid": "value"}}, false}, {"WithKid fail", WithKid(""), empty, true}, {"WithSHA ok", WithSHA("6908751f68290d4573ae0be39a98c8b9b7b7d4e8b2a6694b7509946626adfe98"), &Claims{ExtraClaims: map[string]interface{}{"sha": "6908751f68290d4573ae0be39a98c8b9b7b7d4e8b2a6694b7509946626adfe98"}}, false}, {"WithX5CCerts ok", WithX5CCerts(certStrs), &Claims{ExtraHeaders: map[string]interface{}{"x5c": certStrs}}, false}, {"WithX5CFile ok", WithX5CFile("./testdata/foo.crt", x5cKey), &Claims{ExtraHeaders: map[string]interface{}{"x5c": certStrs}}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { claim := new(Claims) err := tt.option(claim) if (err != nil) != tt.wantErr { t.Errorf("Options error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(claim, tt.want) { t.Errorf("Options claims = %v, want %v", claim, tt.want) } }) } } golang-step-cli-utils-0.7.5+ds/token/parse.go000066400000000000000000000151111433771265400210440ustar00rootroot00000000000000package token import ( "encoding/json" "regexp" "strings" "time" "github.com/pkg/errors" "go.step.sm/crypto/jose" ) // Type indicates the token Type. type Type int // Token types supported. const ( Unknown Type = iota JWK // Smallstep JWK X5C // Smallstep JWK with x5c header OIDC // OpenID Connect GCP // Google Cloud Platform AWS // Amazon Web Services Azure // Microsoft Azure K8sSA // Kubernetes Service Account ) // JSONWebToken represents a JSON Web Token (as specified in RFC7519). Using the // Parse or ParseInsecure it will contain the payloads supported on step ca. type JSONWebToken struct { *jose.JSONWebToken Payload Payload } // Payload represents public claim values (as specified in RFC 7519). In // addition to the standard claims it contains the ones supported in step ca. type Payload struct { jose.Claims SHA string `json:"sha"` // JWK token claims SANs []string `json:"sans"` // ... AtHash string `json:"at_hash"` // OIDC token claims AuthorizedParty string `json:"azp"` // ... Email string `json:"email"` EmailVerified bool `json:"email_verified"` Hd string `json:"hd"` Nonce string `json:"nonce"` AppID string `json:"appid"` // Azure token claims AppIDAcr string `json:"appidacr"` // ... IdentityProvider string `json:"idp"` ObjectID string `json:"oid"` TenantID string `json:"tid"` Version interface{} `json:"ver"` XMSMirID string `json:"xms_mirid"` K8sSANamespace string `json:"kubernetes.io/serviceaccount/namespace,omitempty"` K8sSASecretName string `json:"kubernetes.io/serviceaccount/secret.name,omitempty"` K8sSAServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name,omitempty"` K8sSAServiceAccountUID string `json:"kubernetes.io/serviceaccount/service-account.uid,omitempty"` Google *GCPGooglePayload `json:"google"` // GCP token claims Amazon *AWSAmazonPayload `json:"amazon"` // AWS token claims Azure *AzurePayload `json:"azure"` // Azure token claims } // Type returns the type of the payload. func (p Payload) Type() Type { switch { case p.Google != nil: return GCP case p.Amazon != nil: return AWS case p.Azure != nil: return Azure case p.Issuer == "kubernetes/serviceaccount": return K8sSA case len(p.SHA) > 0 || len(p.SANs) > 0: return JWK case p.Email != "": return OIDC default: return Unknown } } // GCPGooglePayload represents the Google payload in GCP. type GCPGooglePayload struct { ComputeEngine GCPComputeEnginePayload `json:"compute_engine"` } // GCPComputeEnginePayload represents the Google ComputeEngine payload in GCP. type GCPComputeEnginePayload struct { InstanceID string `json:"instance_id"` InstanceName string `json:"instance_name"` InstanceCreationTimestamp *jose.NumericDate `json:"instance_creation_timestamp"` ProjectID string `json:"project_id"` ProjectNumber int64 `json:"project_number"` Zone string `json:"zone"` LicenseID []string `json:"license_id"` } // AWSAmazonPayload represents the Amazon payload for a AWS token. type AWSAmazonPayload struct { Document []byte `json:"document"` Signature []byte `json:"signature"` InstanceIdentityDocument *AWSInstanceIdentityDocument `json:"-"` } // AWSInstanceIdentityDocument is the JSON representation of the instance // identity document. type AWSInstanceIdentityDocument struct { AccountID string `json:"accountId"` Architecture string `json:"architecture"` AvailabilityZone string `json:"availabilityZone"` BillingProducts []string `json:"billingProducts"` DevpayProductCodes []string `json:"devpayProductCodes"` ImageID string `json:"imageId"` InstanceID string `json:"instanceId"` InstanceType string `json:"instanceType"` KernelID string `json:"kernelId"` PendingTime time.Time `json:"pendingTime"` PrivateIP string `json:"privateIp"` RamdiskID string `json:"ramdiskId"` Region string `json:"region"` Version string `json:"version"` } // azureXMSMirIDRegExp is the regular expression used to parse the xms_mirid claim. // Using case insensitive as resourceGroups appears as resourcegroups. var azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachines/([^/]+)$`) // AzurePayload contains the information in the xms_mirid claim. type AzurePayload struct { SubscriptionID string ResourceGroup string VirtualMachine string } // Parse parses the given token verifying the signature with the key. func Parse(token string, key interface{}) (*JSONWebToken, error) { jwt, err := jose.ParseSigned(token) if err != nil { return nil, errors.Wrap(err, "error parsing token") } var p Payload if err := jwt.Claims(key, &p); err != nil { return nil, errors.Wrap(err, "error parsing token claims") } return parseResponse(jwt, p) } // ParseInsecure parses the given token. func ParseInsecure(token string) (*JSONWebToken, error) { jwt, err := jose.ParseSigned(token) if err != nil { return nil, errors.Wrap(err, "error parsing token") } var p Payload if err := jwt.UnsafeClaimsWithoutVerification(&p); err != nil { return nil, errors.Wrap(err, "error parsing token claims") } return parseResponse(jwt, p) } func parseResponse(jwt *jose.JSONWebToken, p Payload) (*JSONWebToken, error) { switch { case p.Type() == AWS: if err := json.Unmarshal(p.Amazon.Document, &p.Amazon.InstanceIdentityDocument); err != nil { return nil, errors.Wrap(err, "error unmarshaling instance identity document") } case strings.HasPrefix(p.Issuer, "https://sts.windows.net/"): if re := azureXMSMirIDRegExp.FindStringSubmatch(p.XMSMirID); len(re) > 0 { p.Azure = &AzurePayload{ SubscriptionID: re[1], ResourceGroup: re[2], VirtualMachine: re[3], } } } return &JSONWebToken{ JSONWebToken: jwt, Payload: p, }, nil } golang-step-cli-utils-0.7.5+ds/token/parse_test.go000066400000000000000000000342741433771265400221160ustar00rootroot00000000000000package token import ( "encoding/base64" "encoding/json" "reflect" "testing" "time" "go.step.sm/crypto/jose" ) const ( jwkToken = "eyJhbGciOiJFUzI1NiIsImtpZCI6ImpPMzdkdERia3UtUW5hYnM1VlIwWXc2WUZGdjl3ZUExOGRwM2h0dmRFanMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2NhLnNtYWxsc3RlcC5jb206OTAwMC8xLjAvc2lnbiIsImV4cCI6MTU1NTU0OTk1NywiaWF0IjoxNTU1NTQ5NjU3LCJpc3MiOiJtYXJpYW5vQHNtYWxsc3RlcC5jb20iLCJqdGkiOiJkNThmOWM0YmJhNmVjNTZhYTA1Yzc2N2I4MGE2NjQyMjBlYzZmZGJlYjVjYmVjYmU0OTI2MGRiNjc5NWRkODFlIiwibmJmIjoxNTU1NTQ5NjU3LCJzYW5zIjpbImZvby5iYXIuemFyIl0sInNoYSI6IjJkZDRmN2Q2NTNjNWE0ZjE4MjQ4YTkwM2M2ZTNkNjdiMTcwNDkyMzQxOWM5Zjc0NGZkNjgwZmM2NTI0MmYzYjIiLCJzdWIiOiJmb28uYmFyLnphciJ9.8GmY7dnjFQiXCABjD01n0-3hw4pNwMwdGBLxR4Qx1-pCr6PNlrZaN44QIsDfsi70hZFVdlG4l5MjGU8r4OopIg" jwkJWKSet = `{ "keys": [ { "use": "sig", "kty": "EC", "kid": "jO37dtDbku-Qnabs5VR0Yw6YFFv9weA18dp3htvdEjs", "crv": "P-256", "alg": "ES256", "x": "vo6GTwfXryV5WDI-_JL1FeK0k2AvWwUnSbtdSE3IQl0", "y": "Z4j_nNmETqTsKq-6ZCjyCIIMNE_308Mx866z3pD6sJ0" } ] }` oidcToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjM3ODJkM2YwYmM4OTAwOGQ5ZDJjMDE3MzBmNzY1Y2ZiMTlkM2I3MGUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxMDg3MTYwNDg4NDIwLThxdDdiYXZnM3Flc2RoczZpdDgyNG1obmZnY2ZlOGlsLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiMTA4NzE2MDQ4ODQyMC04cXQ3YmF2ZzNxZXNkaHM2aXQ4MjRtaG5mZ2NmZThpbC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEwNDQyMzU1MDUwMjY2NTkyMDEwMiIsImhkIjoic21hbGxzdGVwLmNvbSIsImVtYWlsIjoibWFyaWFub0BzbWFsbHN0ZXAuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJZVTJUQVN4LW42NjBqUWZOb0h2SHlnIiwibm9uY2UiOiI4ZWRhMDU2YjMyMjIwYWRjYjZhMjcxNjM1NzFkZjI1M2E3NjM5ZjM0YWQ2NDQ0MWIyNDI5YTNkZTJjNWQ3ZWFmIiwiaWF0IjoxNTU1NTUwNjY3LCJleHAiOjE1NTU1NTQyNjd9.LZ0OymOWMt59aqdsDr8TorxJ-J3ZpCjUjnA8m-TabcZmVgODqJpi8b5Z5O9Tnam3GHPjAAmPTGBEcF3VvH73RIF5p4nFJFL1sVtWetWR3kotWxha8mb5BKgW19NDDQSYKoQZXLZLmzBuaeBdjfZ8FGYrUaHHz4UdmlPjq38D_Oc7ZXNi7opMvjb8FKCQuT1rvJZzpOD8lA9lAnN82Z9IWLSJRUV39ecV0SiF1tPDimIjbvvfXNRUS7wgBVbUMSJW1YnXNGE8gtEd2OEFABaqrqqrf3RoWowav-wNAYVj56pQBwhnj7ALXciRXCAjxiL6kJQrhLLDnWup_ihohI9C0g" gcpToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjM3ODJkM2YwYmM4OTAwOGQ5ZDJjMDE3MzBmNzY1Y2ZiMTlkM2I3MGUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJnY3A6S29wcyBQcm92aXNpb25lciIsImF6cCI6IjEwNjY1MjI1MDIxMTYwNjU0NjIyMyIsImVtYWlsIjoiODQ3MjUwNjI1OTAwLWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNTU1NTQ3ODY4LCJnb29nbGUiOnsiY29tcHV0ZV9lbmdpbmUiOnsiaW5zdGFuY2VfY3JlYXRpb25fdGltZXN0YW1wIjoxNTU1NDQxMzQ2LCJpbnN0YW5jZV9pZCI6IjMwMTUzNzg2NjEwNDcwMDcyODYiLCJpbnN0YW5jZV9uYW1lIjoia29wcy1hZG1pbiIsInByb2plY3RfaWQiOiJrb3BzLWRldi0yMDE5LTAxIiwicHJvamVjdF9udW1iZXIiOjg0NzI1MDYyNTkwMCwiem9uZSI6InVzLWNlbnRyYWwxLWMifX0sImlhdCI6MTU1NTU0NDI2OCwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTA2NjUyMjUwMjExNjA2NTQ2MjIzIn0.ozbrQ8aoDKbc5IYWUntGOiff9Yvl2vaceEDXe9X4uHUz_PANfLMgx-VGxGnEtr-QXT0BVTYxY0gVaAiO_FvuvyPf_6Zn-a3l7o2quZtO1zZziOX9QnwFoLRyT1kUZ3lq50h-oP5u79iAwDfk2PbKs1CgILDFs7EUbxJt1cXhW2KxJokk8Fy4nfPzx2Wg8RbYm_pgQv1wPnitLXBAbyLDyNIA03myrhgYkXxogp_AsT8VH_nCyP_b_autTN3W3_o_UIpxXZljr8fVjHrWjP-tz9gNuh1W1o_tekjcrmDiswJmokOme15L5gV8oMS-WZ1EZ01i3SoDkVMJcg3-jTXVEA" gcpJWKSet = `{ "keys": [ { "kid": "6f6781ba71199a658e760aa5aa93e5fc3dc752b5", "e": "AQAB", "kty": "RSA", "alg": "RS256", "n": "1J287_drOWg9YJohe9TO7T0_l3EFkXOOWECkX5U-7ELhGFcfSnug-X7jnk4UJe2IyzlxZYSzsshUgTAvXSkLQCbkNT9gqigOmE7X70UAKaaq3IryR_yM92kpmBeH0zoNRFr-0f9vATrt3E2q9oKyKT16NEyzWqurk9w5cgdEB27OF389ftFK8H-BOtyB1gGziLvXVY4UTVfGOPe8VKTt2TfNWrdc40gt9L8iW4hCDzMlYCZQ-61dLhj_muIYXDXDfMqH1YK_JaCzAowzzuw6zCWLd9cUEAncotEbEsQUGqhof7KIsuX96ajGZKOWKBkvzBOUzr8EaOU4YGAyOvyVJw", "use": "sig" }, { "e": "AQAB", "kty": "RSA", "alg": "RS256", "n": "vIs0vGoFJRWXRbPOwrbkAYtocuQbkHON9xUdC3Yp0Wyg1RXGnFjO4EZJWiWXlIRdMORW_ABEz8ggh5-51zdSZK4RES7OglD9TzoUvZgCwveI__wz2YvqvvZjelHixksHJn7dxBKd_qIB94A9JCtTTcX4tJExugBrZz5OpS9PoBeR4_cwHRk2618Q9CezhjBmOWEW5kyfDAhzJc8f6mpd1pX004e_OybD6xhfUHgnB0vT45ocFHmKzZ5LGfJyPxqXkLkpezofEC4lO5ru9yUhK209s7GABo39ZX6gYjHKocKeGxMRw2jZ_5jBK9-jcp9upqO7sgbfGpHjxZE6Pr6bsw", "use": "sig", "kid": "3782d3f0bc89008d9d2c01730f765cfb19d3b70e" } ] }` awsToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbWF6b24iOnsiZG9jdW1lbnQiOiJld29nSUNKaFkyTnZkVzUwU1dRaU9pQWlTVzV0UWxOMGVuaDJjeUlzQ2lBZ0ltRnlZMmhwZEdWamRIVnlaU0k2SUNKNE9EWmZOalFpTEFvZ0lDSmhkbUZwYkdGaWFXeHBkSGxhYjI1bElqb2dJblZ6TFhkbGMzUXRNbUlpTEFvZ0lDSmlhV3hzYVc1blVISnZaSFZqZEhNaU9pQnVkV3hzTEFvZ0lDSmtaWFp3WVhsUWNtOWtkV04wUTI5a1pYTWlPaUJ1ZFd4c0xBb2dJQ0pwYldGblpVbGtJam9nSWxOQk5IaFpWVEZGWWtRaUxBb2dJQ0pwYm5OMFlXNWpaVWxrSWpvZ0lsb3pXRmRzV0VkT1dsSWlMQW9nSUNKcGJuTjBZVzVqWlZSNWNHVWlPaUFpZERJdWJXbGpjbThpTEFvZ0lDSnJaWEp1Wld4SlpDSTZJQ0lpTEFvZ0lDSndaVzVrYVc1blZHbHRaU0k2SUNJeU1ERTVMVEExTFRBNVZEQXhPakEwT2pJekxqVXlORFkyT1ZvaUxBb2dJQ0p3Y21sMllYUmxTWEFpT2lBaU1USTNMakF1TUM0eElpd0tJQ0FpY21GdFpHbHphMGxrSWpvZ0lpSXNDaUFnSW5KbFoybHZiaUk2SUNKMWN5MTNaWE4wTFRFaUxBb2dJQ0oyWlhKemFXOXVJam9nSWpJd01UY3RNRGt0TXpBaUNuMD0iLCJzaWduYXR1cmUiOiJKbkdlaWl2UzMrU1QxNGdFd2NCQS9MK0dHYkRyZm1iWmh3dU9tMjIrbFhqWjZzSUN4YUpRaFg1bmxyRDBrZUlvL2Y0dmRkT3FGTzhvUWNHd0J1YXl6aHFSVEt6cGpZVXBoSDFRMlJBUGl4dmcvZ0NSZG5YV0pETHpIRGhjTXdraCtvWnpFTkFuSTlhWGR4Vko4dGFKSlRHZ2lEVzdBcXYzK1JYZG4xcndiVVE9In0sImF1ZCI6WyJhd3M6TGdONHU4OUliNyJdLCJleHAiOjE1NTczNjQxNjMsImlhdCI6MTU1NzM2Mzg2MywiaXNzIjoiZWMyLmFtYXpvbmF3cy5jb20iLCJqdGkiOiI2ZDAyZDQ1MTEzODUzOTc3OTRmMmJkNjc0M2I1Nzc0MmIyNDMzYzE1Y2ZhNjEzZGNjYWYwNmNiMzUzOGY5YzJmIiwibmJmIjoxNTU3MzYzODYzLCJzYW5zIjpudWxsLCJzdWIiOiJaM1hXbFhHTlpSIn0.bP40Dk1YMQlNMK_XYVde93x53c92hCwYCsUp4tL7wrA" azureToken = "eyJhbGciOiJFUzI1NiIsImtpZCI6IjY2YjQxZjFlOTNmYTM5YTQzMjVkNmFhYmNlZGYwODJhZjg0NGZiMjJlZTk1NDAzYmVmOWQ0ZGNmMzcwYjcxMGUiLCJ0eXAiOiJKV1QifQ.eyJhcHBpZCI6InRoZS1hcHBpZCIsImFwcGlkYWNyIjoidGhlLWFwcGlkYWNyIiwiYXVkIjpbImh0dHBzOi8vbWFuYWdlbWVudC5henVyZS5jb20vIl0sImV4cCI6MTU1NzM0NTY1OCwiaWF0IjoxNTU3MzQ1MzU4LCJpZHAiOiJ0aGUtaWRwIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvUDg3aEFXVUUwYy8iLCJqdGkiOiJ0aGUtanRpIiwibmJmIjoxNTU3MzQ1MzU4LCJvaWQiOiJ0aGUtb2lkIiwic3ViIjoic3ViamVjdCIsInRpZCI6IlA4N2hBV1VFMGMiLCJ2ZXIiOiJ0aGUtdmVyc2lvbiIsInhtc19taXJpZCI6Ii9zdWJzY3JpcHRpb25zL3N1YnNjcmlwdGlvbklEL3Jlc291cmNlR3JvdXBzL3Jlc291cmNlR3JvdXAvcHJvdmlkZXJzL01pY3Jvc29mdC5Db21wdXRlL3ZpcnR1YWxNYWNoaW5lcy92aXJ0dWFsTWFjaGluZSJ9.U_NdLMXLztkYEn0RXYD394SiR-QpaJ8eoRFBu0FYQJ6zDflc-veAceFbLsTdihNE21zm7qYAkDqXlE0q3Y7Zdg" badToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyeyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" ) func TestParse(t *testing.T) { mustKeys := func(s string) interface{} { keys := new(jose.JSONWebKeySet) if err := json.Unmarshal([]byte(s), keys); err != nil { t.Fatal(err) } return keys } mustJoseClaims := func(tok string) jose.Claims { jwt, err := jose.ParseSigned(tok) if err != nil { t.Fatal(err) } var c jose.Claims if err := jwt.UnsafeClaimsWithoutVerification(&c); err != nil { t.Fatal(err) } return c } type args struct { token string key interface{} } tests := []struct { name string args args want Payload wantErr bool }{ {"ok JWK", args{jwkToken, mustKeys(jwkJWKSet)}, Payload{ Claims: mustJoseClaims(jwkToken), SHA: "2dd4f7d653c5a4f18248a903c6e3d67b1704923419c9f744fd680fc65242f3b2", SANs: []string{"foo.bar.zar"}, }, false}, {"ok OIDC", args{oidcToken, mustKeys(gcpJWKSet)}, Payload{ Claims: mustJoseClaims(oidcToken), AuthorizedParty: "1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com", AtHash: "YU2TASx-n660jQfNoHvHyg", Email: "mariano@smallstep.com", EmailVerified: true, Hd: "smallstep.com", Nonce: "8eda056b32220adcb6a27163571df253a7639f34ad64441b2429a3de2c5d7eaf", }, false}, {"ok GCP", args{gcpToken, mustKeys(gcpJWKSet)}, Payload{ Claims: mustJoseClaims(gcpToken), AuthorizedParty: "106652250211606546223", Email: "847250625900-compute@developer.gserviceaccount.com", EmailVerified: true, Google: &GCPGooglePayload{ ComputeEngine: GCPComputeEnginePayload{ InstanceID: "3015378661047007286", InstanceName: "kops-admin", InstanceCreationTimestamp: jose.NewNumericDate(time.Unix(1555441346, 0)), ProjectID: "kops-dev-2019-01", ProjectNumber: 847250625900, Zone: "us-central1-c", }, }, }, false}, {"fail bad token", args{"foobarzar", mustKeys(jwkJWKSet)}, Payload{}, true}, {"fail bad claims", args{badToken, mustKeys(jwkJWKSet)}, Payload{}, true}, {"fail bad keys", args{jwkToken, mustKeys(gcpJWKSet)}, Payload{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Parse(tt.args.token, tt.args.key) if (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr == false { if !reflect.DeepEqual(got.Payload, tt.want) { t.Errorf("Parse() = %v, want %v", got.Payload, tt.want) } } }) } } func TestParseInsecure(t *testing.T) { mustJoseClaims := func(tok string) jose.Claims { jwt, err := jose.ParseSigned(tok) if err != nil { t.Fatal(err) } var c jose.Claims if err := jwt.UnsafeClaimsWithoutVerification(&c); err != nil { t.Fatal(err) } return c } mustBase64 := func(s string) []byte { b64, err := base64.StdEncoding.DecodeString(s) if err != nil { t.Fatal(err) } return b64 } type args struct { token string } tests := []struct { name string args args want Payload wantErr bool }{ {"ok JWK", args{jwkToken}, Payload{ Claims: mustJoseClaims(jwkToken), SHA: "2dd4f7d653c5a4f18248a903c6e3d67b1704923419c9f744fd680fc65242f3b2", SANs: []string{"foo.bar.zar"}, }, false}, {"ok OIDC", args{oidcToken}, Payload{ Claims: mustJoseClaims(oidcToken), AuthorizedParty: "1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com", AtHash: "YU2TASx-n660jQfNoHvHyg", Email: "mariano@smallstep.com", EmailVerified: true, Hd: "smallstep.com", Nonce: "8eda056b32220adcb6a27163571df253a7639f34ad64441b2429a3de2c5d7eaf", }, false}, {"ok GCP", args{gcpToken}, Payload{ Claims: mustJoseClaims(gcpToken), AuthorizedParty: "106652250211606546223", Email: "847250625900-compute@developer.gserviceaccount.com", EmailVerified: true, Google: &GCPGooglePayload{ ComputeEngine: GCPComputeEnginePayload{ InstanceID: "3015378661047007286", InstanceName: "kops-admin", InstanceCreationTimestamp: jose.NewNumericDate(time.Unix(1555441346, 0)), ProjectID: "kops-dev-2019-01", ProjectNumber: 847250625900, Zone: "us-central1-c", }, }, }, false}, {"ok AWS", args{awsToken}, Payload{ Claims: mustJoseClaims(awsToken), Amazon: &AWSAmazonPayload{ Document: []byte(`{ "accountId": "InmBStzxvs", "architecture": "x86_64", "availabilityZone": "us-west-2b", "billingProducts": null, "devpayProductCodes": null, "imageId": "SA4xYU1EbD", "instanceId": "Z3XWlXGNZR", "instanceType": "t2.micro", "kernelId": "", "pendingTime": "2019-05-09T01:04:23.524669Z", "privateIp": "127.0.0.1", "ramdiskId": "", "region": "us-west-1", "version": "2017-09-30" }`), Signature: mustBase64("JnGeiivS3+ST14gEwcBA/L+GGbDrfmbZhwuOm22+lXjZ6sICxaJQhX5nlrD0keIo/f4vddOqFO8oQcGwBuayzhqRTKzpjYUphH1Q2RAPixvg/gCRdnXWJDLzHDhcMwkh+oZzENAnI9aXdxVJ8taJJTGgiDW7Aqv3+RXdn1rwbUQ="), InstanceIdentityDocument: &AWSInstanceIdentityDocument{ AccountID: "InmBStzxvs", Architecture: "x86_64", AvailabilityZone: "us-west-2b", ImageID: "SA4xYU1EbD", InstanceID: "Z3XWlXGNZR", InstanceType: "t2.micro", KernelID: "", PendingTime: time.Unix(1557363863, 524669000).UTC(), PrivateIP: "127.0.0.1", RamdiskID: "", Region: "us-west-1", Version: "2017-09-30", }, }, }, false}, {"ok Azure", args{azureToken}, Payload{ Claims: mustJoseClaims(azureToken), AppID: "the-appid", AppIDAcr: "the-appidacr", IdentityProvider: "the-idp", ObjectID: "the-oid", TenantID: "P87hAWUE0c", Version: "the-version", XMSMirID: "/subscriptions/subscriptionID/resourceGroups/resourceGroup/providers/Microsoft.Compute/virtualMachines/virtualMachine", Azure: &AzurePayload{ SubscriptionID: "subscriptionID", ResourceGroup: "resourceGroup", VirtualMachine: "virtualMachine", }, }, false}, {"fail bad token", args{"foobarzar"}, Payload{}, true}, {"fail bad claims", args{badToken}, Payload{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseInsecure(tt.args.token) if (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr == false { if !reflect.DeepEqual(got.Payload, tt.want) { t.Errorf("Parse() = %v, want %v", got.Payload, tt.want) } } }) } } func TestPayload_Type(t *testing.T) { type fields struct { SHA string SANs []string Email string Google *GCPGooglePayload Amazon *AWSAmazonPayload Azure *AzurePayload } tests := []struct { name string fields fields want Type }{ {"JWK", fields{"a-sha", []string{"foo.bar.zar"}, "", nil, nil, nil}, JWK}, {"JWK no sans", fields{"a-sha", nil, "", nil, nil, nil}, JWK}, {"JWK no sha", fields{"", []string{"foo.bar.zar"}, "", nil, nil, nil}, JWK}, {"OIDC", fields{"", nil, "mariano@smallstep.com", nil, nil, nil}, OIDC}, {"GCP", fields{"", nil, "", &GCPGooglePayload{}, nil, nil}, GCP}, {"AWS", fields{"", nil, "", nil, &AWSAmazonPayload{}, nil}, AWS}, {"Azure", fields{"", nil, "", nil, nil, &AzurePayload{}}, Azure}, {"Unknown", fields{"", nil, "", nil, nil, nil}, Unknown}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := Payload{ SHA: tt.fields.SHA, SANs: tt.fields.SANs, Email: tt.fields.Email, Google: tt.fields.Google, Amazon: tt.fields.Amazon, Azure: tt.fields.Azure, } if got := p.Type(); got != tt.want { t.Errorf("Payload.Type() = %v, want %v", got, tt.want) } }) } } golang-step-cli-utils-0.7.5+ds/token/provision/000077500000000000000000000000001433771265400214345ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/token/provision/provision.go000066400000000000000000000023101433771265400240070ustar00rootroot00000000000000package provision import ( "go.step.sm/cli-utils/token" "go.step.sm/crypto/jose" ) // Token defines a one time use token that is intended to be exchanged // for a newly provisioned certificate by an end entity. Token differs // from BootstrapToken because it does not self contain networking information // for connecting to the certificate authority or gatewayconsole. // // Token implements the Token interface. type Token struct { claims *token.Claims } // New returns a new unsigned One-Time-Token for authorizing a // single request from a newly provisioned end-entity. // The current implementation uses jwt.Token and is generated using sane defaults // for the claims. // See token/options.go for default claim definitions. func New(subject string, opts ...token.Options) (*Token, error) { o := append([]token.Options{token.WithSubject(subject)}, opts...) c, err := token.NewClaims(o...) if err != nil { return nil, err } return &Token{claims: c}, nil } // SignedString implementation of the Token interface. It returns a JWT using // the compact serialization. func (t *Token) SignedString(sigAlg string, key interface{}) (string, error) { return t.claims.Sign(jose.SignatureAlgorithm(sigAlg), key) } golang-step-cli-utils-0.7.5+ds/token/provision/provision_test.go000066400000000000000000000063151433771265400250570ustar00rootroot00000000000000package provision import ( "crypto/rsa" "reflect" "testing" "time" "github.com/stretchr/testify/assert" "go.step.sm/cli-utils/token" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" ) func withFixedTime(tok *Token, t time.Time) { if tok == nil { return } tok.claims.IssuedAt = jose.NewNumericDate(t) tok.claims.NotBefore = jose.NewNumericDate(t) tok.claims.Expiry = jose.NewNumericDate(t.Add(5 * time.Minute)) } func TestNew(t *testing.T) { type args struct { subject string opts []token.Options } now := time.Now() want := &Token{ claims: token.DefaultClaims(), } wantWithOptions := &Token{ claims: token.DefaultClaims(), } want.claims.Subject = "test.domain" wantWithOptions.claims.Subject = "test.domain" wantWithOptions.claims.Issuer = "new-issuer" wantWithOptions.claims.ExtraClaims = map[string]interface{}{"sha": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"} tests := []struct { name string args args want *Token wantErr bool }{ {"ok", args{"test.domain", nil}, want, false}, {"ok empty options", args{"test.domain", []token.Options{}}, want, false}, {"ok with options", args{"test.domain", []token.Options{token.WithIssuer("new-issuer"), token.WithClaim("sha", "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c")}}, wantWithOptions, false}, {"fail no subject", args{"", []token.Options{}}, nil, true}, {"fail bad option", args{"test.domain", []token.Options{token.WithIssuer("")}}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := New(tt.args.subject, tt.args.opts...) withFixedTime(got, now) if (err != nil) != tt.wantErr { t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) return } assert.Equal(t, got, tt.want) if !reflect.DeepEqual(got, tt.want) { t.Errorf("New() = %v, want %v", got, tt.want) } }) } } func TestToken_SignedString(t *testing.T) { type fields struct { claims *token.Claims } type args struct { sigAlg string key interface{} } rsaKey, err := pemutil.Read("../testdata/openssl.rsa1024.pem") if err != nil { t.Fatal(err) } rsaPublic := rsaKey.(*rsa.PrivateKey).Public() expected := "eyJhbGciOiJSUzI1NiIsImtpZCI6Im50U2lnZFFZNHRLOFlmTDdHQjZjNGRuZzhvSGVGOU5VMkl0QUlVOGtHZGciLCJ0eXAiOiJKV1QifQ.e30.spzx_GFrhXg_LTPBIE3z3uWaA-GH7G0rbPdskxbUahJnXRLwF8S_AAQMTjtsWY9iELwOQQUXW7aPES-jONCebTpXl00RYP7maiS87wcGW6nZ0ICmsbS5NnCDJIKpV4Ei3MZ4MXfZ4vLaONaR5BunHYkicMDqWif_2v8yvxebh7c" tests := []struct { name string fields fields args args want string wantErr bool }{ {"ok", fields{&token.Claims{}}, args{"RS256", rsaKey}, expected, false}, {"fail bad alg", fields{&token.Claims{}}, args{"ES256", rsaKey}, "", true}, {"fail with public", fields{&token.Claims{}}, args{"RS256", rsaPublic}, "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tok := &Token{ claims: tt.fields.claims, } got, err := tok.SignedString(tt.args.sigAlg, tt.args.key) if (err != nil) != tt.wantErr { t.Errorf("Token.SignedString() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("Token.SignedString() = %v, want %v", got, tt.want) } }) } } golang-step-cli-utils-0.7.5+ds/token/testdata/000077500000000000000000000000001433771265400212155ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/token/testdata/ca.crt000066400000000000000000000041021433771265400223070ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIF6zCCA9OgAwIBAgIRAL4t3Jo++cwAle8DdXchv/owDQYJKoZIhvcNAQELBQAw WzEMMAoGA1UEBhMDVVNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQK EwlzbWFsbHN0ZXAxHzAdBgNVBAMTFmludGVybmFsLnNtYWxsc3RlcC5jb20wHhcN MTcwOTIzMDczNTA3WhcNMTgwOTIzMDczNTA3WjBbMQwwCgYDVQQGEwNVU0ExFjAU BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCXNtYWxsc3RlcDEfMB0GA1UE AxMWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP ADCCAgoCggIBAKA+760g0MbZpFCgG6NpzRh0B8ElgQUteMjynL8ge+r8QsFCm2XY P7BYzjyyD9FdNTRw2toUB8G/t3E5jhjrE6qvG0PWsluzFEtfh0uS59BPS6YTgurY LE3PAc/+fCxEI3SfA4TCYVnzUcSkhcHNT0PtMWG8tR7S+0GFc1O22wUn2e/dKK1d fCGhEu9gzuA3TjJgpzfmXTBFUijiIRSaXHiYcUWR0FE3CKVULlM2jJ/uxXZr6kSZ STxQ/kisaIzOe7Y/uA9F4fyfCHdaCsvkv3d11d1SkOdBCY+jx+PG5uLDWxGCgZYZ dWDjOX43gquSaC3bFMi+cglF4Wx+n173elcOuoF77bVNBOOtWIbWNLYVujkvbzec Dn0NLySl79OKMuSuF995iR7Or29gcbaZz5j1NHeqbhb24HWZ+9xi3ws4ike7GZ5Q akZ3AwEcwVwbMhQ5KCoWKroSWpYUvQ58PGgy+ml5f42Cjg/e1nH1/hpnqwzzItbs 6qb9I0RV12Y6KCEqmKIrs1EdHc351aknhiZ1Zgdankhym3TiAo08mkDIqbJUKjR+ 0De7ynBKBDq79NWfb5DLdXH95z1DDZvI4FJ9X0eAlo3DbkZXFIfoeF1gL577pEES NXZKXqmY2hPcsZUKAhXIEK3zmNXJVGeqb9sNnYtBTY6zBo5sA+40WDHNAgMBAAGj gakwgaYwDgYDVR0PAQH/BAQDAgGmMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF BQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRVBozFxzNJ9pSzW4lW MZF/q+6SPTAfBgNVHSMEGDAWgBTd1DpE9Y4R5OoP7f5WT91mPCU2gDAhBgNVHREE GjAYghZpbnRlcm5hbC5zbWFsbHN0ZXAuY29tMA0GCSqGSIb3DQEBCwUAA4ICAQCj 3CT2xk9xLFTX0Ki30PWB6/0OxN5L0sKk7pJAWIzgdsKYrBbh93sA/oYGsnr0iW6F VLWkvqMmGLlp5yLg6LIQaed5C26u2fc0udzXTVfEx7QjOtLtetLt7LQ6Kzb7FOri iiUfvilLttXyESQ3WzlCTh4OlrIhNWg1w56jc0/7GAJY0LTrsCoYOSwR2qlBQiTI 41+fApAlRZKI0eNP9X4GxVkgh+wIVuF4zXr/460VkaWT9RquvS2MIaotdZ3IBTTk tCmFHvbI/eZOWo1KbjPdSByOZVI1gBfpU/eufsdysRebZwBsYRDF391QJ3aKt2cZ WnAjtYl3lfcXA/iFj2HL04vmdTweBVvl7Wa2EsM/iiEhPOWlIXdQx81FURE8nc2H DCQJgQIbwqZ4LQJrmF6tmzhmJUH2/9Vxc/rYMSx6NgT6sSoz+gXt0yDd20tF7SU6 smiL/uCGfSXAbqsI+MO8Nc7gOPhKtHeW4r2Kx/OzuFkYAFBez0GqxmnJ/xKgwfGO v+pzRC09KUgpncGZuB6S9PUWPhC15LO5bFBF1tiUy8hyzzbopfyPtLnhY8tq4u+j lGTAz5g+7chL+j6UqZZYGD5DqIiOYO/YK0wi92ov1wu6Pkvb33dy1LVWJaGjcAQv 7cuffXbN9jXxAtrFxrmY3qfXnUR4K2lCESWPjCCmaw== -----END CERTIFICATE----- golang-step-cli-utils-0.7.5+ds/token/testdata/foo.crt000066400000000000000000000025741433771265400225220ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICDDCCAbGgAwIBAgIRALMjxkh2lUFokljKZbIxRXgwCgYIKoZIzj0EAwIwJDEi MCAGA1UEAxMZU21hbGxzdGVwIEludGVybWVkaWF0ZSBDQTAeFw0yMTA2MDExODM2 NDVaFw0zMTA1MzAxODM3NDVaMA4xDDAKBgNVBAMTA2ZvbzBZMBMGByqGSM49AgEG CCqGSM49AwEHA0IABKILAClbm33rYcebba+HwWGoudc18gMoSzca+kMS17TzQb1o xHB04vyJsWHEEqCdrmVJb44IqY509oG2ZtXgI+yjgdkwgdYwDgYDVR0PAQH/BAQD AgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUcjBD tySu1HypIjCwxXIq3s3PlAcwHwYDVR0jBBgwFoAUjc43KibJVc6mUS4Yo7wD8GRO fRIwDgYDVR0RBAcwBYIDZm9vMFUGDCsGAQQBgqRkxihAAQRFMEMCAQEEEW1heEBz bWFsbHN0ZXAuY29tBCtnU0s3T0xrMVZnMzUyelY2QmR1YlJOM2hna0ZGYVBGWUI2 ZUdUc25Xdl9vMAoGCCqGSM49BAMCA0kAMEYCIQD3r6EGVEafm5jzzOtEXixP0bMf l4TaHklf/+aRhiVcawIhAKBX+OHHwsaIHNRjFt4FFw977AOn6IKthwD9OP9IypJA -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBpzCCAU2gAwIBAgIQT4pzuLWZL2kKRRwcOo03PTAKBggqhkjOPQQDAjAfMR0w GwYDVQQDExRFeGFtcGxlIEluYy4gUm9vdCBDQTAeFw0yMDA1MDQxODAxMTNaFw0z MDA1MDIxODAxMTNaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUg Q0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQoSwDsPSrt77Dw8jgtWj4uKyzk bkRS+itI6SM/jChPFM/wXrqxq5KXPAupoFXkGMR5OYkb822VKR9wjyHMKP6to2Yw ZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU jc43KibJVc6mUS4Yo7wD8GROfRIwHwYDVR0jBBgwFoAUieDwZuVlgJ8xhdEMyPcc 2HJH6ckwCgYIKoZIzj0EAwIDSAAwRQIhAOGZz3UIoLYQb0DOb6Vgb35IHYoXhPUR YDDpH0GywlvWAiAROESV57M1Yxe9cH0cbhu4Bny9Ne81vNLHmUt0bT4DQQ== -----END CERTIFICATE----- golang-step-cli-utils-0.7.5+ds/token/testdata/foo.key000066400000000000000000000003431433771265400225120ustar00rootroot00000000000000-----BEGIN EC PRIVATE KEY----- MHcCAQEEIHNSBYVMDh7WibYRIegkG3irUcqi78gSaHZYe3D62IHXoAoGCCqGSM49 AwEHoUQDQgAEogsAKVubfethx5ttr4fBYai51zXyAyhLNxr6QxLXtPNBvWjEcHTi /ImxYcQSoJ2uZUlvjgipjnT2gbZm1eAj7A== -----END EC PRIVATE KEY----- golang-step-cli-utils-0.7.5+ds/token/testdata/openssl.p256.pem000066400000000000000000000003431433771265400240760ustar00rootroot00000000000000-----BEGIN EC PRIVATE KEY----- MHcCAQEEIOWt6OMAASXS1jJ/Sn2vY8ciPDU74cqjq3J13oYUNeNuoAoGCCqGSM49 AwEHoUQDQgAEp2gPSjWPnX25PYBYpMjl+D2VAI2Smm1pxaMQw5x974BnEg25Axaf 3yN//SwJ3X1Ju9gwfhhHmpqYKzQ/reyNOw== -----END EC PRIVATE KEY----- golang-step-cli-utils-0.7.5+ds/token/testdata/openssl.rsa1024.pem000066400000000000000000000015671433771265400245070ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQCztZjbIWsnC88Npdq6YHwKi41OYPM8wD4Bcn7TU4NcEh5zLiQ9 rpxoAxKmxZtSFD7PbTkQkNa8rHN5vX9qWrrwrAIxX/vAiVo+ZvSNoPeG1Bn18xJ3 4oLPUUv3IkVCSBFWcZ1NBUhde+KabeaGKNb3bHM+4btBb4TLZwhh6EC6RwIDAQAB AoGAENNX1Gx0k9tPL3/v0rNl6bbXLBd3rqBxLcGCjlarXdt0bmRLkFrg2fwvqt2l hTHQD6uyRBLLiC69QRC09Ug5aFb22BQlhcSgy8K8gRAXb9Ekz+9riTiJk/gBoVSk YjsoAIzYuE4BXz3CR2R4Sywh2v/7JDitCZsXHaVhCNpWQMECQQDjZrHOtGeUfsFo 8c9WxGmBNoGned4zaknEK+UAAGCBgR2F9ChkEQT1f1A13e2Hl0XzZmXflOvaDGBI WvzwxribAkEAyk9wbFSeSlyj5U0D2wi61oif16JAu1QUAQr82ESYXarE5pnoms39 cUwLhdCJwmpzpi+3zlz6IEmpA10HtG0xxQJAGoCffG2+HKphNC/qcDxX531IwxIK +YcLrddHyyZAGRfJLxFzm6X4I/yAhqakxka1Glb2zIX4ruL+XbBtBkrCvQJAXS4T gMHEmklq74z2TqcJrxAEVwQTPnSuNgDCjjWh29pwkCmpOcvQhKNa10pCePogxBVM Wk72oXJr1vG9P7vfZQJAduCw0ydHAg7Mh70A3qLMW7+Hj8pMNo7vNKjJE/HDtHWb tQEQ/FjyyQ3jtxoAhDx2HwxKO+ksUO/4FOtuuG3pKw== -----END RSA PRIVATE KEY----- golang-step-cli-utils-0.7.5+ds/token/testdata/openssl.rsa2048.pem000066400000000000000000000032171433771265400245100ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpgIBAAKCAQEA4uI83MBkkLHDoHmLNrd7ooIbzm+ZeX6jSB2HiwpmA7PRjpgt xBD2fjt8QZ1qXUi9GkSlajiomK5Q64+Dg+6CJp97Lkm4EybwQvHJqbzA+pHFCrNL DVBea2O/kRBMzdSr7aI0p8gwruVStwADtNYK4nrd45X08SE3Hl5iB4tMBMrOvBug v5fTaII5ZMQ6AzqqWwk/pTP/dfjKfGPO+rN7g3PQ4/B+yXjDWfAld9dFk7Wl53Po rC7oqcU4MZMyGImXgi58V7lx+XhRqvcRUx1y2CBUJu6aHgvBERsf+vXSf5q1HiIM UFYMhn+duLoTRBZj3JPDE36J6SOIHDg8xoQDnwIDAQABAoIBAQCHt7OWjZPapiuS fAJVuc5AOLovc7yH28QKqHdjKdY6Ur+BH/EIfukkO6spiOOOZ6uO4g9dCgV4R5Xq Qw/1xJ+gQPgriTeOZVWFhiMO4PVDLh2DOBsmHLROYv295dU7rwMlhEkhMHRGurEO /Pg6nWsnbT38HMDH2QmipezX/HB90D73R721ms2wffVs1DU4etTEJc/zUaWzuD2b i2UY8hne/Jv0hIvS7mb2vM/vGJOG7k4mFdKvvGOgsLBR/bwIoMIuSKcDQbf1rTDm V3NLNwA/irWR8F9SZL7QwY/8z+3vgylTYq6PlPAXrK7Fts2N3Qdp/UYrUxjI4kUe H3drLm/xAoGBAPmD9oZauAQle5MyvBfi1XU5eVZr0kRpFLoQaAeHI60Aia42+qCt RQbtk+x+XR3CjA+FoVEr+644MZWTYaKawA2Mt931OVxuQG8CkzRutBmnFK3atsNq 3yfrOXm+vyQoM5eXmFtLTJwiG8a7EjlgX3Fl4aLMRlkmdwjJy6Y0Tyr5AoGBAOjH tHBrY1FQSM5qcPeoH5ah4bGKgF6sSOZm5YOF4J42zz0BE/t1vChEZb00D9+2+lC/ vSeplLhyvKueV+sRObo8VfQAbSOOC3u/Cm9zIbkdilOW8Ux3KXtkvOLxdsTpwvZh Zqu8LwWe7WnOBXILFmBaLkuOTfe6AGBrPcbbaPFXAoGBAPebqS0zIaGbwMIWeuoJ RGMMIglM/mC9FsB+P34Y8aJhAkBMdvK0f+ecJEtwKt+5jFxq8+cliqEdSrdwhldi 0muf1WcCT2YWUwLWv1Ys9bTvRWoxvWS3zbRDjcnvLKeo7WnmGl+enevjPUU9p5wg sxZJUFzJ8pXNwhqKhvnstxOhAoGBAOczEuBliKuGlgmOZs1TyqwN9OAltAJUE8Pj hynumn4J6iOpInOrKErGRFZ7kxib4Fq7VeBC6leYfhPmnWP4I+H5c1V55uxddMJf qLmxHFmEIZOMY/WSlTzdfU3ajiBeHSog65y+t+VZSGzCF16B7KOebkTU/lOCBkW9 vgn4em7ZAoGBAJoBN2+76jXFkd98UFesXWGi9CFUFAffgAeu71RzZsxsbuwEn0z6 /4uxDDxUFLcguK1PBCljeHUFAZUS/+k8eEjsfITWzmixAPQlvYGpYDuU9Z2P4agn TM16XFyRvNMNqtuY2LovA9FuPdYceoneQj2C1AybyiskRZjUfNCGdWPD -----END RSA PRIVATE KEY----- golang-step-cli-utils-0.7.5+ds/token/token.go000066400000000000000000000075351433771265400210650ustar00rootroot00000000000000package token import ( "crypto" "encoding/base64" "time" "github.com/pkg/errors" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" ) const ( // DefaultIssuer when generating tokens. DefaultIssuer = "step-cli" // DefaultAudience when generating tokens. DefaultAudience = "https://ca/sign" // MinValidity token validity token duration. MinValidity = 10 * time.Second // MaxValidity token validity token duration. MaxValidity = 1 * time.Hour // DefaultValidity token validity duration. DefaultValidity = 5 * time.Minute // MaxValidityDelay allowable delay between Now and beginning of token validity period. MaxValidityDelay = 30 * time.Minute ) // RootSHAClaim is the property name for a JWT claim that stores the SHA256 of a root certificate. const RootSHAClaim = "sha" // SANSClaim is the property name for a JWT claim that stores the list of required subject alternative names. const SANSClaim = "sans" // StepClaim is the property name for a JWT claim the stores the custom information in the certificate. const StepClaim = "step" // timeNowUTC returns the current time in UTC. var timeNowUTC = func() time.Time { return time.Now().UTC() } // Token interface which all token types should attempt to implement. type Token interface { SignedString(sigAlg string, priv interface{}) (string, error) } // Claims represents the claims that a token might have. type Claims struct { jose.Claims ExtraClaims map[string]interface{} ExtraHeaders map[string]interface{} } // Set adds the given key and value to the map of extra claims. func (c *Claims) Set(key string, value interface{}) { if c.ExtraClaims == nil { c.ExtraClaims = make(map[string]interface{}) } c.ExtraClaims[key] = value } // SetHeader adds the given key and value to the map of extra headers. func (c *Claims) SetHeader(key string, value interface{}) { if c.ExtraHeaders == nil { c.ExtraHeaders = make(map[string]interface{}) } c.ExtraHeaders[key] = value } // Sign creates a JWT with the claims and signs it with the given key. func (c *Claims) Sign(alg jose.SignatureAlgorithm, key interface{}) (string, error) { kid, err := GenerateKeyID(key) if err != nil { return "", err } so := new(jose.SignerOptions) so.WithType("JWT") so.WithHeader("kid", kid) // Used to override the kid too for k, v := range c.ExtraHeaders { so.WithHeader(jose.HeaderKey(k), v) } signer, err := jose.NewSigner(jose.SigningKey{ Algorithm: alg, Key: key, }, so) if err != nil { return "", errors.Wrapf(err, "error creating JWT signer") } // Force aud to be a string if len(c.Audience) == 1 { c.Set("aud", c.Audience[0]) } raw, err := jose.Signed(signer).Claims(c.Claims).Claims(c.ExtraClaims).CompactSerialize() if err != nil { return "", errors.Wrapf(err, "error serializing JWT") } return raw, nil } // NewClaims returns the default claims with the given options added. func NewClaims(opts ...Options) (*Claims, error) { c := DefaultClaims() for _, fn := range opts { if err := fn(c); err != nil { return nil, err } } return c, nil } // DefaultClaims returns the default claims of any token. func DefaultClaims() *Claims { now := timeNowUTC() return &Claims{ Claims: jose.Claims{ Issuer: DefaultIssuer, Audience: jose.Audience{DefaultAudience}, Expiry: jose.NewNumericDate(now.Add(DefaultValidity)), NotBefore: jose.NewNumericDate(now), IssuedAt: jose.NewNumericDate(now), }, ExtraClaims: make(map[string]interface{}), } } // GenerateKeyID returns the SHA256 of a public key. func GenerateKeyID(priv interface{}) (string, error) { pub, err := keyutil.PublicKey(priv) if err != nil { return "", errors.Wrap(err, "error generating kid") } jwk := jose.JSONWebKey{Key: pub} keyID, err := jwk.Thumbprint(crypto.SHA256) if err != nil { return "", errors.Wrap(err, "error generating kid") } return base64.RawURLEncoding.EncodeToString(keyID), nil } golang-step-cli-utils-0.7.5+ds/token/token_test.go000066400000000000000000000231171433771265400221160ustar00rootroot00000000000000package token import ( "crypto/ed25519" "crypto/rand" "crypto/rsa" "reflect" "testing" "time" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/randutil" ) func TestClaims_Set(t *testing.T) { type fields struct { Claims jose.Claims ExtraClaims map[string]interface{} } type args struct { key string value interface{} } tests := []struct { name string fields fields args args want *Claims }{ {"ok nil", fields{jose.Claims{}, nil}, args{"key", "value"}, &Claims{ExtraClaims: map[string]interface{}{"key": "value"}}}, {"ok empty", fields{jose.Claims{}, make(map[string]interface{})}, args{"key", "value"}, &Claims{ExtraClaims: map[string]interface{}{"key": "value"}}}, {"ok not empty", fields{jose.Claims{}, map[string]interface{}{"foo": "bar"}}, args{"key", "value"}, &Claims{ExtraClaims: map[string]interface{}{"key": "value", "foo": "bar"}}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Claims{ Claims: tt.fields.Claims, ExtraClaims: tt.fields.ExtraClaims, } c.Set(tt.args.key, tt.args.value) if !reflect.DeepEqual(c, tt.want) { t.Errorf("Options claims = %v, want %v", c, tt.want) } }) } } func TestClaims_SetHeader(t *testing.T) { type fields struct { Claims jose.Claims ExtraHeaders map[string]interface{} } type args struct { key string value interface{} } tests := []struct { name string fields fields args args want *Claims }{ {"ok nil", fields{jose.Claims{}, nil}, args{"key", "value"}, &Claims{ExtraHeaders: map[string]interface{}{"key": "value"}}}, {"ok empty", fields{jose.Claims{}, make(map[string]interface{})}, args{"key", "value"}, &Claims{ExtraHeaders: map[string]interface{}{"key": "value"}}}, {"ok not empty", fields{jose.Claims{}, map[string]interface{}{"foo": "bar"}}, args{"key", "value"}, &Claims{ExtraHeaders: map[string]interface{}{"key": "value", "foo": "bar"}}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Claims{ Claims: tt.fields.Claims, ExtraHeaders: tt.fields.ExtraHeaders, } c.SetHeader(tt.args.key, tt.args.value) if !reflect.DeepEqual(c, tt.want) { t.Errorf("Options claims = %v, want %v", c, tt.want) } }) } } func TestClaims_Sign(t *testing.T) { type fields struct { Claims jose.Claims ExtraClaims map[string]interface{} ExtraHeaders map[string]interface{} } type args struct { alg jose.SignatureAlgorithm key interface{} } rsaKey, err := pemutil.Read("testdata/openssl.rsa2048.pem") if err != nil { t.Fatal(err) } badKey, err := rsa.GenerateKey(rand.Reader, 123) if err != nil { t.Fatal(err) } tests := []struct { name string fields fields args args want string wantErr bool }{ {"ok", fields{jose.Claims{}, nil, nil}, args{"RS256", rsaKey}, "eyJhbGciOiJSUzI1NiIsImtpZCI6IkoyUThZSzJsM2wyZmgwYURYLUxHLWlxTmlneVkwZHhUNHM2TkgtOFFmVTAiLCJ0eXAiOiJKV1QifQ.e30.0FbvZdz3qUv-k9AmGerLh5c1LbT6XRBUwFKe7iU0GU7fZnDYAmVyTIkyxwhc6zD5K5rURfqnGXKYNOa4JVUQ7T6S0fZOVFYI3pBExDImi3it9JTXWwDpDle6GqESQfpyZMdIWBB16B6vf8S4K7ZlKmAD00F6f1SB-Vzd_bV_kgdjTkBaHl3DSGRJdn8UBbGrkmrxm7iBogLKXtYMxDQLzIqZF3mMzZiTWNVELpoLOO5cAPXpcdVQ24a2u66KU5WM5n-GxBBYW7JwEDmFi7AoXWCixDlPlBTOVr2BIta99U9e7QwiD3OvEAnjrqUHHtsaJ-kcSxLiRM49CfnJZm_cjg", false}, {"ok one audience", fields{jose.Claims{Audience: jose.Audience{"value"}}, nil, nil}, args{"RS256", rsaKey}, "eyJhbGciOiJSUzI1NiIsImtpZCI6IkoyUThZSzJsM2wyZmgwYURYLUxHLWlxTmlneVkwZHhUNHM2TkgtOFFmVTAiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJ2YWx1ZSJ9.o1OyBGmRNi0YEiaMNYnJrSgegbQePHc267Y-fDEyRKxa8LQzSL7eZekUuI1VsSGsBJxWUBu9WI9XHjCEHCrhQst9rNiYaEG4ooz0SCS7JSii4CedvhgI0PPfw5g1uTmkHQ-1qxM9cUfRFM0sCd3ULffb8qdvg8l6CEwYKS0nCZmIH8rt8I9Z24k6gs85budt0g0ZM9iwH-irmFwTsn6ZbVfKgggeEPNXEMvcjsofch3p9-n30kkicsyAZMo4oTD1Z8nnX4JlghGpt1kjRh79kNXO7KdQKHNcyus7Hk2Cpf304cMT5yuxezhUJDl4-gqPE8Im4OvLvMmUibjWHiVfQw", false}, {"ok multiple audiences", fields{jose.Claims{Audience: jose.Audience{"foo", "bar"}}, nil, nil}, args{"RS256", rsaKey}, "eyJhbGciOiJSUzI1NiIsImtpZCI6IkoyUThZSzJsM2wyZmgwYURYLUxHLWlxTmlneVkwZHhUNHM2TkgtOFFmVTAiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl19.ykdEryzCfOi1GzpM9VrYw7tCGlzK3u_0sehk4Cx_o3TiDz82dbyMfPQMAr_2agVPeNvnkPIadZutyt5rFyj-Dn0W1svQpwG7PKqSJKLjSBBiBT1gbJuCQBAs7dzmsJVqcq9i4UmgIKH4EQsvFWoxA52PSD9TtvfhKtFYP23deJZ22ViqBrbYjaz1nQG0Lm_hX8RyOoEWem25n_mwPUtYuKHNy9rekwHanpyEUiehCpIzuZgMGcZ6_lln1BYlPMWkfyTz6K1uRMeZC3phWdSgW4JTvFp7xRGiC8L-pAboPoj7t8GArZgm2aGnF2OAtIw1eJ8GiaH3EBS7Adnw8CUb9w", false}, {"ok with empty payload", fields{jose.Claims{}, map[string]interface{}{}, nil}, args{"RS256", rsaKey}, "eyJhbGciOiJSUzI1NiIsImtpZCI6IkoyUThZSzJsM2wyZmgwYURYLUxHLWlxTmlneVkwZHhUNHM2TkgtOFFmVTAiLCJ0eXAiOiJKV1QifQ.e30.0FbvZdz3qUv-k9AmGerLh5c1LbT6XRBUwFKe7iU0GU7fZnDYAmVyTIkyxwhc6zD5K5rURfqnGXKYNOa4JVUQ7T6S0fZOVFYI3pBExDImi3it9JTXWwDpDle6GqESQfpyZMdIWBB16B6vf8S4K7ZlKmAD00F6f1SB-Vzd_bV_kgdjTkBaHl3DSGRJdn8UBbGrkmrxm7iBogLKXtYMxDQLzIqZF3mMzZiTWNVELpoLOO5cAPXpcdVQ24a2u66KU5WM5n-GxBBYW7JwEDmFi7AoXWCixDlPlBTOVr2BIta99U9e7QwiD3OvEAnjrqUHHtsaJ-kcSxLiRM49CfnJZm_cjg", false}, {"ok with payload", fields{jose.Claims{}, map[string]interface{}{"foo": "bar"}, nil}, args{"RS256", rsaKey}, "eyJhbGciOiJSUzI1NiIsImtpZCI6IkoyUThZSzJsM2wyZmgwYURYLUxHLWlxTmlneVkwZHhUNHM2TkgtOFFmVTAiLCJ0eXAiOiJKV1QifQ.eyJmb28iOiJiYXIifQ.KFHfeRbE3Xk-EaJee1WIyzNB4J3Ybwt56pnTkArU0a5F8cING1Z2bhrtYFrm8ejJOMwuYBxp3JzZQKvKND363hPuiT-Zz9fF_cYjOAk1lj2yEXHPhUkWKWb91uaCus6F4AgMsacjMWN26cw3fEDdGmitRii2sNUKcM6sf7rpUo2k1hME8PpevEpXtwIecAmxwvQWBaKMXc_stxMnvbTGO_BTbFtmdDwvrqbapHxzRXMJbZcDAyREi_qqLN-2ylBfs-mRtccghrdzuq3ckvFy_7ojBk9bWoxHbcOqL0tDwOu8AOjEMX2GE5zlfqltU_IXVm95xlVObkAsL6buqrdfog", false}, {"ok with header", fields{jose.Claims{}, nil, map[string]interface{}{"foo": "bar"}}, args{"RS256", rsaKey}, "eyJhbGciOiJSUzI1NiIsImZvbyI6ImJhciIsImtpZCI6IkoyUThZSzJsM2wyZmgwYURYLUxHLWlxTmlneVkwZHhUNHM2TkgtOFFmVTAiLCJ0eXAiOiJKV1QifQ.e30.WpiGDkrqo-6D3JHJRDozHC_jV_ROzf0GYhiQCwSVMUKArMb7BWAoccDKym6vuPSUYxm6luV-3dr-HNG1lKp-g4weikZ04YUwfhmF02JU60GZDP2YG67BR3TWItaLLGWKfLAp8p_B0dSdDWQaevLLBpM765rpYG6OeBhh2qFQ3PdXRJtR4GRzf-D0HH82z7591g8Rb0DxawcB3W2KYqyWFUcRg7lYwXMIaA0sYGuH-Go0ho-6xmHF13tckv2E9b_lSwynwNy1JyEQyxYcPa7QD9xxM4_qKK2JkjIsamBGohXA4KO1sXdwbgvoqRSVYVF55WKErDHCsmfXHCZ8REvz5g", false}, {"ok with kid", fields{jose.Claims{}, nil, map[string]interface{}{"kid": "my-custom-kid"}}, args{"RS256", rsaKey}, "eyJhbGciOiJSUzI1NiIsImtpZCI6Im15LWN1c3RvbS1raWQiLCJ0eXAiOiJKV1QifQ.e30.4BXXjh4OixA9RnO08DX_3d98enThNz4RHf2kuIbP46O-QOPHYAs9JTSZouK-64P2ErKUTKcfj7I4Qj0cV9QXUjT8jeqk7sy-MtSF8bBNUU8qHZs53PE_1HXatPqqVVQnm7iffQCIBo52OInOqlHTZW5ruV2TE1hiAkRGF9iU2LjeGtPy2lruDtyPAoCjTcox8bnYH3LjpfVQKlhMmEzCG7xrq_rytiY3aXsnoj4PAspJvZPgMYaIJGcnbEEzpdmPK0ikhYPHJSdbwzGCvPT2wc5xg-1yNgaK-n2f5AXLYhW6C4pLABOH10TrFbDo-Yjp4WfxeWaekmY04lxgFxhvGw", false}, {"fail with unsupported key", fields{jose.Claims{}, nil, nil}, args{"HS256", []byte("the-key")}, "", true}, {"fail with wrong alg", fields{jose.Claims{}, nil, nil}, args{"FOOBAR", rsaKey}, "", true}, {"fail with invalid alg", fields{jose.Claims{}, nil, nil}, args{"HS256", rsaKey}, "", true}, {"fail on sign", fields{jose.Claims{}, nil, nil}, args{"RS256", badKey}, "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Claims{ Claims: tt.fields.Claims, ExtraClaims: tt.fields.ExtraClaims, ExtraHeaders: tt.fields.ExtraHeaders, } got, err := c.Sign(tt.args.alg, tt.args.key) if (err != nil) != tt.wantErr { t.Errorf("Claims.Sign() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("Claims.Sign() = %v, want %v", got, tt.want) } }) } } func withFixedTime(c *Claims, t time.Time) { if c == nil { return } c.IssuedAt = jose.NewNumericDate(t) } func TestNewClaims(t *testing.T) { type args struct { opts []Options } now := time.Now() want := DefaultClaims() want.Subject = "subject" want.IssuedAt = jose.NewNumericDate(now) want.NotBefore = jose.NewNumericDate(now) want.Expiry = jose.NewNumericDate(now.Add(10 * time.Minute)) tests := []struct { name string args args want *Claims wantErr bool }{ {"ok", args{[]Options{WithSubject("subject"), WithValidity(now, now.Add(10*time.Minute))}}, want, false}, {"fail", args{[]Options{WithSubject("")}}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := NewClaims(tt.args.opts...) withFixedTime(got, now) if (err != nil) != tt.wantErr { t.Errorf("NewClaims() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("NewClaims() = %v, want %v", got, tt.want) } }) } } func TestGenerateKeyID(t *testing.T) { type args struct { priv interface{} } rsaKey, err := pemutil.Read("testdata/openssl.rsa1024.pem") if err != nil { t.Fatal(err) } esKey, err := pemutil.Read("testdata/openssl.p256.pem") if err != nil { t.Fatal(err) } b, err := randutil.Salt(64) if err != nil { t.Fatal(err) } badKey := ed25519.PublicKey(b) tests := []struct { name string args args want string wantErr bool }{ {"ok rsa", args{rsaKey}, "ntSigdQY4tK8YfL7GB6c4dng8oHeF9NU2ItAIU8kGdg", false}, {"ok es", args{esKey}, "COu8GPmatXsngf8XdSj5J3aqQotmjs7QR1lll517DxM", false}, {"fail with unsupported", args{[]byte("the-key")}, "", true}, {"fail with bad key", args{badKey}, "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := GenerateKeyID(tt.args.priv) if (err != nil) != tt.wantErr { t.Errorf("GenerateKeyID() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("GenerateKeyID() = %v, want %v", got, tt.want) } }) } } golang-step-cli-utils-0.7.5+ds/ui/000077500000000000000000000000001433771265400167015ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/ui/options.go000066400000000000000000000101071433771265400207220ustar00rootroot00000000000000package ui import ( "fmt" "regexp" "strings" "github.com/manifoldco/promptui" ) type options struct { mask rune defaultValue string value string allowEdit bool printTemplate string promptTemplates *promptui.PromptTemplates selectTemplates *promptui.SelectTemplates validateFunc promptui.ValidateFunc } // apply applies the given options. func (o *options) apply(opts []Option) *options { for _, fn := range opts { fn(o) } return o } // valid returns true if the validate function passes on the value. func (o *options) valid() bool { if o.validateFunc == nil { return true } return o.validateFunc(o.value) == nil } // getValue validates the value and returns it. func (o *options) getValue() (string, error) { if o.validateFunc == nil { return o.value, nil } if err := o.validateFunc(o.value); err != nil { return "", err } return o.value, nil } // getValueBytes validates the value and returns it as a byte slice. func (o *options) getValueBytes() ([]byte, error) { if o.validateFunc == nil { return []byte(o.value), nil } if err := o.validateFunc(o.value); err != nil { return nil, err } return []byte(o.value), nil } // Option is the type of the functions that modify the prompt options. type Option func(*options) func extractOptions(args []interface{}) (opts []Option, rest []interface{}) { rest = args[:0] for _, arg := range args { if o, ok := arg.(Option); ok { opts = append(opts, o) } else { rest = append(rest, arg) } } return } // WithMask adds a mask to a prompt. func WithMask(r rune) Option { return func(o *options) { o.mask = r } } // WithDefaultValue adds a custom string as the default value. func WithDefaultValue(s string) Option { return func(o *options) { o.defaultValue = s } } // WithSliceValue sets a custom string as the result of a prompt. If value is set, // the prompt won't be displayed. func WithSliceValue(values []string) Option { return func(o *options) { o.value = strings.Join(values, ",") } } // WithValue sets a custom string as the result of a prompt. If value is set, // the prompt won't be displayed. func WithValue(value string) Option { return func(o *options) { o.value = value } } // WithAllowEdit if true, let's the user edit the default value set. func WithAllowEdit(b bool) Option { return func(o *options) { o.allowEdit = b } } // WithPrintTemplate sets the template to use on the print methods. func WithPrintTemplate(template string) Option { return func(o *options) { o.printTemplate = template } } // WithPromptTemplates adds a custom template to a prompt. func WithPromptTemplates(t *promptui.PromptTemplates) Option { return func(o *options) { o.promptTemplates = t } } // WithSelectTemplates adds a custom template to a select. func WithSelectTemplates(t *promptui.SelectTemplates) Option { return func(o *options) { o.selectTemplates = t } } // WithValidateFunc adds a custom validation function to a prompt. func WithValidateFunc(fn func(string) error) Option { return func(o *options) { o.validateFunc = fn } } // WithValidateNotEmpty adds a custom validation function to a prompt that // checks that the propted string is not empty. func WithValidateNotEmpty() Option { return WithValidateFunc(NotEmpty()) } // WithValidateYesNo adds a custom validation function to a prompt for a Yes/No // prompt. func WithValidateYesNo() Option { return WithValidateFunc(YesNo()) } // WithRichPrompt add the template option with rich templates. func WithRichPrompt() Option { return WithPromptTemplates(PromptTemplates()) } // WithSimplePrompt add the template option with simple templates. func WithSimplePrompt() Option { return WithPromptTemplates(SimplePromptTemplates()) } // WithValidateRegexp checks a prompt answer with a regular expression. If the // regular expression is not a valid one, the option will panic. func WithValidateRegexp(re string) Option { rx := regexp.MustCompile(re) return WithValidateFunc(func(s string) error { if rx.MatchString(s) { return nil } return fmt.Errorf("%s does not match the regular expresion %s", s, re) }) } golang-step-cli-utils-0.7.5+ds/ui/templates.go000066400000000000000000000065461433771265400212410ustar00rootroot00000000000000package ui import ( "fmt" "runtime" "github.com/chzyer/readline" "github.com/manifoldco/promptui" ) var ( // IconInitial is the icon used when starting in prompt mode and the icon next to the label when // starting in select mode. IconInitial = promptui.Styler(promptui.FGBlue)("?") // IconGood is the icon used when a good answer is entered in prompt mode. IconGood = promptui.Styler(promptui.FGGreen)("✔") // IconWarn is the icon used when a good, but potentially invalid answer is entered in prompt mode. IconWarn = promptui.Styler(promptui.FGYellow)("⚠") // IconBad is the icon used when a bad answer is entered in prompt mode. IconBad = promptui.Styler(promptui.FGRed)("✗") // IconSelect is the icon used to identify the currently selected item in select mode. IconSelect = promptui.Styler(promptui.FGBold)("▸") ) func init() { // Set VT100 characters for windows too if runtime.GOOS == "windows" { promptui.KeyEnter = readline.CharEnter promptui.KeyBackspace = readline.CharBackspace promptui.KeyPrev = readline.CharPrev promptui.KeyPrevDisplay = "↑" promptui.KeyNext = readline.CharNext promptui.KeyNextDisplay = "↓" promptui.KeyBackward = readline.CharBackward promptui.KeyBackwardDisplay = "←" promptui.KeyForward = readline.CharForward promptui.KeyForwardDisplay = "→" } } // PrintSelectedTemplate returns the default template used in PrintSelected. func PrintSelectedTemplate() string { return fmt.Sprintf(`{{ %q | green }} {{ .Name | bold }}{{ ":" | bold }} {{ .Value }}`, IconGood) + "\n" } // PromptTemplates is the default style for a prompt. func PromptTemplates() *promptui.PromptTemplates { bold := promptui.Styler(promptui.FGBold) return &promptui.PromptTemplates{ Prompt: fmt.Sprintf("%s {{ . | bold }}%s ", IconInitial, bold(":")), Success: fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconGood), bold(":")), // Confirm: fmt.Sprintf(`{{ "%s" | bold }} {{ . | bold }}? {{ "[]" | faint }} `, IconInitial), Valid: fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconGood), bold(":")), Invalid: fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconBad), bold(":")), } } // SimplePromptTemplates is a prompt with a simple style, used by default on password prompts. func SimplePromptTemplates() *promptui.PromptTemplates { return &promptui.PromptTemplates{ Prompt: "{{ . }}: ", Success: "{{ . }}: ", Valid: "{{ . }}: ", Invalid: "{{ . }}: ", } } // SelectTemplates returns the default promptui.SelectTemplate for string // slices. The given name is the prompt of the selected option. func SelectTemplates(name string) *promptui.SelectTemplates { return &promptui.SelectTemplates{ Label: fmt.Sprintf("%s {{ . }}: ", IconInitial), Active: fmt.Sprintf("%s {{ . | underline }}", IconSelect), Inactive: " {{ . }}", Selected: fmt.Sprintf(`{{ %q | green }} {{ "%s:" | bold }} {{ .Name }}`, IconGood, name), } } // NamedSelectTemplates returns the default promptui.SelectTemplate for struct // slices with a name property. The given name is the prompt of the selected // option. func NamedSelectTemplates(name string) *promptui.SelectTemplates { return &promptui.SelectTemplates{ Label: fmt.Sprintf("%s {{.Name}}: ", IconInitial), Active: fmt.Sprintf("%s {{ .Name | underline }}", IconSelect), Inactive: " {{.Name}}", Selected: fmt.Sprintf(`{{ %q | green }} {{ "%s:" | bold }} {{ .Name }}`, IconGood, name), } } golang-step-cli-utils-0.7.5+ds/ui/ui.go000066400000000000000000000200261433771265400176450ustar00rootroot00000000000000package ui import ( "fmt" "os" "strings" "text/template" "github.com/chzyer/readline" "github.com/manifoldco/promptui" "github.com/pkg/errors" "go.step.sm/crypto/randutil" ) // stderr implements an io.WriteCloser that skips the terminal bell character // (ASCII code 7), and writes the rest to os.Stderr. It's used to replace // readline.Stdout, that is the package used by promptui to display the prompts. type stderr struct{} // Write implements an io.WriterCloser over os.Stderr, but it skips the terminal // bell character. func (s *stderr) Write(b []byte) (int, error) { if len(b) == 1 && b[0] == readline.CharBell { return 0, nil } return os.Stderr.Write(b) } // Close implements an io.WriterCloser over os.Stderr. func (s *stderr) Close() error { return os.Stderr.Close() } func init() { readline.Stdout = &stderr{} } // Init initializes the terminal to be used by this package. This is generally a // noop except for windows. func Init() { setConsoleMode() } // Reset sets the terminal as it was before the initialization. This is // generally a noop except for windows. func Reset() { resetConsoleMode() } // Print uses templates to print the arguments formated to os.Stderr. func Print(args ...interface{}) error { var o options opts, args := extractOptions(args) o.apply(opts) // Return with a default value. This is useful when we split the question // and the response in two lines. if o.value != "" && o.valid() { return nil } text := fmt.Sprint(args...) t, err := template.New("Print").Funcs(promptui.FuncMap).Parse(text) if err != nil { return errors.Wrap(err, "error parsing template") } if err := t.Execute(os.Stderr, nil); err != nil { return errors.Wrap(err, "error executing template") } return nil } // Printf uses templates to print the string formated to os.Stderr. func Printf(format string, args ...interface{}) error { var o options opts, args := extractOptions(args) o.apply(opts) // Return with a default value. This is useful when we split the question // and the response in two lines. if o.value != "" && o.valid() { return nil } text := fmt.Sprintf(format, args...) t, err := template.New("Printf").Funcs(promptui.FuncMap).Parse(text) if err != nil { return errors.Wrap(err, "error parsing template") } if err := t.Execute(os.Stderr, nil); err != nil { return errors.Wrap(err, "error executing template") } return nil } // Println uses templates to print the given arguments to os.Stderr func Println(args ...interface{}) error { var o options opts, args := extractOptions(args) o.apply(opts) // Return with a default value. This is useful when we split the question // and the response in two lines. if o.value != "" && o.valid() { return nil } text := fmt.Sprintln(args...) t, err := template.New("Println").Funcs(promptui.FuncMap).Parse(text) if err != nil { return errors.Wrap(err, "error parsing template") } if err := t.Execute(os.Stderr, nil); err != nil { return errors.Wrap(err, "error executing template") } return nil } // PrintSelected prints the given name and value as if they were selected from a // promptui.Select. func PrintSelected(name, value string, opts ...Option) error { o := &options{ printTemplate: PrintSelectedTemplate(), } o.apply(opts) t, err := template.New(name).Funcs(promptui.FuncMap).Parse(o.printTemplate) if err != nil { return errors.Wrap(err, "error parsing template") } data := struct { Name string Value string }{name, value} if err := t.Execute(os.Stderr, data); err != nil { return errors.Wrap(err, "error executing template") } return nil } // Prompt creates and runs a promptui.Prompt with the given label. func Prompt(label string, opts ...Option) (string, error) { o := &options{ promptTemplates: PromptTemplates(), } o.apply(opts) // Return value if set if o.value != "" { return o.getValue() } // Prompt using the terminal clean, err := preparePromptTerminal() if err != nil { return "", err } defer clean() prompt := &promptui.Prompt{ Label: label, Default: o.defaultValue, AllowEdit: o.allowEdit, Validate: o.validateFunc, Templates: o.promptTemplates, } value, err := prompt.Run() if err != nil { return "", errors.Wrap(err, "error running prompt") } return value, nil } // PromptPassword creates and runs a promptui.Prompt with the given label. This // prompt will mask the key entries with \r. func PromptPassword(label string, opts ...Option) ([]byte, error) { // Using a not printable character as they work better than \r o := &options{ mask: 1, promptTemplates: SimplePromptTemplates(), } o.apply(opts) // Return value if set if o.value != "" { return o.getValueBytes() } // Prompt using the terminal clean, err := preparePromptTerminal() if err != nil { return nil, err } defer clean() prompt := &promptui.Prompt{ Label: label, Mask: o.mask, Default: o.defaultValue, AllowEdit: o.allowEdit, Validate: o.validateFunc, Templates: o.promptTemplates, } pass, err := prompt.Run() if err != nil { return nil, errors.Wrap(err, "error reading password") } return []byte(pass), nil } // PromptPasswordGenerate creates and runs a promptui.Prompt with the given label. // This prompt will mask the key entries with \r. If the result password length // is 0, it will generate a new prompt with a generated password that can be // edited. func PromptPasswordGenerate(label string, opts ...Option) ([]byte, error) { pass, err := PromptPassword(label, opts...) if err != nil || len(pass) > 0 { return pass, err } passString, err := randutil.ASCII(32) if err != nil { return nil, err } passString, err = Prompt("Password", WithDefaultValue(passString), WithAllowEdit(true), WithValidateNotEmpty()) if err != nil { return nil, err } return []byte(passString), nil } // PromptYesNo creates and runs a promptui.Prompt with the given label, and // returns true if the answer is y/yes and false if the answer is n/no. func PromptYesNo(label string, opts ...Option) (bool, error) { opts = append([]Option{WithValidateYesNo()}, opts...) s, err := Prompt(label, opts...) if err != nil { return false, err } switch strings.ToLower(strings.TrimSpace(s)) { case "y", "yes": return true, nil case "n", "no": return false, nil default: return false, fmt.Errorf("%s is not a valid answer", s) } } // Select creates and runs a promptui.Select with the given label and items. func Select(label string, items interface{}, opts ...Option) (int, string, error) { o := &options{ selectTemplates: SelectTemplates(label), } o.apply(opts) clean, err := prepareSelectTerminal() if err != nil { return 0, "", err } defer clean() prompt := &promptui.Select{ Label: label, Items: items, Templates: o.selectTemplates, } n, s, err := prompt.Run() if err != nil { return 0, "", errors.Wrap(err, "error running prompt") } return n, s, nil } func preparePromptTerminal() (func(), error) { nothing := func() {} if !readline.DefaultIsTerminal() { tty, err := os.Open("/dev/tty") if err != nil { return nothing, errors.Wrap(err, "error allocating terminal") } clean := func() { tty.Close() } fd := int(tty.Fd()) state, err := readline.MakeRaw(fd) if err != nil { defer clean() return nothing, errors.Wrap(err, "error making raw terminal") } stdin := readline.Stdin readline.Stdin = tty clean = func() { readline.Stdin = stdin readline.Restore(fd, state) tty.Close() } return clean, nil } return nothing, nil } func prepareSelectTerminal() (func(), error) { nothing := func() {} if !readline.DefaultIsTerminal() { tty, err := os.Open("/dev/tty") if err != nil { return nothing, errors.Wrap(err, "error allocating terminal") } clean := func() { tty.Close() } fd := int(tty.Fd()) state, err := readline.MakeRaw(fd) if err != nil { defer clean() return nothing, errors.Wrap(err, "error making raw terminal") } stdin := os.Stdin os.Stdin = tty clean = func() { os.Stdin = stdin readline.Restore(fd, state) tty.Close() } return clean, nil } return nothing, nil } golang-step-cli-utils-0.7.5+ds/ui/ui_other.go000066400000000000000000000001511433771265400210430ustar00rootroot00000000000000//go:build !windows // +build !windows package ui func setConsoleMode() {} func resetConsoleMode() {} golang-step-cli-utils-0.7.5+ds/ui/ui_windows.go000066400000000000000000000023751433771265400214260ustar00rootroot00000000000000//go:build windows // +build windows package ui import ( "fmt" "os" "golang.org/x/sys/windows" ) var inMode, outMode uint32 func init() { var _ = windows.GetConsoleMode(windows.Stdin, &inMode) var _ = windows.GetConsoleMode(windows.Stdout, &outMode) } func setConsoleMode() { in := inMode | windows.ENABLE_VIRTUAL_TERMINAL_INPUT out := outMode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING if inMode != 0 && inMode != in { if err := windows.SetConsoleMode(windows.Stdin, in); err != nil { fmt.Fprintf(os.Stderr, "Failed to set console mode: %v\n", err) } } if outMode != 0 && outMode != out { if err := windows.SetConsoleMode(windows.Stdout, out); err != nil { fmt.Fprintf(os.Stderr, "Failed to set console mode: %v\n", err) } } } func resetConsoleMode() { in := inMode | windows.ENABLE_VIRTUAL_TERMINAL_INPUT out := outMode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING if inMode != 0 && inMode != in { if err := windows.SetConsoleMode(windows.Stdin, inMode); err != nil { fmt.Fprintf(os.Stderr, "Failed to reset console mode: %v\n", err) } } if outMode != 0 && outMode != out { if err := windows.SetConsoleMode(windows.Stdout, outMode); err != nil { fmt.Fprintf(os.Stderr, "Failed to reset console mode: %v\n", err) } } } golang-step-cli-utils-0.7.5+ds/ui/validators.go000066400000000000000000000032601433771265400214010ustar00rootroot00000000000000package ui import ( "errors" "fmt" "net" "strings" "github.com/manifoldco/promptui" ) var errEmptyValue = errors.New("value is empty") // NotEmpty is a validation function that checks that the prompted string is not // empty. func NotEmpty() promptui.ValidateFunc { return func(s string) error { if strings.TrimSpace(s) == "" { return errEmptyValue } return nil } } // Address is a validation function that checks that the prompted string is a // valid TCP address. func Address() promptui.ValidateFunc { return func(s string) error { if _, _, err := net.SplitHostPort(s); err != nil { return fmt.Errorf("%s is not an TCP address", s) } return nil } } // IPAddress is validation function that checks that the prompted string is a // valid IP address. func IPAddress() promptui.ValidateFunc { return func(s string) error { if net.ParseIP(s) == nil { return fmt.Errorf("%s is not an ip address", s) } return nil } } // DNS is a validation function that checks that the prompted string is a valid // DNS name or IP address. func DNS() promptui.ValidateFunc { return func(s string) error { if strings.TrimSpace(s) == "" { return errEmptyValue } if ip := net.ParseIP(s); ip != nil { return nil } if _, _, err := net.SplitHostPort(s + ":443"); err != nil { return fmt.Errorf("%s is not a valid DNS name or IP address", s) } return nil } } // YesNo is a validation function that checks for a Yes/No answer. func YesNo() promptui.ValidateFunc { return func(s string) error { s = strings.ToLower(strings.TrimSpace(s)) switch s { case "y", "yes", "n", "no": return nil default: return fmt.Errorf("%s is not a valid answer", s) } } } golang-step-cli-utils-0.7.5+ds/ui/validators_test.go000066400000000000000000000031551433771265400224430ustar00rootroot00000000000000package ui import ( "testing" ) func TestDNS(t *testing.T) { tests := []struct { name string input string wantErr bool }{ { name: "empty", input: "", wantErr: true, }, { name: "localhost", input: "localhost", wantErr: false, }, { name: "example.com", input: "example.com", wantErr: false, }, { name: "ca.smallstep.com", input: "ca.smallstep.com", wantErr: false, }, { name: "localhost-with-port", input: "localhost:443", wantErr: true, }, { name: "quad1", input: "1.1.1.1", wantErr: false, }, { name: "ipv4-localhost", input: "127.0.0.1", wantErr: false, }, { name: "ipv6-localhost", input: "::1", wantErr: false, }, { name: "ipv6-localhost-brackets", input: "[::1]", wantErr: false, }, { name: "ipv6", input: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", wantErr: false, }, { name: "ipv6-brackets", input: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", wantErr: false, }, { name: "ipv6-shortened", input: "2001:0db8:85a3::8a2e:0370:7334", wantErr: false, }, { name: "ipv6-shortened-brackets", input: "[2001:0db8:85a3::8a2e:0370:7334]", wantErr: false, }, { name: "ipv6-shortened-brackets-missing-end", input: "[2001:0db8:85a3::8a2e:0370:7334", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotErr := DNS()(tt.input) != nil if gotErr != tt.wantErr { t.Errorf("DNS()(%s) = %v, want %v", tt.input, gotErr, tt.wantErr) } }) } } golang-step-cli-utils-0.7.5+ds/usage/000077500000000000000000000000001433771265400173705ustar00rootroot00000000000000golang-step-cli-utils-0.7.5+ds/usage/css.go000066400000000000000000000340441433771265400205140ustar00rootroot00000000000000package usage // CSS code replicating Github style. // From https://github.com/sindresorhus/github-markdown-css // MIT license var css = `@font-face { font-family: octicons-link; src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); } .wrapper { margin: 0 auto; max-width: 700px; padding: 20px 10px; } .markdown-body { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; line-height: 1.5; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; line-height: 1.5; word-wrap: break-word; } .markdown-body .pl-c { color: #6a737d; } .markdown-body .pl-c1, .markdown-body .pl-s .pl-v { color: #005cc5; } .markdown-body .pl-e, .markdown-body .pl-en { color: #6f42c1; } .markdown-body .pl-smi, .markdown-body .pl-s .pl-s1 { color: #24292e; } .markdown-body .pl-ent { color: #22863a; } .markdown-body .pl-k { color: #d73a49; } .markdown-body .pl-s, .markdown-body .pl-pds, .markdown-body .pl-s .pl-pse .pl-s1, .markdown-body .pl-sr, .markdown-body .pl-sr .pl-cce, .markdown-body .pl-sr .pl-sre, .markdown-body .pl-sr .pl-sra { color: #032f62; } .markdown-body .pl-v, .markdown-body .pl-smw { color: #e36209; } .markdown-body .pl-bu { color: #b31d28; } .markdown-body .pl-ii { color: #fafbfc; background-color: #b31d28; } .markdown-body .pl-c2 { color: #fafbfc; background-color: #d73a49; } .markdown-body .pl-c2::before { content: "^M"; } .markdown-body .pl-sr .pl-cce { font-weight: bold; color: #22863a; } .markdown-body .pl-ml { color: #735c0f; } .markdown-body .pl-mh, .markdown-body .pl-mh .pl-en, .markdown-body .pl-ms { font-weight: bold; color: #005cc5; } .markdown-body .pl-mi { font-style: italic; color: #24292e; } .markdown-body .pl-mb { font-weight: bold; color: #24292e; } .markdown-body .pl-md { color: #b31d28; background-color: #ffeef0; } .markdown-body .pl-mi1 { color: #22863a; background-color: #f0fff4; } .markdown-body .pl-mc { color: #e36209; background-color: #ffebda; } .markdown-body .pl-mi2 { color: #f6f8fa; background-color: #005cc5; } .markdown-body .pl-mdr { font-weight: bold; color: #6f42c1; } .markdown-body .pl-ba { color: #586069; } .markdown-body .pl-sg { color: #959da5; } .markdown-body .pl-corl { text-decoration: underline; color: #032f62; } .markdown-body .octicon { display: inline-block; vertical-align: text-top; fill: currentColor; } .markdown-body a { background-color: transparent; } .markdown-body a:active, .markdown-body a:hover { outline-width: 0; } .markdown-body strong { font-weight: inherit; } .markdown-body strong { font-weight: bolder; } .markdown-body h1 { font-size: 2em; margin: 0.67em 0; } .markdown-body img { border-style: none; } .markdown-body code, .markdown-body kbd, .markdown-body pre { font-family: monospace, monospace; font-size: 1em; } .markdown-body hr { box-sizing: content-box; height: 0; overflow: visible; } .markdown-body input { font: inherit; margin: 0; } .markdown-body input { overflow: visible; } .markdown-body [type="checkbox"] { box-sizing: border-box; padding: 0; } .markdown-body * { box-sizing: border-box; } .markdown-body input { font-family: inherit; font-size: inherit; line-height: inherit; } .markdown-body a { color: #0366d6; text-decoration: none; } .markdown-body a:hover { text-decoration: underline; } .markdown-body strong { font-weight: 600; } .markdown-body hr { height: 0; margin: 15px 0; overflow: hidden; background: transparent; border: 0; border-bottom: 1px solid #dfe2e5; } .markdown-body hr::before { display: table; content: ""; } .markdown-body hr::after { display: table; clear: both; content: ""; } .markdown-body table { border-spacing: 0; border-collapse: collapse; } .markdown-body td, .markdown-body th { padding: 0; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 0; margin-bottom: 0; } .markdown-body h1 { font-size: 32px; font-weight: 600; } .markdown-body h2 { font-size: 24px; font-weight: 600; } .markdown-body h3 { font-size: 20px; font-weight: 600; } .markdown-body h4 { font-size: 16px; font-weight: 600; } .markdown-body h5 { font-size: 14px; font-weight: 600; } .markdown-body h6 { font-size: 12px; font-weight: 600; } .markdown-body p { margin-top: 0; margin-bottom: 10px; } .markdown-body blockquote { margin: 0; } .markdown-body ul, .markdown-body ol { padding-left: 0; margin-top: 0; margin-bottom: 0; } .markdown-body ol ol, .markdown-body ul ol { list-style-type: lower-roman; } .markdown-body ul ul ol, .markdown-body ul ol ol, .markdown-body ol ul ol, .markdown-body ol ol ol { list-style-type: lower-alpha; } .markdown-body dd { margin-left: 0; } .markdown-body code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; } .markdown-body pre { margin-top: 0; margin-bottom: 0; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; } .markdown-body .octicon { vertical-align: text-bottom; } .markdown-body .pl-0 { padding-left: 0 !important; } .markdown-body .pl-1 { padding-left: 4px !important; } .markdown-body .pl-2 { padding-left: 8px !important; } .markdown-body .pl-3 { padding-left: 16px !important; } .markdown-body .pl-4 { padding-left: 24px !important; } .markdown-body .pl-5 { padding-left: 32px !important; } .markdown-body .pl-6 { padding-left: 40px !important; } .markdown-body::before { display: table; content: ""; } .markdown-body::after { display: table; clear: both; content: ""; } .markdown-body>*:first-child { margin-top: 0 !important; } .markdown-body>*:last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body .anchor { float: left; padding-right: 4px; margin-left: -20px; line-height: 1; } .markdown-body .anchor:focus { outline: none; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre { margin-top: 0; margin-bottom: 16px; } .markdown-body hr { height: 0.25em; padding: 0; margin: 24px 0; background-color: #e1e4e8; border: 0; } .markdown-body blockquote { padding: 0 1em; color: #6a737d; border-left: 0.25em solid #dfe2e5; } .markdown-body blockquote>:first-child { margin-top: 0; } .markdown-body blockquote>:last-child { margin-bottom: 0; } .markdown-body kbd { display: inline-block; padding: 3px 5px; font-size: 11px; line-height: 10px; color: #444d56; vertical-align: middle; background-color: #fafbfc; border: solid 1px #c6cbd1; border-bottom-color: #959da5; border-radius: 3px; box-shadow: inset 0 -1px 0 #959da5; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 24px; margin-bottom: 16px; font-weight: 600; line-height: 1.25; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: #1b1f23; vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body h1 { padding-bottom: 0.3em; font-size: 2em; border-bottom: 1px solid #eaecef; } .markdown-body h2 { padding-bottom: 0.3em; font-size: 1.5em; border-bottom: 1px solid #eaecef; } .markdown-body h3 { font-size: 1.25em; } .markdown-body h4 { font-size: 1em; } .markdown-body h5 { font-size: 0.875em; } .markdown-body h6 { font-size: 0.85em; color: #6a737d; } .markdown-body ul, .markdown-body ol { padding-left: 2em; } .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0; margin-bottom: 0; } .markdown-body li { word-wrap: break-all; } .markdown-body li>p { margin-top: 16px; } .markdown-body li+li { margin-top: 0.25em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { padding: 0; margin-top: 16px; font-size: 1em; font-style: italic; font-weight: 600; } .markdown-body dl dd { padding: 0 16px; margin-bottom: 16px; } .markdown-body table { display: block; width: 100%; overflow: auto; } .markdown-body table th { font-weight: 600; } .markdown-body table th, .markdown-body table td { padding: 6px 13px; border: 1px solid #dfe2e5; } .markdown-body table tr { background-color: #fff; border-top: 1px solid #c6cbd1; } .markdown-body table tr:nth-child(2n) { background-color: #f6f8fa; } .markdown-body img { max-width: 100%; box-sizing: content-box; background-color: #fff; } .markdown-body img[align=right] { padding-left: 20px; } .markdown-body img[align=left] { padding-right: 20px; } .markdown-body code { padding: 0.2em 0.4em; margin: 0; font-size: 85%; background-color: rgba(27,31,35,0.05); border-radius: 3px; } .markdown-body pre { word-wrap: normal; } .markdown-body pre>code { padding: 0; margin: 0; font-size: 100%; word-break: normal; white-space: pre; background: transparent; border: 0; } .markdown-body .highlight { margin-bottom: 16px; } .markdown-body .highlight pre { margin-bottom: 0; word-break: normal; } .markdown-body .highlight pre, .markdown-body pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 3px; } .markdown-body pre code { display: inline; max-width: auto; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; } .markdown-body .full-commit .btn-outline:not(:disabled):hover { color: #005cc5; border-color: #005cc5; } .markdown-body kbd { display: inline-block; padding: 3px 5px; font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 10px; color: #444d56; vertical-align: middle; background-color: #fafbfc; border: solid 1px #d1d5da; border-bottom-color: #c6cbd1; border-radius: 3px; box-shadow: inset 0 -1px 0 #c6cbd1; } .markdown-body :checked+.radio-label { position: relative; z-index: 1; border-color: #0366d6; } .markdown-body .task-list-item { list-style-type: none; } .markdown-body .task-list-item+.task-list-item { margin-top: 3px; } .markdown-body .task-list-item input { margin: 0 0.2em 0.25em -1.6em; vertical-align: middle; } .markdown-body hr { border-bottom-color: #eee; } .command { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 16px; padding-left: 40px; } .command h1 { border: none; margin-left: -40px; } .command h2 { border: none; margin-top: 2em; margin-left: -40px; font-size: 18px; } .command>ul { padding-left: 0; } .command ul { list-style-type: none; } .command table { margin: 2em 0 1em; display: block; width: 100%; overflow: auto; border-collapse: collapse; } .command table th { font-weight: 600; } .command table th, .command table td { padding: 6px 13px; border: 1px solid #dfe2e5; } .command table tr { background-color: #fff; border-top: 1px solid #c6cbd1; } .command table tr:nth-child(2n) { background-color: #f6f8fa; } ` golang-step-cli-utils-0.7.5+ds/usage/help.go000066400000000000000000000105521433771265400206520ustar00rootroot00000000000000package usage import ( "fmt" "strings" "github.com/urfave/cli" ) // HelpCommandAction is the action function of the overwritten help command. var HelpCommandAction = cli.ActionFunc(helpAction) // HelpCommand overwrites default urfvafe/cli help command to support one or // multiple subcommands like: // // step help // step help crypto // step help crypto jwt // step help crypto jwt sign // ... func HelpCommand() cli.Command { return cli.Command{ Name: "help", Aliases: []string{"h"}, Usage: "display help for the specified command or command group", UsageText: "**step help** ", Description: `**step help** command displays help for a command or command group. ## EXAMPLES Display help for **step ca certificate**: ''' $ step help ca certificate ''' Display help for **step ssh**: ''' $ step help ssh '''`, Action: HelpCommandAction, Flags: []cli.Flag{ cli.StringFlag{ Name: "http", Usage: "HTTP service address (e.g., ':8080')", }, cli.StringFlag{ Name: "html", Usage: "The export for HTML docs.", }, cli.StringFlag{ Name: "markdown", Usage: "The export for Markdown docs.", }, cli.BoolFlag{ Name: "report", Usage: "Writes a JSON report to the HTML docs directory.", }, cli.BoolFlag{ Name: "hugo", Usage: "Writes hugo (vs jekyll) compatible markdown files", }, }, } } func helpAction(ctx *cli.Context) error { // use html version if ctx.IsSet("http") { return httpHelpAction(ctx) } if ctx.IsSet("html") { return htmlHelpAction(ctx) } if ctx.IsSet("markdown") { return markdownHelpAction(ctx) } args := ctx.Args() if args.Present() { last := len(args) - 1 lastName := args[last] subcmd := ctx.App.Commands parent := createParentCommand(ctx) for _, name := range args[:last] { for _, cmd := range subcmd { if cmd.HasName(name) { parent = cmd subcmd = cmd.Subcommands break } } } for _, cmd := range subcmd { if !cmd.HasName(lastName) { continue } cmd.HelpName = fmt.Sprintf("%s %s", ctx.App.HelpName, strings.Join(args, " ")) parent.HelpName = fmt.Sprintf("%s %s", ctx.App.HelpName, strings.Join(args[:last], " ")) ctx.Command = cmd if len(cmd.Subcommands) == 0 { ctx.App = createCliApp(ctx, parent) return cli.ShowCommandHelp(ctx, lastName) } ctx.App = createCliApp(ctx, cmd) return cli.ShowCommandHelp(ctx, "") } return cli.NewExitError(fmt.Sprintf("No help topic for '%s %s'", ctx.App.Name, strings.Join(args, " ")), 3) } cli.ShowAppHelp(ctx) return nil } // createParentCommand returns a command representation of the app. func createParentCommand(ctx *cli.Context) cli.Command { return cli.Command{ Name: ctx.App.Name, HelpName: ctx.App.HelpName, Usage: ctx.App.Usage, UsageText: ctx.App.UsageText, ArgsUsage: ctx.App.ArgsUsage, Description: ctx.App.Description, Subcommands: ctx.App.Commands, Flags: ctx.App.Flags, } } // createCliApp is re-implementation of urfave/cli method (in command.go): // // func (c Command) startApp(ctx *Context) error // // It lets us show the subcommands when help is executed like: // // step help foo // step help foo bar // ... func createCliApp(ctx *cli.Context, cmd cli.Command) *cli.App { app := cli.NewApp() app.Metadata = ctx.App.Metadata // set the name and usage app.Name = cmd.HelpName app.HelpName = cmd.HelpName app.Usage = cmd.Usage app.UsageText = cmd.UsageText app.Description = cmd.Description app.ArgsUsage = cmd.ArgsUsage // set CommandNotFound app.CommandNotFound = ctx.App.CommandNotFound app.CustomAppHelpTemplate = cmd.CustomHelpTemplate // set the flags and commands app.Commands = cmd.Subcommands app.Flags = cmd.Flags app.Version = ctx.App.Version app.Compiled = ctx.App.Compiled app.Author = ctx.App.Author app.Email = ctx.App.Email app.Writer = ctx.App.Writer app.ErrWriter = ctx.App.ErrWriter // Do not show help or version on subcommands app.HideHelp = true app.HideVersion = true // bash completion app.EnableBashCompletion = ctx.App.EnableBashCompletion if cmd.BashComplete != nil { app.BashComplete = cmd.BashComplete } // set the actions app.Before = cmd.Before app.After = cmd.After if cmd.Action != nil { app.Action = cmd.Action } else { app.Action = helpAction } app.OnUsageError = cmd.OnUsageError app.Setup() return app } golang-step-cli-utils-0.7.5+ds/usage/html.go000066400000000000000000000174711433771265400206750ustar00rootroot00000000000000package usage import ( "fmt" "net/http" "os" "path" "strings" "github.com/urfave/cli" "go.step.sm/cli-utils/errs" ) func httpHelpAction(ctx *cli.Context) error { addr := ctx.String("http") if addr == "" { return errs.RequiredFlag(ctx, "http") } fmt.Printf("Serving HTTP on %s ...\n", addr) return http.ListenAndServe(addr, &htmlHelpHandler{ cliApp: ctx.App, }) } func markdownHelpAction(ctx *cli.Context) error { dir := path.Clean(ctx.String("markdown")) if err := os.MkdirAll(dir, 0755); err != nil { return errs.FileError(err, dir) } isHugo := ctx.Bool("hugo") // app index index := path.Join(dir, "index.md") w, err := os.Create(index) if err != nil { return errs.FileError(err, index) } markdownHelpPrinter(w, mdAppHelpTemplate, "", ctx.App) if err := w.Close(); err != nil { return errs.FileError(err, index) } // Subcommands for _, cmd := range ctx.App.Commands { if err := markdownHelpCommand(ctx.App, cmd, cmd, path.Join(dir, cmd.Name), isHugo); err != nil { return err } } return nil } func markdownHelpCommand(app *cli.App, cmd, parent cli.Command, base string, isHugo bool) error { if err := os.MkdirAll(base, 0755); err != nil { return errs.FileError(err, base) } fileName := "index.md" // preserve jekyll compatibility for transition period if isHugo && len(cmd.Subcommands) > 0 { fileName = "_index.md" } index := path.Join(base, fileName) w, err := os.Create(index) if err != nil { return errs.FileError(err, index) } parentName := parent.HelpName if cmd.HelpName == parent.HelpName { parentName = "step" } if len(cmd.Subcommands) == 0 { markdownHelpPrinter(w, mdCommandHelpTemplate, parentName, cmd) return errs.FileError(w.Close(), index) } ctx := cli.NewContext(app, nil, nil) ctx.App = createCliApp(ctx, cmd) markdownHelpPrinter(w, mdSubcommandHelpTemplate, parentName, ctx.App) if err := w.Close(); err != nil { return errs.FileError(err, index) } for _, sub := range cmd.Subcommands { sub.HelpName = fmt.Sprintf("%s %s", cmd.HelpName, sub.Name) if err := markdownHelpCommand(app, sub, cmd, path.Join(base, sub.Name), isHugo); err != nil { return err } } return nil } func htmlHelpAction(ctx *cli.Context) error { dir := path.Clean(ctx.String("html")) if err := os.MkdirAll(dir, 0755); err != nil { return errs.FileError(err, dir) } // app index index := path.Join(dir, "index.html") w, err := os.Create(index) if err != nil { return errs.FileError(err, index) } tophelp := htmlHelpPrinter(w, mdAppHelpTemplate, ctx.App) var report *Report if ctx.IsSet("report") { report = NewReport(ctx.App.Name, tophelp) } if err := w.Close(); err != nil { return errs.FileError(err, index) } // css style cssFile := path.Join(dir, "style.css") if err := os.WriteFile(cssFile, []byte(css), 0666); err != nil { return errs.FileError(err, cssFile) } // Subcommands for _, cmd := range ctx.App.Commands { if err := htmlHelpCommand(ctx.App, cmd, path.Join(dir, cmd.Name), report); err != nil { return err } } // report if report != nil { repjson := path.Join(dir, "report.json") rjw, err := os.Create(repjson) if err != nil { return errs.FileError(err, repjson) } if err := report.Write(rjw); err != nil { return err } if err := rjw.Close(); err != nil { return errs.FileError(err, repjson) } } return nil } func htmlHelpCommand(app *cli.App, cmd cli.Command, base string, report *Report) error { if err := os.MkdirAll(base, 0755); err != nil { return errs.FileError(err, base) } index := path.Join(base, "index.html") w, err := os.Create(index) if err != nil { return errs.FileError(err, index) } if len(cmd.Subcommands) == 0 { cmdhelp := htmlHelpPrinter(w, mdCommandHelpTemplate, cmd) if report != nil { report.Process(cmd.HelpName, cmdhelp) } return errs.FileError(w.Close(), index) } ctx := cli.NewContext(app, nil, nil) ctx.App = createCliApp(ctx, cmd) subhelp := htmlHelpPrinter(w, mdSubcommandHelpTemplate, ctx.App) if report != nil { report.Process(cmd.HelpName, subhelp) } if err := w.Close(); err != nil { return errs.FileError(err, index) } for _, sub := range cmd.Subcommands { sub.HelpName = fmt.Sprintf("%s %s", cmd.HelpName, sub.Name) if err := htmlHelpCommand(app, sub, path.Join(base, sub.Name), report); err != nil { return err } } return nil } type htmlHelpHandler struct { cliApp *cli.App } func (h *htmlHelpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := cli.NewContext(h.cliApp, nil, nil) // clean request URI requestURI := path.Clean(req.RequestURI) if requestURI == "/" { htmlHelpPrinter(w, mdAppHelpTemplate, ctx.App) return } if requestURI == "/style.css" { w.Header().Set("Content-Type", `text/css; charset="utf-8"`) w.Write([]byte(css)) return } args := strings.Split(requestURI, "/") last := len(args) - 1 lastName := args[last] subcmd := ctx.App.Commands parent := createParentCommand(ctx) for _, name := range args[:last] { for _, cmd := range subcmd { if cmd.HasName(name) { parent = cmd subcmd = cmd.Subcommands break } } } for _, cmd := range subcmd { if !cmd.HasName(lastName) { continue } cmd.HelpName = fmt.Sprintf("%s %s", ctx.App.HelpName, strings.Join(args, " ")) parent.HelpName = fmt.Sprintf("%s %s", ctx.App.HelpName, strings.Join(args[:last], " ")) ctx.Command = cmd if len(cmd.Subcommands) == 0 { htmlHelpPrinter(w, mdCommandHelpTemplate, cmd) return } ctx.App = createCliApp(ctx, cmd) htmlHelpPrinter(w, mdSubcommandHelpTemplate, ctx.App) return } http.NotFound(w, req) } // AppHelpTemplate contains the modified template for the main app var mdAppHelpTemplate = `## NAME **{{.HelpName}}** -- {{.Usage}} ## USAGE '''raw {{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}**{{if .Commands}} {{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments]{{end}}{{end}} ''' {{- if .Description}} ## DESCRIPTION {{.Description}}{{end}}{{if .VisibleCommands}} ## COMMANDS {{range .VisibleCategories}}{{if .Name}}{{.Name}}:{{end}} | Name | Usage | |---|---|{{range .VisibleCommands}} | **[{{join .Names ", "}}]({{.Name}}/)** | {{.Usage}} |{{end}} {{end}}{{if .VisibleFlags}}{{end}} ## OPTIONS {{range $index, $option := .VisibleFlags}}{{if $index}} {{end}}{{$option}} {{end}}{{end}}{{if .Copyright}}{{if len .Authors}} ## AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{range $index, $author := .Authors}}{{if $index}} {{end}}{{$author}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} ## VERSION {{.Version}}{{end}}{{end}} ## COPYRIGHT {{.Copyright}} {{end}} ` // SubcommandHelpTemplate contains the modified template for a sub command // Note that the weird "|||\n|---|---|" syntax sets up a markdown table with empty headers. var mdSubcommandHelpTemplate = `## NAME **{{.HelpName}}** -- {{.Usage}} ## USAGE '''raw {{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}** {{if .VisibleFlags}} [options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments]{{end}}{{end}} ''' {{- if .Description}} ## DESCRIPTION {{.Description}}{{end}} ## COMMANDS {{range .VisibleCategories}}{{if .Name}}{{.Name}}:{{end}} | Name | Usage | |---|---|{{range .VisibleCommands}} | **[{{join .Names ", "}}]({{.Name}}/)** | {{.Usage}} |{{end}} {{end}}{{if .VisibleFlags}} ## OPTIONS {{range .VisibleFlags}} {{.}} {{end}}{{end}} ` // CommandHelpTemplate contains the modified template for a command var mdCommandHelpTemplate = `## NAME **{{.HelpName}}** -- {{.Usage}} ## USAGE '''raw {{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}**{{if .VisibleFlags}} [options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments]{{end}}{{end}} ''' {{- if .Category}} ## CATEGORY {{.Category}}{{end}}{{if .Description}} ## DESCRIPTION {{.Description}}{{end}}{{if .VisibleFlags}} ## OPTIONS {{range .VisibleFlags}} {{.}} {{end}}{{end}} ` golang-step-cli-utils-0.7.5+ds/usage/printer.go000066400000000000000000000130051433771265400214010ustar00rootroot00000000000000package usage import ( "bytes" "fmt" "io" "regexp" "strings" "text/template" "unicode" "github.com/urfave/cli" md "go.step.sm/cli-utils/pkg/blackfriday" ) var sectionRe = regexp.MustCompile(`(?m:^##)`) var sectionNameRe = regexp.MustCompile(`(?m:^## [^\n]+)`) var indentRe = regexp.MustCompile(`(?m:^:[^\n]+)`) var definitionListRe = regexp.MustCompile(`(?m:^[\t ]+\*\*[^\*]+\*\*[^\n]*\s+:[^\n]+)`) //var sectionRe = regexp.MustCompile(`^## [^\n]*$`) type frontmatterData struct { Data interface{} Parent string Children []string } // HelpPrinter overwrites cli.HelpPrinter and prints the formatted help to the terminal. func HelpPrinter(w io.Writer, templ string, data interface{}) { b := helpPreprocessor(w, templ, data, false) w.Write(Render(b)) } func htmlHelpPrinter(w io.Writer, templ string, data interface{}) []byte { b := helpPreprocessor(w, templ, data, true) w.Write([]byte(`step command line documentation`)) w.Write([]byte(``)) w.Write([]byte(`
`)) html := md.Run(b) w.Write(html) w.Write([]byte(`
`)) return html } func markdownHelpPrinter(w io.Writer, templ, parent string, data interface{}) { b := helpPreprocessor(w, templ, data, true) frontmatter := frontmatterData{ Data: data, Parent: parent, } if app, ok := data.(*cli.App); ok { for _, cmd := range app.Commands { frontmatter.Children = append(frontmatter.Children, cmd.Name) } } var frontMatterTemplate = `--- layout: auto-doc category: reference title: {{.Data.HelpName}} menu: docs: {{- if .Parent}} parent: {{.Parent}} {{- end }} {{- if .Children }} children: {{- range .Children }} - {{.}} {{- end }} {{- end }} --- ` t, err := template.New("frontmatter").Parse(frontMatterTemplate) if err != nil { panic(err) } err = t.Execute(w, frontmatter) if err != nil { panic(err) } w.Write(b) } func helpPreprocessor(w io.Writer, templ string, data interface{}, applyRx bool) []byte { buf := new(bytes.Buffer) cli.HelpPrinterCustom(buf, templ, data, nil) //w.Write(buf.Bytes()) // s := string(markdownify(buf.Bytes())) s := markdownify(buf) // Move the OPTIONS section to the right place. urfave puts them at the end // of the file, we want them to be after POSITIONAL ARGUMENTS, DESCRIPTION, // USAGE, or NAME (in that order, depending on which sections exist). optLoc := strings.Index(s, "## OPTIONS") if optLoc != -1 { optEnd := findSectionEnd("OPTIONS", s) if optEnd != -1 { options := s[optLoc:optEnd] s = s[:optLoc] + s[optEnd:] if newLoc := findSectionEnd("POSITIONAL ARGUMENTS", s); newLoc != -1 { s = s[:newLoc] + options + s[newLoc:] } else if newLoc := findSectionEnd("DESCRIPTION", s); newLoc != -1 { s = s[:newLoc] + options + s[newLoc:] } else if newLoc := findSectionEnd("USAGE", s); newLoc != -1 { s = s[:newLoc] + options + s[newLoc:] } else if newLoc := findSectionEnd("NAME", s); newLoc != -1 { s = s[:newLoc] + options + s[newLoc:] } else { // Keep it at the end I guess :/. s += options } } } if applyRx { // Keep capitalized only the first letter in arguments names. s = sectionNameRe.ReplaceAllStringFunc(s, func(s string) string { return s[0:4] + strings.ToLower(s[4:]) }) // Remove `:` at the start of a line. s = indentRe.ReplaceAllStringFunc(s, func(s string) string { return strings.TrimSpace(s[1:]) }) // Convert lines like: // **Foo** // : Bar zar ... // To: // - **Foo**: Bar zar ... s = definitionListRe.ReplaceAllStringFunc(s, func(s string) string { i := strings.Index(s, "\n") j := strings.Index(s, ":") return "- " + strings.TrimSpace(s[:i]) + ": " + strings.TrimSpace(s[j+1:]) }) } return []byte(s) } func findSectionEnd(h, s string) int { start := strings.Index(s, fmt.Sprintf("## %s", h)) if start == -1 { return start } nextSection := sectionRe.FindStringIndex(s[start+2:]) if nextSection == nil { return len(s) } return start + 2 + nextSection[0] } // Convert some stuff that we can't easily write in help files because // // backticks and raw strings don't mix: // // - "" to "`foo`" // - "”'" to "```" func markdownify(r *bytes.Buffer) string { const escapeByte = byte('\\') var last byte var inCode bool w := new(bytes.Buffer) for { b, err := r.ReadByte() if err != nil { return w.String() } loop: switch b { case '<': if last != escapeByte && !inCode { w.WriteByte('`') } else { w.WriteByte(b) } case '>': if last != escapeByte && !inCode { w.WriteByte('`') } else { w.WriteByte(b) } case '\'': b1, _ := r.ReadByte() b2, _ := r.ReadByte() if b1 == b && b2 == b { w.WriteString("```") if !inCode { if n, _, err := r.ReadRune(); err == nil { if unicode.IsSpace(n) { w.WriteString("shell") } r.UnreadRune() } } inCode = !inCode } else { // We can only unread the last one (b2) w.WriteByte(b) r.UnreadByte() b = b1 last = b goto loop } case '*': if inCode { if b1, _ := r.ReadByte(); b1 != '*' { w.WriteByte(b) w.UnreadByte() } } else { w.WriteByte(b) } case escapeByte: if last == escapeByte { w.WriteByte(escapeByte) b = 0 } else if n, _, err := r.ReadRune(); err == nil { if unicode.IsSpace(n) { w.WriteByte(escapeByte) } r.UnreadRune() } case 0: // probably because io.EOF default: w.WriteByte(b) } last = b } } golang-step-cli-utils-0.7.5+ds/usage/renderer.go000066400000000000000000000233231433771265400215300ustar00rootroot00000000000000package usage import ( "bufio" "bytes" "fmt" "io" "regexp" "strings" "text/tabwriter" "unicode" "github.com/mgutz/ansi" md "go.step.sm/cli-utils/pkg/blackfriday" ) // Render renders the given data with a custom markdown renderer. func Render(b []byte) []byte { return md.Run(b, md.WithRenderer(&Renderer{6, 0, nil, nil, false})) } var colorEscapeRe = regexp.MustCompile(`\033\[\d*(;\d*)?m?\]?`) var maxLineLength = 80 func stripColors(b []byte) []byte { return colorEscapeRe.ReplaceAll(b, []byte("")) } type item struct { flags md.ListType term []byte definitions [][]byte } type list struct { items []item flags md.ListType parent *list } /* TODO: commented because unused func (l *list) isUnordered() bool { return !l.isOrdered() && !l.isDefinition() } func (l *list) isOrdered() bool { return l.flags&md.ListTypeOrdered != 0 } func (l *list) containsBlock() bool { // TODO: Not sure if we have to check every item or if it gets // automatically set on the list? return l.flags&md.ListItemContainsBlock != 0 } */ func (l *list) isDefinition() bool { return l.flags&md.ListTypeDefinition != 0 } type bufqueue struct { w io.Writer buf *bytes.Buffer next *bufqueue mode RenderMode } // RenderMode enumerates different line breaks modes. type RenderMode int const ( // RenderModeKeepBreaks will keep the line breaks in the docs. RenderModeKeepBreaks RenderMode = iota // RenderModeBreakLines will automatically wrap the lines. RenderModeBreakLines ) // Renderer implements a custom markdown renderer for blackfriday. type Renderer struct { depth int listdepth int list *list out *bufqueue inpara bool } func (r *Renderer) write(b []byte) { r.out.w.Write(b) } func (r *Renderer) printf(s string, a ...interface{}) { fmt.Fprintf(r.out.w, s, a...) } func (r *Renderer) capture(mode RenderMode) { buf := new(bytes.Buffer) r.out = &bufqueue{buf, buf, r.out, mode} } func (r *Renderer) finishCapture() *bytes.Buffer { buf := r.out.buf r.out = r.out.next return buf } func (r *Renderer) inParagraph() bool { return r.inpara } /* TODO: commented because unused func (r *Renderer) inList() bool { return r.list != nil } */ func (r *Renderer) renderParagraphKeepBreaks(buf *bytes.Buffer) { scanner := bufio.NewScanner(buf) for scanner.Scan() { r.printf(strings.Repeat(" ", r.depth)+"%s\n", scanner.Text()) } } func (r *Renderer) renderParagraphBreakLines(buf *bytes.Buffer, maxlen int) { maxlen -= r.depth scanner := bufio.NewScanner(buf) scanner.Split(bufio.ScanWords) line := []string{} length := 0 for scanner.Scan() { word := scanner.Text() wordLength := len(stripColors([]byte(word))) // Print the line if we've got a collection of words over 80 characters, or if // we have a single word that is over 80 characters on an otherwise empty line. switch { case length+wordLength > maxlen: r.printf(strings.Repeat(" ", r.depth)+"%s\n", strings.Join(line, " ")) line = []string{word} length = wordLength case length == 0 && wordLength > maxlen: r.printf(strings.Repeat(" ", r.depth)+"%s\n", word) default: line = append(line, word) length += wordLength + 1 // Plus one for space } } if len(line) > 0 { r.printf(strings.Repeat(" ", r.depth)+"%s\n", strings.Join(line, " ")) } } func (r *Renderer) renderParagraph(buf *bytes.Buffer) { switch r.out.mode { case RenderModeKeepBreaks: r.renderParagraphKeepBreaks(buf) case RenderModeBreakLines: r.renderParagraphBreakLines(buf, maxLineLength) } } // RenderNode implements blackfriday.Renderer interface. func (r *Renderer) RenderNode(w io.Writer, node *md.Node, entering bool) md.WalkStatus { if r.out == nil { r.out = &bufqueue{w, nil, nil, RenderModeBreakLines} } switch node.Type { case md.Paragraph: // Alternative idea here: call r.RenderNode() with our new buffer as // `w`. In the `else` condition here render to the outter buffer and // always return md.Terminate. So when we enter a paragraph we start // parsing with a new output buffer and capture the output. if entering { if r.inParagraph() { panic("already in paragraph") } r.inpara = true //r.printf(out, "[paragraph:") r.capture(r.out.mode) } else { r.renderParagraph(r.finishCapture()) // Write a newline unless the parent node is a definition list term. if node.Parent.Type != md.Item || node.Parent.ListFlags&md.ListTypeTerm == 0 { r.printf("\n") } r.inpara = false //r.printf(w, ":paragraph]") } case md.Text: // TODO: is this necessary? I think all text is in a paragraph. if r.inParagraph() { r.write(node.Literal) } else { s := strings.ReplaceAll(string(node.Literal), "\n", "\n"+strings.Repeat(" ", r.depth)) r.printf(s) } case md.Heading: if entering { r.printf(ansi.ColorCode("default+bh")) } else { r.printf(ansi.Reset) r.printf("\n") } case md.Link: if entering { r.printf(ansi.ColorCode("default+b")) //r.printf("\033[2m") // Dim } else { r.printf(ansi.Reset) } case md.Strong: if entering { r.printf(ansi.ColorCode("default+bh")) } else { r.printf(ansi.Reset) } case md.Emph: if entering { r.printf(ansi.ColorCode("default+u")) } else { r.printf(ansi.Reset) } case md.Code: r.printf(ansi.ColorCode("default+u")) r.write(node.Literal) r.printf(ansi.Reset) case md.List: if entering { r.listdepth++ r.list = &list{[]item{}, node.ListFlags, r.list} //r.printf("[list (type %s:", node.ListData.ListFlags) } else { if r.listdepth > 1 && r.list.isDefinition() { w := new(tabwriter.Writer) w.Init(r.out.w, 0, 8, 4, ' ', tabwriter.StripEscape) for _, item := range r.list.items { fmt.Fprint(w, strings.TrimRight(string(item.term), " \n")) fmt.Fprint(w, "\n") for _, def := range item.definitions { fmt.Fprint(w, strings.TrimRight(string(def), " \n")) } fmt.Fprintf(w, "\n\n") } w.Flush() } else { ordered := (node.ListFlags&md.ListTypeOrdered != 0) unordered := (node.ListFlags&md.ListTypeOrdered == 0 && node.ListFlags&md.ListTypeDefinition == 0) for i, item := range r.list.items { if ordered || unordered { p := bytes.IndexFunc(item.term, func(r rune) bool { return !unicode.IsSpace(r) }) switch { case ordered: // add numbers on ordered lists item.term = append(item.term[:p], append([]byte(fmt.Sprintf("%d. ", i+1)), item.term[p:]...)...) case unordered: // add bullet points on unordered lists item.term = append(item.term[:p], append([]byte("• "), item.term[p:]...)...) } } r.write(item.term) for _, def := range item.definitions { r.write(def) } } } r.listdepth-- r.list = r.list.parent //r.printf(":list]") } case md.Item: incdepth := 4 //ltype := "normal" if node.ListFlags&md.ListTypeTerm != 0 { // Nested definition list terms get indented two spaces. Non-nested // definition list terms are not indented. if r.listdepth > 1 { incdepth = 2 } else { incdepth = 0 } //ltype = "dt" } else if node.ListFlags&md.ListTypeDefinition != 0 { incdepth = 4 //ltype = "dd" } if entering { //fmt.Fprintf(out, "[list item %s:", ltype) r.depth += incdepth if r.listdepth > 1 && r.list.isDefinition() { r.capture(RenderModeKeepBreaks) } else { r.capture(RenderModeBreakLines) } if !r.list.isDefinition() || node.ListFlags&md.ListTypeTerm != 0 { r.list.items = append(r.list.items, item{node.ListFlags, nil, nil}) } } else { //fmt.Fprintf(out, ":list item]") r.depth -= incdepth buf := r.finishCapture() if r.list.isDefinition() && node.ListFlags&md.ListTypeTerm == 0 { i := len(r.list.items) - 1 r.list.items[i].definitions = append(r.list.items[i].definitions, buf.Bytes()) } else { r.list.items[len(r.list.items)-1].term = buf.Bytes() } } case md.Table: if entering { r.capture(RenderModeKeepBreaks) w := new(tabwriter.Writer) w.Init(r.out.w, 1, 8, 2, ' ', tabwriter.StripEscape) r.out.w = w } else { r.out.w.(*tabwriter.Writer).Flush() buf := r.finishCapture() r.renderParagraphKeepBreaks(buf) r.printf("\n") } case md.TableBody: // Do nothing. case md.TableHead: if entering { r.capture(r.out.mode) } else { // Markdown doens't have a way to create a table without headers. // We've opted to fix that here by not rendering headers at all if // they're empty. result := r.finishCapture().Bytes() if strings.TrimSpace(string(stripColors(result))) != "" { parts := strings.Split(strings.TrimRight(string(result), "\t\n"), "\t") for i := 0; i < len(parts); i++ { parts[i] = "\xff" + ansi.ColorCode("default+bh") + "\xff" + parts[i] + "\xff" + ansi.Reset + "\xff" } r.printf(strings.Join(parts, "\t") + "\t\n") } } case md.TableRow: if entering { r.capture(r.out.mode) } else { // Escape any colors in the row before writing to the // tabwriter, otherwise they screw up the width calculations. The // escape character for tabwriter is \xff. result := r.finishCapture().Bytes() result = colorEscapeRe.ReplaceAll(result, []byte("\xff$0\xff")) r.write(result) r.printf("\n") } case md.TableCell: if !entering { r.printf("\t") } case md.CodeBlock: r.depth += 4 r.renderParagraphKeepBreaks(bytes.NewBuffer(node.Literal)) r.printf("\n") r.depth -= 4 case md.Document: default: r.printf("unknown block %s:", node.Type) r.write(node.Literal) } //w.Write([]byte(fmt.Sprintf("node<%s; %t>", node.Type, entering))) //w.Write(node.Literal) return md.GoToNext } // RenderHeader implements blackfriday.Renderer interface. func (r *Renderer) RenderHeader(w io.Writer, ast *md.Node) {} // RenderFooter implements blackfriday.Renderer interface. func (r *Renderer) RenderFooter(w io.Writer, ast *md.Node) {} golang-step-cli-utils-0.7.5+ds/usage/report.go000066400000000000000000000060361433771265400212370ustar00rootroot00000000000000package usage import ( "bytes" "encoding/json" "errors" "fmt" "io" "regexp" "strings" "golang.org/x/net/html" ) // Section keeps track of individual sections type Section struct { Command string `json:"command"` Name string `json:"name"` Text string `json:"text"` Words int `json:"words"` Lines int `json:"lines"` Sections []*Section `json:"sections"` } // Report holds together a report of sections type Report struct { Report []*Section `json:"report"` } // NewReport returns report based on raw func NewReport(command string, top []byte) *Report { report := Report{} report.Process(command, top) return &report } // Write serializes the report to json func (report *Report) Write(w io.Writer) error { j, err := json.MarshalIndent(report, "", " ") if err != nil { return err } w.Write(j) return nil } // Process adds a html based help page to the report func (report *Report) Process(command string, raw []byte) error { r := bytes.NewBuffer(raw) doc, err := html.Parse(r) if err != nil { return err } if doc.FirstChild.Type != html.ElementNode || doc.FirstChild.Data != "html" || doc.FirstChild.FirstChild.NextSibling.Data != "body" { return errors.New("error parsing raw html") } body := doc.FirstChild.FirstChild.NextSibling report.addSection(command, body.FirstChild, nil) return nil } func (report *Report) addSection(command string, node *html.Node, section *Section) (*html.Node, *Section) { if node == nil || node.Type != html.ElementNode || node.Data != "h2" { return nil, nil } text, next := report.processNode(node) words := strings.Fields(text) lines := strings.Split(text, "\n") s := Section{ Command: command, Name: node.FirstChild.Data, Text: text, Words: len(words), Lines: len(lines), } if section == nil { report.Report = append(report.Report, &s) return report.addSection(command, next, &s) } section.Sections = append(section.Sections, &s) return report.addSection(command, next, section) } func (report *Report) processNode(node *html.Node) (string, *html.Node) { text := "" current := node.NextSibling r := regexp.MustCompile(`<[^>]*>`) for current != nil { var buf bytes.Buffer w := io.Writer(&buf) html.Render(w, current) notags := r.ReplaceAllString(buf.String(), "") clean := strings.TrimSpace(notags) if len(text) > 0 && len(clean) > 0 { text = fmt.Sprintf("%s %s", text, clean) } else if len(clean) > 0 { text = clean } current = current.NextSibling if current == nil { return text, nil } else if current.Type == html.ElementNode && current.Data == "h2" { node = current current = nil } } return text, node } // PerHeadline returns all sections across commands/pages with the same headline func (report *Report) PerHeadline(headline string) []Section { var results []Section for _, top := range report.Report { for _, section := range top.Sections { if section.Name != headline { continue } results = append(results, *section) } } return results } golang-step-cli-utils-0.7.5+ds/usage/usage.go000066400000000000000000000123231433771265400210240ustar00rootroot00000000000000package usage import ( "bytes" "fmt" "html" "strconv" "strings" "text/template" ) var usageTextTempl = " {{.Name}}\n {{.Usage}} {{if .Required}}(Required){{else}}(Optional){{end}}{{if .Multiple}} (Multiple can be specified){{end}}\n" var templ *template.Template func init() { templ = template.Must(template.New("usageText").Parse(usageTextTempl)) } // Argument specifies the Name, Usage, and whether or not an Argument is // required or not type Argument struct { Required bool Multiple bool Name string Usage string } // Decorate returns the name of an Argument and decorates it with notation to // indicate whether its required or not func (a Argument) Decorate() string { name := a.Name if a.Multiple { name += "(s)..." } if a.Required { return fmt.Sprintf("<%s>", name) } return fmt.Sprintf("[%s]", name) } // Arguments is an array of Argument structs that specify which arguments are // accepted by a Command type Arguments []Argument // UsageText returns the value of the UsageText property for a cli.Command for // these arguments func (args Arguments) UsageText() string { var buf bytes.Buffer for _, a := range args { data := map[string]interface{}{ "Name": a.Decorate(), "Multiple": a.Multiple, "Required": a.Required, "Usage": a.Usage, } err := templ.Execute(&buf, data) if err != nil { panic(fmt.Sprintf("Could not generate args template for %s: %s", a.Name, err)) } } return "\n\n" + buf.String() } // ArgsUsage returns the value of the ArgsUsage property for a cli.Command for // these arguments func (args Arguments) ArgsUsage() string { out := "" for i, a := range args { out += a.Decorate() if i < len(args)-1 { out += " " } } return out } // AppHelpTemplate contains the modified template for the main app var AppHelpTemplate = `## NAME **{{.HelpName}}** -- {{.Usage}} ## USAGE {{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}**{{if .Commands}} {{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}_[arguments]_{{end}}{{end}}{{if .Description}} ## DESCRIPTION {{.Description}}{{end}}{{if .VisibleCommands}} ## COMMANDS {{range .VisibleCategories}}{{if .Name}}{{.Name}}:{{end}} ||| |---|---|{{range .VisibleCommands}} | **{{join .Names ", "}}** | {{.Usage}} |{{end}} {{end}}{{if .VisibleFlags}}{{end}} ## OPTIONS {{range $index, $option := .VisibleFlags}}{{if $index}} {{end}}{{$option}} {{end}}{{end}}{{if .Copyright}}{{if len .Authors}} ## AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{range $index, $author := .Authors}}{{if $index}} {{end}}{{$author}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} ## ONLINE This documentation is available online at https://smallstep.com/docs/cli ## VERSION {{.Version}}{{end}}{{end}} ## COPYRIGHT {{.Copyright}} ## FEEDBACK ` + html.UnescapeString("&#"+strconv.Itoa(128525)+";") + " " + html.UnescapeString("&#"+strconv.Itoa(127867)+";") + ` The **step** utility is not instrumented for usage statistics. It does not phone home. But your feedback is extremely valuable. Any information you can provide regarding how you’re using **step** helps. Please send us a sentence or two, good or bad: **feedback@smallstep.com** or ask in [GitHub Discussions](https://github.com/smallstep/certificates/discussions). {{end}} ` // SubcommandHelpTemplate contains the modified template for a sub command // Note that the weird "|||\n|---|---|" syntax sets up a markdown table with empty headers. var SubcommandHelpTemplate = `## NAME **{{.HelpName}}** -- {{.Usage}} ## USAGE {{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}** {{if .VisibleFlags}} _[options]_{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}_[arguments]_{{end}}{{end}}{{if .Description}} ## DESCRIPTION {{.Description}}{{end}} ## COMMANDS {{range .VisibleCategories}}{{if .Name}}{{.Name}}:{{end}} ||| |---|---|{{range .VisibleCommands}} | **{{join .Names ", "}}** | {{.Usage}} |{{end}} {{end}}{{if .VisibleFlags}} ## OPTIONS {{range .VisibleFlags}} {{.}} {{end}}{{end}} ` // CommandHelpTemplate contains the modified template for a command var CommandHelpTemplate = `## NAME **{{.HelpName}}** -- {{.Usage}} ## USAGE {{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}**{{if .VisibleFlags}} _[options]_{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}_[arguments]_{{end}}{{end}}{{if .Category}} ## CATEGORY {{.Category}}{{end}}{{if .Description}} ## DESCRIPTION {{.Description}}{{end}}{{if .VisibleFlags}} ## OPTIONS {{range .VisibleFlags}} {{.}} {{end}}{{end}} ` // FlagNamePrefixer converts a full flag name and its placeholder into the help // message flag prefix. This is used by the default FlagStringer. // // This method clones urflave/cli functionality but adds a new line at the end. func FlagNamePrefixer(fullName, placeholder string) string { var prefixed string parts := strings.Split(fullName, ",") for i, name := range parts { name = strings.Trim(name, " ") prefixed += "**" + prefixFor(name) + name + "**" if placeholder != "" { prefixed += "=" + placeholder } if i < len(parts)-1 { prefixed += ", " } } //return "* " + prefixed + "\n" return prefixed + "\n: " } func prefixFor(name string) (prefix string) { if len(name) == 1 { prefix = "-" } else { prefix = "--" } return }