Skip to content

Commit

Permalink
backport auth
Browse files Browse the repository at this point in the history
  • Loading branch information
rusq committed Jan 13, 2024
1 parent 1e9c246 commit f3a4780
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 77 deletions.
8 changes: 6 additions & 2 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"io"
"net/http"
"os"
"runtime/trace"
"strings"

Expand Down Expand Up @@ -37,8 +38,6 @@ type Provider interface {
SlackToken() string
// Cookies should return a set of Slack Session cookies.
Cookies() []*http.Cookie
// Type returns the auth type.
Type() Type
// Validate should return error, in case the token or cookies cannot be
// retrieved.
Validate() error
Expand Down Expand Up @@ -140,3 +139,8 @@ func (s simpleProvider) Test(ctx context.Context) error {
func (s simpleProvider) HTTPClient() (*http.Client, error) {
return chttp.New(SlackURL, s.Cookies())
}

func IsDocker() bool {
_, err := os.Stat("/.dockerenv")
return err == nil
}
14 changes: 11 additions & 3 deletions auth/auth_ui/auth_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@ import (
"strings"
)

// LoginType is the login type, that is used to choose the authentication flow,
// for example login headlessly or interactively.
type LoginType int8

const (
LoginEmail = 0
LoginSSO = 1
LoginCancel = 2
// LInteractive is the SSO login type (Google, Apple, etc).
LInteractive LoginType = iota
// LHeadless is the email/password login type.
LHeadless
// LCancel should be returned if the user cancels the login intent.
LCancel
)

// Sanitize takes a workspace name or URL and returns the workspace name.
func Sanitize(workspace string) (string, error) {
if !strings.Contains(workspace, ".slack.com") {
return workspace, nil
Expand Down
16 changes: 8 additions & 8 deletions auth/auth_ui/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ func (cl *CLI) RequestPassword(w io.Writer, account string) (string, error) {
return prompt(w, fmt.Sprintf("Enter Password for %s (won't be visible): ", account), readpwd)
}

func (cl *CLI) RequestLoginType(w io.Writer) (int, error) {
func (cl *CLI) RequestLoginType(w io.Writer) (LoginType, error) {
var types = []struct {
name string
value int
value LoginType
}{
{"Email", LoginEmail},
{"Google", LoginSSO},
{"Apple", LoginSSO},
{"Login with Single-Sign-On (SSO)", LoginSSO},
{"Other/Manual", LoginSSO},
{"Cancel", LoginCancel},
{"Email", LHeadless},
{"Google", LInteractive},
{"Apple", LInteractive},
{"Login with Single-Sign-On (SSO)", LInteractive},
{"Other/Manual", LInteractive},
{"Cancel", LCancel},
}

var idx int = -1
Expand Down
67 changes: 16 additions & 51 deletions auth/auth_ui/huh.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/charmbracelet/huh"
)

// Huh is the Auth UI that uses the huh library to provide a terminal UI.
type Huh struct {
theme huh.Theme
}
Expand All @@ -35,28 +36,31 @@ func (*Huh) RequestCreds(w io.Writer, workspace string) (email string, passwd st
huh.NewGroup(
huh.NewInput().
Title("You Slack Login Email").Value(&email).
Placeholder("[email protected]").
Description(fmt.Sprintf("This is the email that you log into %s with.", workspace)).
Validate(valAND(valEmail, valRequired)),
huh.NewInput().
Title("Password").Value(&passwd).
Placeholder("your slack password").
Validate(valRequired).Password(true),
),
)
err = f.Run()
return
}

func (*Huh) RequestLoginType(w io.Writer) (int, error) {
var loginType int
err := huh.NewSelect[int]().Title("Select login type").
func (*Huh) RequestLoginType(w io.Writer) (LoginType, error) {
var loginType LoginType
err := huh.NewSelect[LoginType]().Title("Select login type").
Options(
huh.NewOption("Email", LoginEmail),
huh.NewOption("Google", LoginSSO),
huh.NewOption("Apple", LoginSSO),
huh.NewOption("Login with Single-Sign-On (SSO)", LoginSSO),
huh.NewOption("Other/Manual", LoginSSO),
huh.NewOption("------", -1),
huh.NewOption("Cancel", LoginCancel),
huh.NewOption("Email (manual)", LInteractive),
huh.NewOption("Email (automatic, experimental)", LHeadless),
huh.NewOption("Google", LInteractive),
huh.NewOption("Apple", LInteractive),
huh.NewOption("Login with Single-Sign-On (SSO)", LInteractive),
huh.NewOption("Other/Manual", LInteractive),
huh.NewOption("------", LoginType(-1)),
huh.NewOption("Cancel", LCancel),
).
Value(&loginType).
Validate(valSepEaster()).
Expand All @@ -65,46 +69,8 @@ func (*Huh) RequestLoginType(w io.Writer) (int, error) {
return loginType, err
}

func valSepEaster() func(v int) error {
var phrases = []string{
"This is a separator, it does nothing",
"Seriously, it does nothing",
"Stop clicking on it",
"Stop it",
"Stop",
"Why are you so persistent?",
"Fine, you win",
"Here's a cookie: 🍪",
"🍪",
"🍪",
"Don't be greedy, you already had three.",
"Ok, here's another one: 🍪",
"Nothing will happen if you click on it again",
"",
"",
"",
"You must have a lot of time on your hands",
"Or maybe you're just bored",
"Or maybe you're just procrastinating",
"Or maybe you're just trying to get a cookie",
"These are virtual cookies, you can't eat them, but here's another one: 🍪",
"🍪",
"You have reached the end of this joke, it will now repeat",
"Seriously...",
"Ah, shit, here we go again",
}
var i int
return func(v int) error {
if v == -1 {
// separator selected
msg := phrases[i]
i = (i + 1) % len(phrases)
return errors.New(msg)
}
return nil
}
}

// ConfirmationCode asks the user to input the confirmation code, does some
// validation on it and returns it as an int.
func (*Huh) ConfirmationCode(email string) (int, error) {
var strCode string
q := huh.NewInput().
Expand All @@ -125,7 +91,6 @@ func (*Huh) ConfirmationCode(email string) (int, error) {

var numChlgRE = regexp.MustCompile(`^\d{6}$`)

// valSixDigits is a validation function for confirmation code input.
func valSixDigits(s string) error {
if numChlgRE.MatchString(s) {
return nil
Expand Down
41 changes: 41 additions & 0 deletions auth/auth_ui/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,44 @@ func valEmail(s string) error {
}
return nil
}

// valSepEaster is probably the most useless validation function ever.
func valSepEaster() func(v LoginType) error {
var phrases = []string{
"This is a separator, it does nothing",
"Seriously, it does nothing",
"Stop clicking on it",
"Stop it",
"Stop",
"Why are you so persistent?",
"Fine, you win",
"Here's a cookie: 🍪",
"🍪",
"🍪",
"Don't be greedy, you already had three.",
"Ok, here's another one: 🍪",
"Nothing will happen if you click on it again",
"",
"",
"",
"You must have a lot of time on your hands",
"Or maybe you're just bored",
"Or maybe you're just procrastinating",
"Or maybe you're just trying to get a cookie",
"These are virtual cookies, you can't eat them, but here's another one: 🍪",
"🍪",
"You have reached the end of this joke, it will now repeat",
"Seriously...",
"Ah, shit, here we go again",
}
var i int
return func(v LoginType) error {
if v == -1 {
// separator selected
msg := phrases[i]
i = (i + 1) % len(phrases)
return errors.New(msg)
}
return nil
}
}
7 changes: 1 addition & 6 deletions auth/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func NewBrowserAuth(ctx context.Context, opts ...Option) (BrowserAuth, error) {
for _, opt := range opts {
opt(&br.opts)
}
if isDocker() {
if IsDocker() {
return BrowserAuth{}, &Error{Err: ErrNotSupported, Msg: "browser auth is not supported in docker, use token/cookie auth instead"}
}

Expand Down Expand Up @@ -81,8 +81,3 @@ func NewBrowserAuth(ctx context.Context, opts ...Option) (BrowserAuth, error) {
func (BrowserAuth) Type() Type {
return TypeBrowser
}

func isDocker() bool {
_, err := os.Stat("/.dockerenv")
return err == nil
}
32 changes: 25 additions & 7 deletions auth/rod.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ import (
"github.com/rusq/slackdump/v2/auth/auth_ui"
)

// RodAuth is an authentication provider that uses a headless or interactive
// browser to authenticate with Slack, depending on the user's choice. It uses
// rod library to drive the browser via the CDP protocol.
//
// User can choose between:
// - Email/password login - will be done headlessly
// - SSO authentication - will open the browser and let the user do the thing.
// - Cancel - will cancel the login flow.
//
// Headless login is a bit fragile. If it fails, user should be advised to
// login interactively by choosing SSO auth type.
type RodAuth struct {
simpleProvider
opts options
Expand All @@ -25,11 +36,19 @@ type rodOpts struct {

type browserAuthUIExt interface {
BrowserAuthUI
RequestLoginType(w io.Writer) (int, error)
// RequestLoginType should request the login type from the user and return
// one of the [auth_ui.LoginType] constants. The implementation should
// provide a way to cancel the login flow, returning [auth_ui.LoginCancel].
RequestLoginType(w io.Writer) (auth_ui.LoginType, error)
// RequestCreds should request the user's email and password and return
// them.
RequestCreds(w io.Writer, workspace string) (email string, passwd string, err error)
// ConfirmationCode should request the confirmation code from the user and
// return it.
ConfirmationCode(email string) (code int, err error)
}

// NewRODAuth constructs new RodAuth provider.
func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) {
r := RodAuth{
opts: options{
Expand Down Expand Up @@ -64,22 +83,23 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) {

var sp simpleProvider
switch resp {
case auth_ui.LoginSSO:
case auth_ui.LInteractive:
var err error
sp.Token, sp.Cookie, err = slackauth.Browser(ctx, r.opts.workspace)
if err != nil {
return r, err
}
case auth_ui.LoginEmail:
case auth_ui.LHeadless:
sp, err = headlessFlow(ctx, r.opts.workspace, r.opts.ui)
if err != nil {
return r, err
}
fmt.Fprintln(os.Stderr, "authenticated.")
case auth_ui.LoginCancel:
case auth_ui.LCancel:
return r, ErrCancelled
}

fmt.Fprintln(os.Stderr, "authenticated.")

return RodAuth{
simpleProvider: sp,
}, nil
Expand Down Expand Up @@ -109,7 +129,5 @@ func headlessFlow(ctx context.Context, workspace string, ui browserAuthUIExt) (s
if loginErr != nil {
return sp, loginErr
}

fmt.Fprintln(os.Stderr, "authenticated.")
return
}

0 comments on commit f3a4780

Please sign in to comment.