Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add new subcommand auth in tkn-pac CLI #1736

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
38 changes: 36 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ require (
code.gitea.io/sdk/gitea v0.18.0
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0
github.com/cli/cli/v2 v2.46.0
github.com/cli/oauth v1.0.1
github.com/cloudevents/sdk-go/v2 v2.15.2
github.com/fvbommel/sortorder v1.1.0
github.com/gfleury/go-bitbucket-v1 v0.0.0-20240131155556-0b41d7863037
Expand All @@ -18,6 +20,7 @@ require (
github.com/google/go-github/scrape v0.0.0-20240403195118-24209f034709
github.com/google/go-github/v60 v60.0.0
github.com/google/go-github/v61 v61.0.0
github.com/h2non/gock v1.2.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/jonboulle/clockwork v0.4.0
github.com/juju/ansiterm v1.0.0
Expand All @@ -27,10 +30,11 @@ require (
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/mitchellh/mapstructure v1.5.0
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.0
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/tektoncd/pipeline v0.62.1
github.com/xanzy/go-gitlab v0.101.0
github.com/zalando/go-keyring v0.2.5
go.opencensus.io v0.24.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
Expand All @@ -48,7 +52,36 @@ require (
sigs.k8s.io/yaml v1.4.0
)

require github.com/coreos/go-oidc/v3 v3.9.0 // indirect
require (
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/briandowns/spinner v1.18.1 // indirect
github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c // indirect
github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/cli/go-gh/v2 v2.9.0 // indirect
github.com/cli/safeexec v1.0.1 // indirect
github.com/cli/shurcooL-graphql v0.0.4 // indirect
github.com/coreos/go-oidc/v3 v3.9.0 // indirect
github.com/danieljoos/wincred v1.2.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/henvic/httpretty v0.1.3 // indirect
github.com/itchyny/gojq v0.12.15 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
)

replace (
k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20230515203736-54b630e78af5
Expand Down Expand Up @@ -86,6 +119,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic v0.7.0 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/go-containerregistry v0.19.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
Expand Down
81 changes: 77 additions & 4 deletions go.sum

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions pkg/cmd/tknpac/auth/keyring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package auth

import (
"errors"
"strings"
"time"

"github.com/zalando/go-keyring"
)

var ErrNotFound = errors.New("secret not found in keyring")

type TimeoutError struct {
message string
}

func (e *TimeoutError) Error() string {
return e.message
}

// Set secret in keyring for user.
func SetCred(service, user, secret string) error {
ch := make(chan error, 1)
go func() {
defer close(ch)
ch <- keyring.Set(keyringServiceName(service), user, secret)
}()
select {
case err := <-ch:
return err
case <-time.After(3 * time.Second):
return &TimeoutError{"timeout while trying to set secret in keyring"}
}
}

// Get secret from keyring given service and user name.
func GetCred(service, user string) (string, error) {
ch := make(chan struct {
val string
err error
}, 1)
go func() {
defer close(ch)
val, err := keyring.Get(keyringServiceName(service), user)
ch <- struct {
val string
err error
}{val, err}
}()
select {
case res := <-ch:
if errors.Is(res.err, keyring.ErrNotFound) {
return "", ErrNotFound
}
return res.val, res.err
case <-time.After(3 * time.Second):
return "", &TimeoutError{"timeout while trying to get secret from keyring"}
}
}

// Delete secret from keyring.
func Delete(service, user string) error {
ch := make(chan error, 1)
go func() {
defer close(ch)
ch <- keyring.Delete(service, user)
}()
select {
case err := <-ch:
return err
case <-time.After(3 * time.Second):
return &TimeoutError{"timeout while trying to delete secret from keyring"}
}
}

func keyringServiceName(hostname string) string {
switch {
case strings.Contains(hostname, "github"):
return "gh:" + hostname
case strings.Contains(hostname, "gitlab"):
return "gl:" + hostname
case strings.Contains(hostname, "bitbucket"):
return "bb:" + hostname
}
return hostname
}
188 changes: 188 additions & 0 deletions pkg/cmd/tknpac/auth/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package auth

import (
"fmt"
"net/http"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
"github.com/spf13/cobra"
)

var (
provider string
authToken string
hostname string
authMode string
)

func loginCommand(_ *params.Run, ioStreams *cli.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "login",
Short: "login user with provider",
RunE: func(_ *cobra.Command, _ []string) error {
var username string
var err error
cs := ioStreams.ColorScheme()

if provider != "github" && provider != "gitlab" && provider != "bitbucket" {
return fmt.Errorf("provide is invalid must be amongst these three [github, gitlab, bitbucket]")
}

if provider != "github" {
return fmt.Errorf("feature is in under development, at the moment only github is supported")
}

hosts := []string{"Github.com", "Github Enterprise Server"}
authModes := []string{"Login with web browser", "Paste an authentication token"}

// if user hasn't specified `--hostname` flag
if hostname == "" {
err = askForHostname(hosts)
if err != nil {
return err
}
}

// if user hasn't specified token, it is needed to ask user for auth methods
if authToken == "" {
err = askForAuthMode(authModes)
if err != nil {
return err
}
} else {
// if user specifies `--token`, no need to ask for auth methods
authMode = authModes[1]
}

if strings.EqualFold(hostname, hosts[0]) {
hostname = defaultGithubHostname
} else {
hostname, err = askForEnterpriseHostName()
if err != nil {
return err
}
}

if authMode == authModes[0] {
authToken, err = RunAuthFlow(hostname, ioStreams, "", []string{}, true, true)
if err != nil {
return fmt.Errorf("failed to authenticate via web browser: %w", err)
}

username, err = GetViewer(hostname, authToken, ioStreams.ErrOut)
if err != nil {
return fmt.Errorf("failed to get user name from github: %w", err)
}

fmt.Fprintf(ioStreams.ErrOut, "%s Authentication complete for user %s.\n", cs.SuccessIcon(), cs.GreenBold(username))
} else {
minimumScopes := []string{"repo", "read:org"}
fmt.Fprintf(ioStreams.ErrOut, "Tip: you can generate a Personal Access Token here https://%s/settings/tokens, The minimum required scopes are %s.\n", hostname, scopesSentence(minimumScopes))

if authToken == "" {
err = askForAuthToken()
if err != nil {
return err
}
}

// checking github permission scopes for authToken
if err = shared.HasMinimumScopes(http.DefaultClient, hostname, authToken); err != nil {
return fmt.Errorf("error validating token: %w", err)
}
}

err = SetCred(hostname, username, authToken)
if err != nil {
return fmt.Errorf("error saving token in keyring: %w", err)
}
return nil
},
Annotations: map[string]string{
"commandType": "main",
},
}

cmd.PersistentFlags().StringVarP(&provider, "provider", "p", "github", "Git provider possible values [github, gitlab, bitbucket]")
cmd.PersistentFlags().StringVar(&hostname, "hostname", "", "The host name of git provider to authenticate user with")
cmd.PersistentFlags().StringVarP(&authToken, "token", "t", "", "Read token directly from standard input")
return cmd
}

func askForAuthToken() error {
err := survey.AskOne(&survey.Password{
Message: "Please enter you authentication token here:",
}, &authToken)
if err != nil {
return err
}

return nil
}

func askForHostname(hosts []string) error {
answers := struct {
HostName string `survey:"hostName"`
}{}
qs := []*survey.Question{
{
Name: "hostName",
Prompt: &survey.Select{
Message: "Which account do you want to log in to?",
Options: hosts,
Default: hosts[0],
},
},
}

err := survey.Ask(qs, &answers)
if err != nil {
return err
}
hostname = strings.ToLower(answers.HostName)

return nil
}

func askForAuthMode(authenticationMethods []string) error {
answers := struct {
LoginMethod string `survey:"loginMethod"`
}{}
qs := []*survey.Question{
{
Name: "loginMethod",
Prompt: &survey.Select{
Message: "How would you like to authenticate?",
Options: authenticationMethods,
Default: authenticationMethods[0],
},
},
}

err := survey.Ask(qs, &answers)
if err != nil {
return err
}
authMode = answers.LoginMethod

return nil
}

func askForEnterpriseHostName() (string, error) {
var hostName string
err := survey.Ask([]*survey.Question{{
Name: "enterpriseHostName",
Prompt: &survey.Input{Message: "Enter your GHE hostname:"},
Validate: survey.Required,
Transform: survey.Title,
}}, &hostName)
if err != nil {
return "", err
}

return hostName, nil
}
Loading