Skip to content

Commit

Permalink
Merge pull request #354 from rusq/v3-auth-logic
Browse files Browse the repository at this point in the history
Authentication
  • Loading branch information
rusq authored Nov 17, 2024
2 parents 718b58b + 792b1b3 commit 349b2c3
Show file tree
Hide file tree
Showing 35 changed files with 746 additions and 540 deletions.
32 changes: 0 additions & 32 deletions auth/auth_ui/auth_ui.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
package auth_ui

import (
"errors"
"fmt"
"net/url"
"strings"
)

// LoginType is the login type, that is used to choose the authentication flow,
// for example login headlessly or interactively.
type LoginType int8
Expand All @@ -21,28 +14,3 @@ const (
// LCancel should be returned if the user cancels the login intent.
LCancel
)

var ErrInvalidDomain = errors.New("invalid domain")

// Sanitize takes a workspace name or URL and returns the workspace name.
func Sanitize(workspace string) (string, error) {
if !strings.Contains(workspace, ".slack.com") && !strings.Contains(workspace, ".") {
return workspace, nil
}
if strings.HasPrefix(workspace, "https://") {
uri, err := url.Parse(workspace)
if err != nil {
return "", err
}
workspace = uri.Host
}
// parse
name, domain, found := strings.Cut(workspace, ".")
if !found {
return "", errors.New("workspace name is empty")
}
if strings.TrimRight(domain, "/") != "slack.com" {
return "", fmt.Errorf("%s: %w", domain, ErrInvalidDomain)
}
return name, nil
}
34 changes: 0 additions & 34 deletions auth/auth_ui/auth_ui_test.go

This file was deleted.

3 changes: 2 additions & 1 deletion auth/auth_ui/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"github.com/fatih/color"
"github.com/rusq/slackdump/v3/internal/structures"
"golang.org/x/term"
)

Expand All @@ -30,7 +31,7 @@ func (cl *CLI) RequestWorkspace(w io.Writer) (string, error) {
if err != nil {
return "", err
}
return Sanitize(workspace)
return structures.ExtractWorkspace(workspace)
}

func (*CLI) RequestCreds(w io.Writer, workspace string) (email string, passwd string, err error) {
Expand Down
7 changes: 4 additions & 3 deletions auth/auth_ui/huh.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/huh"
"github.com/rusq/slackauth"
"github.com/rusq/slackdump/v3/internal/structures"
)

// Huh is the Auth UI that uses the huh library to provide a terminal UI.
Expand All @@ -26,7 +27,7 @@ func (h *Huh) RequestWorkspace(w io.Writer) (string, error) {
Value(&workspace).
Validate(valWorkspace).
Description("The workspace name is the part of the URL that comes before `.slack.com' in\nhttps://<workspace>.slack.com/. Both workspace name or URL are acceptable."),
)).WithTheme(Theme).Run()
)).WithTheme(Theme).WithKeyMap(keymap).Run()
if err != nil {
return "", err
}
Expand Down Expand Up @@ -120,7 +121,7 @@ func (*Huh) RequestLoginType(ctx context.Context, w io.Writer, workspace string)

fields = append(fields, huh.NewSelect[LoginType]().
TitleFunc(func() string {
wsp, err := Sanitize(ret.Workspace)
wsp, err := structures.ExtractWorkspace(ret.Workspace)
if err != nil {
return "Select login type"
}
Expand Down Expand Up @@ -150,7 +151,7 @@ func (*Huh) RequestLoginType(ctx context.Context, w io.Writer, workspace string)
return ret, err
}
var err error
ret.Workspace, err = Sanitize(ret.Workspace)
ret.Workspace, err = structures.ExtractWorkspace(ret.Workspace)
if err != nil {
return ret, err
}
Expand Down
4 changes: 3 additions & 1 deletion auth/auth_ui/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package auth_ui
import (
"errors"
"regexp"

"github.com/rusq/slackdump/v3/internal/structures"
)

var (
Expand Down Expand Up @@ -105,6 +107,6 @@ func valWorkspace(s string) error {
if err := valRequired(s); err != nil {
return err
}
_, err := Sanitize(s)
_, err := structures.ExtractWorkspace(s)
return err
}
7 changes: 6 additions & 1 deletion auth/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package auth
import (
"context"
"io"
"log/slog"
"os"
"time"

"github.com/rusq/slackdump/v3/auth/auth_ui"
"github.com/rusq/slackdump/v3/auth/browser"
"github.com/rusq/slackdump/v3/internal/structures"
)

var _ Provider = BrowserAuth{}
Expand Down Expand Up @@ -58,11 +60,14 @@ func NewBrowserAuth(ctx context.Context, opts ...Option) (BrowserAuth, error) {
}
defer br.opts.flow.Stop()
}
if wsp, err := auth_ui.Sanitize(br.opts.workspace); err != nil {
if wsp, err := structures.ExtractWorkspace(br.opts.workspace); err != nil {
return br, err
} else {
br.opts.workspace = wsp
}
slog.Info("Please wait while Playwright is initialising.")
slog.Info("If you're running it for the first time, it will take a couple of minutes...")

auther, err := browser.New(br.opts.workspace, browser.OptBrowser(br.opts.browser), browser.OptTimeout(br.opts.loginTimeout), browser.OptVerbose(br.opts.verbose))
if err != nil {
return br, err
Expand Down
55 changes: 55 additions & 0 deletions auth/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package auth

import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/joho/godotenv"
"github.com/rusq/slackdump/v3/internal/structures"
)

func parseDotEnv(fsys fs.FS, filename string) (string, string, error) {
const (
tokenKey = "SLACK_TOKEN"
cookieKey = "SLACK_COOKIE"

clientTokenPrefix = "xoxc-"
)
f, err := fsys.Open(filename)
if err != nil {
return "", "", err
}
defer f.Close()
secrets, err := godotenv.Parse(f)
if err != nil {
return "", "", errors.New("not a secrets file")
}
token, ok := secrets[tokenKey]
if !ok {
return "", "", errors.New("no SLACK_TOKEN found in the file")
}
if err := structures.ValidateToken(token); err != nil {
return "", "", err
}
if !strings.HasPrefix(token, clientTokenPrefix) {
return token, "", nil
}
cook, ok := secrets[cookieKey]
if !ok {
return "", "", errors.New("no SLACK_COOKIE found in the file")
}
if !strings.HasPrefix(cook, "xoxd-") {
return "", "", errors.New("invalid cookie")
}
return token, cook, nil
}

func ParseDotEnv(filename string) (string, string, error) {
dir := filepath.Dir(filename)
dirfs := os.DirFS(dir)
pth := filepath.Base(filename)
return parseDotEnv(dirfs, pth)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package workspaceui
package auth

import (
"os"
Expand Down Expand Up @@ -26,7 +26,7 @@ func writeEnvFile(t *testing.T, filename string, m map[string]string) string {
return filename
}

func Test_parseSecretsTxt(t *testing.T) {
func Test_ParseDotEnv(t *testing.T) {
dir := t.TempDir()
type args struct {
filename string
Expand Down Expand Up @@ -121,16 +121,16 @@ func Test_parseSecretsTxt(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := parseSecretsTxt(tt.args.filename)
got, got1, err := ParseDotEnv(tt.args.filename)
if (err != nil) != tt.wantErr {
t.Errorf("parseSecretsTxt() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("ParseDotEnv() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseSecretsTxt() got = %v, want %v", got, tt.want)
t.Errorf("ParseDotEnv() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("parseSecretsTxt() got1 = %v, want %v", got1, tt.want1)
t.Errorf("ParseDotEnv() got1 = %v, want %v", got1, tt.want1)
}
})
}
Expand Down
3 changes: 2 additions & 1 deletion auth/rod.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/rusq/slackauth"

"github.com/rusq/slackdump/v3/auth/auth_ui"
"github.com/rusq/slackdump/v3/internal/structures"
"github.com/rusq/slackdump/v3/logger"
)

Expand Down Expand Up @@ -89,7 +90,7 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) {
for _, opt := range opts {
opt(&r.opts)
}
if wsp, err := auth_ui.Sanitize(r.opts.workspace); err != nil {
if wsp, err := structures.ExtractWorkspace(r.opts.workspace); err != nil {
return r, err
} else {
r.opts.workspace = wsp
Expand Down
8 changes: 5 additions & 3 deletions cmd/slackdump/internal/bootstrap/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ import (
"github.com/rusq/slackdump/v3/auth"
"github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg"
"github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace"
"github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace/workspaceui"
"github.com/rusq/slackdump/v3/internal/cache"
)

func CurrentOrNewProviderCtx(ctx context.Context) (context.Context, error) {
prov, err := workspace.AuthCurrent(ctx, cfg.CacheDir(), cfg.Workspace, cfg.LegacyBrowser)
cachedir := cfg.CacheDir()
prov, err := workspace.AuthCurrent(ctx, cachedir, cfg.Workspace, cfg.LegacyBrowser)
if err != nil {
if errors.Is(err, cache.ErrNoWorkspaces) {
// ask to create a new workspace
if err := workspace.CmdWspNew.Run(ctx, workspace.CmdWspNew, []string{}); err != nil {
if err := workspaceui.ShowUI(ctx, workspaceui.WithQuickLogin(), workspaceui.WithTitle("No workspaces, please choose a login method")); err != nil {
return ctx, fmt.Errorf("auth error: %w", err)
}
// one more time...
prov, err = workspace.AuthCurrent(ctx, cfg.CacheDir(), cfg.Workspace, cfg.LegacyBrowser)
prov, err = workspace.AuthCurrent(ctx, cachedir, cfg.Workspace, cfg.LegacyBrowser)
if err != nil {
return ctx, err
}
Expand Down
10 changes: 6 additions & 4 deletions cmd/slackdump/internal/cfg/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ var (

Log logger.Interface

// LoadSecrets is a flag that indicates whether to load secrets from the
// environment variables.
LoadSecrets bool

Version BuildInfo // version propagated by main package.
)

Expand Down Expand Up @@ -104,14 +108,14 @@ func SetBaseFlags(fs *flag.FlagSet, mask FlagMask) {

if mask&OmitAuthFlags == 0 {
fs.StringVar(&SlackToken, "token", osenv.Secret("SLACK_TOKEN", ""), "Slack `token`")
// COOKIE environment variable is deprecated and will be removed in v2.5.0, use SLACK_COOKIE instead.
fs.StringVar(&SlackCookie, "cookie", osenv.Secret("SLACK_COOKIE", osenv.Secret("COOKIE", "")), "d= cookie `value` or a path to a cookie.txt file\n(environment: SLACK_COOKIE)")
fs.StringVar(&SlackCookie, "cookie", osenv.Secret("SLACK_COOKIE", ""), "d= cookie `value` or a path to a cookie.txt file\n(environment: SLACK_COOKIE)")
fs.Var(&Browser, "browser", "browser to use for legacy EZ-Login 3000 (default: firefox)")
fs.DurationVar(&LoginTimeout, "browser-timeout", LoginTimeout, "Browser login `timeout`")
fs.DurationVar(&HeadlessTimeout, "autologin-timeout", HeadlessTimeout, "headless autologin `timeout`, without the browser starting time, just the interaction time")
fs.BoolVar(&LegacyBrowser, "legacy-browser", false, "use legacy browser automation (playwright) for EZ-Login 3000")
fs.BoolVar(&ForceEnterprise, "enterprise", false, "enable Enteprise module, you need to specify this option if you're using Slack Enterprise Grid")
fs.StringVar(&RODUserAgent, "user-agent", "", "override the user agent string for EZ-Login 3000")
fs.BoolVar(&LoadSecrets, "load-env", false, "load secrets from the .env, .env.txt or secrets.txt file")
}
if mask&OmitDownloadFlag == 0 {
fs.BoolVar(&DownloadFiles, "files", true, "enables file attachments (to disable, specify: -files=false)")
Expand Down Expand Up @@ -146,7 +150,5 @@ func SetBaseFlags(fs *flag.FlagSet, mask FlagMask) {
if mask&OmitTimeframeFlag == 0 {
fs.Var(&Oldest, "time-from", "timestamp of the oldest message to fetch (UTC timezone)")
fs.Var(&Latest, "time-to", "timestamp of the newest message to fetch (UTC timezone)")
fs.Var(&Oldest, "date-from", "alias for -time-from (DEPRECATED)")
fs.Var(&Latest, "date-to", "alias for -time-to (DEPRECATED)")
}
}
4 changes: 2 additions & 2 deletions cmd/slackdump/internal/ui/cfgui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.child = child
return m, cmd
}

CASE:
switch msg := msg.(type) {
case updaters.WMClose:
// child sends a close message
Expand Down Expand Up @@ -121,7 +121,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keymap.Select):
i, j := locateParam(m.cfgFn(), m.cursor)
if i == notFound || j == notFound {
return m, nil
break CASE
}
if params := m.cfgFn()[i].Params[j]; params.Updater != nil {
if params.Inline {
Expand Down
2 changes: 1 addition & 1 deletion cmd/slackdump/internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package ui

const (
// MenuSeparator is the separator to use in the wizard menus.
MenuSeparator = "────────────"
MenuSeparator = "────────────────"
)

type inputOptions struct {
Expand Down
24 changes: 24 additions & 0 deletions cmd/slackdump/internal/workspace/assets/import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Workspace Import Command

**Import** allows you to import credentials from a .env or secrets.txt file.

It requires the file to have the following format:
```
SLACK_TOKEN=xoxc-...
SLACK_COOKIE=xoxd-...
```

`SLACK_TOKEN` can be one of the following:

- xoxa-...: app token
- xoxb-...: bot token
- xoxc-...: client token
- xoxe-...: export token
- xoxp-...: legacy user token

`SLACK_COOKIE` is only required, if the `SLACK_TOKEN` is a client type token
(starts with `xoxc-`).

It will test the provided credentials, and if successful, encrypt and save
them to the to the slackdump credential storage. It is recommended to delete
the .env file afterwards.
Loading

0 comments on commit 349b2c3

Please sign in to comment.