Skip to content
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions bridge/gitlab/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ var (
ErrBadProjectURL = errors.New("bad project url")
)

var (
// Tokens issued before GitLab's routable tokens existed: an optional
// "glpat-" prefix followed by exactly 20 base62 characters.
legacyTokenRe = regexp.MustCompile(`^(glpat-)?[a-zA-Z0-9\-\_]{20}$`)
// Routable tokens, as generated by GitLab since 16.10:
// "glpat-" followed by a base64url payload (27 to 300 characters), a ".",
// a two character token version, another ".", then the payload length and
// a crc32, both encoded in base36 on two and seven characters.
// See https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/routable_tokens
routableTokenRe = regexp.MustCompile(`^glpat-[0-9a-zA-Z_-]{27,300}\.[0-9a-z]{2}\.[0-9a-z]{2}[0-9a-z]{7}$`)
)

// validTokenFormat tells whether a token looks like a personal access token
// that GitLab could have issued. The actual validity is checked by the API
// call in promptToken, this is only there to catch typos early.
func validTokenFormat(token string) bool {
return legacyTokenRe.MatchString(token) || routableTokenRe.MatchString(token)
}

func (g *Gitlab) ValidParams() map[string]interface{} {
return map[string]interface{}{
"URL": nil,
Expand Down Expand Up @@ -200,12 +219,10 @@ func promptToken(baseUrl string) (*auth.Token, error) {
fmt.Println("'api' access scope: to be able to make api calls")
fmt.Println()

re := regexp.MustCompile(`^(glpat-)?[a-zA-Z0-9\-\_]{20}$`)

var login string

validator := func(name string, value string) (complaint string, err error) {
if !re.MatchString(value) {
if !validTokenFormat(value) {
return "token has incorrect format", nil
}
login, err = getLoginFromToken(baseUrl, auth.NewToken(target, value))
Expand Down
97 changes: 97 additions & 0 deletions bridge/gitlab/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gitlab

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -88,3 +89,99 @@ func TestProjectPath(t *testing.T) {
})
}
}

func TestValidTokenFormat(t *testing.T) {
tests := []struct {
name string
token string
want bool
}{
{
name: "legacy token without prefix",
token: "token-string-here123",
want: true,
},
{
name: "legacy token with prefix",
token: "glpat-token-string-here123",
want: true,
},
{
// https://github.com/trufflesecurity/trufflehog/issues/4551, as
// issued by both gitlab.com and a self-managed instance
name: "routable token",
token: "glpat-rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01.6z70tqjnm",
want: true,
},
{
// a payload close to the 300 characters maximum
name: "routable token with a long payload",
token: "glpat-" + strings.Repeat("a", 300) + ".01.6z70tqjnm",
want: true,
},
{
// the token from the issue report
name: "routable token from the issue report",
token: "glpat-bE8-c60h8AD4A0v-59rAr246MQj8ObF1DA.01.0y0nl1q64",
want: true,
},
{
name: "empty token",
token: "",
want: false,
},
{
name: "too short",
token: "glpat-tooshort",
want: false,
},
{
name: "legacy length with a dot",
token: "glpat-token-string-here1.xxxxxxxxxxxxxxx",
want: false,
},
{
// the payload is 26 characters, one short of the 27 minimum
name: "routable payload too short",
token: "glpat-" + strings.Repeat("a", 26) + ".01.6z70tqjnm",
want: false,
},
{
// the payload is one character over the 300 maximum
name: "routable payload too long",
token: "glpat-" + strings.Repeat("a", 301) + ".01.6z70tqjnm",
want: false,
},
{
name: "routable token without the checksum",
token: "glpat-rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01",
want: false,
},
{
name: "routable token with a bad checksum length",
token: "glpat-rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01.6z70tqj",
want: false,
},
{
name: "routable token with a non base36 checksum",
token: "glpat-rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01.6z70tqj-m",
want: false,
},
{
name: "routable token without a prefix",
token: "rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01.6z70tqjnm",
want: false,
},
{
name: "not a token at all",
token: "hunter2",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, validTokenFormat(tt.token))
})
}
}
27 changes: 26 additions & 1 deletion commands/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"log/slog"
"strings"

"github.com/spf13/cobra"

Expand All @@ -16,7 +17,9 @@ var (
)

func newVersionCommand(env *execenv.Env) *cobra.Command {
return &cobra.Command{
var raw bool

cmd := &cobra.Command{
Use: "version",
Short: "Print version information",
Example: "git bug version",
Expand All @@ -41,12 +44,34 @@ Format Description:
<platform> is the target platform (GOOS).

<arch> is the target architecture (GOARCH).

With --raw, print the version string alone, on a line of its own, with no
"git-bug" prefix and no build metadata. That is the form to compare against
another version or to embed in a script.
`,
Run: func(cmd *cobra.Command, args []string) {
defer warnDeprecated()
if raw {
// <version> is always the first space-separated field of the
// full string, so the raw form stays a prefix of the default
// output rather than a second source of truth.
env.Out.Printf("%s\n", rawVersion(cmd.Root().Version))
return
}
env.Out.Printf("%s %s", execenv.RootCommandName, cmd.Root().Version)
},
}

cmd.Flags().BoolVar(&raw, "raw", false, "Print only the version string, with no prefix or build metadata")

return cmd
}

// rawVersion returns <version> on its own, extracted from the same string
// `git-bug version` prints, so the two can never disagree.
func rawVersion(full string) string {
version, _, _ := strings.Cut(full, " ")
return version
}

// warnDeprecated warns about deprecated build variables
Expand Down
86 changes: 86 additions & 0 deletions commands/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package commands

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/git-bug/git-bug/commands/execenv"
)

func TestRawVersion(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "version alone",
in: "v1.2.3",
want: "v1.2.3",
},
{
name: "version and commit",
in: "v1.2.3 8a3f1b2c9d0e",
want: "v1.2.3",
},
{
name: "dirty build",
in: "v1.2.3 8a3f1b2c9d0e/dirty",
want: "v1.2.3",
},
{
name: "no build info",
in: "undefined (no build info)\n",
want: "undefined",
},
{
name: "full documented shape",
in: "v1.2.3 8a3f1b2c9d0e go1.21.0 linux amd64",
want: "v1.2.3",
},
{
name: "empty",
in: "",
want: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, rawVersion(tt.in))
})
}
}

const testFullVersion = "v1.2.3 8a3f1b2c9d0e go1.21.0 linux amd64"

// runVersion executes `version` with the given args against a command whose
// root carries testFullVersion, and returns what was printed.
func runVersion(t *testing.T, args ...string) string {
t.Helper()

env := execenv.NewTestEnv(t)
cmd := newVersionCommand(env)
cmd.Version = testFullVersion
cmd.SetArgs(args)
cmd.SetOut(env.Out.Raw())
cmd.SetErr(env.Out.Raw())
require.NoError(t, cmd.Execute())

return env.Out.Raw().(*bytes.Buffer).String()
}

// The whole point of --raw is that its output is embeddable: one line, no
// "git-bug" prefix, no build metadata.
func TestVersionCommandRaw(t *testing.T) {
assert.Equal(t, "v1.2.3\n", runVersion(t, "--raw"))
}

// Without --raw the output is unchanged: the flag must not alter the default
// form that scripts and users already parse.
func TestVersionCommandDefaultUnchanged(t *testing.T) {
assert.Equal(t, execenv.RootCommandName+" "+testFullVersion, runVersion(t))
}
9 changes: 9 additions & 0 deletions doc/man/git-bug-version.1
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,20 @@ Format Description:
.PP
is the target architecture (GOARCH).

.PP
With --raw, print the version string alone, on a line of its own, with no "git-bug"
prefix and no build metadata. That is the form to compare against another
version or to embed in a script.


.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for version

.PP
\fB--raw\fP[=false]
Print only the version string, with no prefix or build metadata


.SH EXAMPLE
.EX
Expand Down
5 changes: 5 additions & 0 deletions doc/md/git-bug_version.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ Format Description:

<arch> is the target architecture (GOARCH).

With --raw, print the version string alone, on a line of its own, with no "git-bug"
prefix and no build metadata. That is the form to compare against another
version or to embed in a script.


```
git-bug version [flags]
Expand All @@ -41,6 +45,7 @@ git bug version

```
-h, --help help for version
--raw Print only the version string, with no prefix or build metadata
```

### SEE ALSO
Expand Down