package grants
import (
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// nbsp is U+00A0 NON-BREAKING SPACE, built from its code point rather than
// pasted in as a character.
//
// It is the input asciiFields exists for: under strings.Fields a grant string
// holding nothing but this splits into zero members, and zero members means the
// universal set. Spelling it as a code point keeps the case that matters from
// depending on an invisible byte pair surviving an editor's whitespace
// normalisation, a copy-paste or a diff viewer — the very fragility the rule
// under test is about.
var nbsp = string(rune(0x00a0))
func TestParseReadsTheVocabularyOfTheSpec(t *testing.T) {
g, err := Parse("cov:upload bench:upload spec:propose")
require.NoError(t, err)
assert.False(t, g.All())
assert.True(t, g.Has("cov:upload"))
assert.True(t, g.Has("bench:upload"))
assert.True(t, g.Has("spec:propose"))
assert.False(t, g.Has("cov:read"), "a grant not named is not held")
assert.Equal(t, 0, g.TokenID(), "no id: member, so no row id")
}
// A blank grant string means every action, which is what the column of a parent
// minted before some service existed holds (SPEC ch. 3).
func TestBlankGrantsMeanEverything(t *testing.T) {
for _, s := range []string{"", " ", "\t\n"} {
g, err := Parse(s)
require.NoError(t, err, "parse %q", s)
assert.True(t, g.All(), "%q should be universal", s)
assert.True(t, g.Has("anything:at-all"))
}
}
func TestUniversalGrantAdmitsEverything(t *testing.T) {
g, err := Parse("*")
require.NoError(t, err)
assert.True(t, g.All())
assert.True(t, g.Has("bench:upload"))
assert.Equal(t, "*", g.String())
}
// The zero value must grant nothing. It is what a caller who forgot to parse
// ends up holding, and the failure has to be closed.
func TestZeroGrantsAdmitNothing(t *testing.T) {
var g Grants
assert.False(t, g.All())
assert.True(t, g.Empty())
assert.False(t, g.Has("bench:upload"))
assert.Equal(t, "", g.String())
}
// "* cov:upload" is the universal set with one member spelled out redundantly.
// Keeping the member would make String render something longer than what it
// means.
func TestUniversalAbsorbsNamedMembers(t *testing.T) {
g, err := Parse("cov:upload * bench:read")
require.NoError(t, err)
assert.True(t, g.All())
assert.Equal(t, "*", g.String())
}
func TestStringIsSortedAndDeduplicated(t *testing.T) {
g, err := Parse("spec:propose cov:upload bench:upload cov:upload")
require.NoError(t, err)
assert.Equal(t, "bench:upload cov:upload spec:propose", g.String(),
"one permission set must have one spelling: the string is both a column and a token payload")
}
// SPEC ch. 3 requires the format not to forbid the repository-scoped form a
// later version may want.
func TestActionsMayCarryFurtherSegments(t *testing.T) {
g, err := Parse("bench:upload:~bigbes/foo")
require.NoError(t, err)
assert.True(t, g.Has("bench:upload:~bigbes/foo"))
assert.False(t, g.Has("bench:upload"),
"a repository-scoped grant is not the unscoped one")
}
func TestMalformedGrantsAreRefused(t *testing.T) {
for name, s := range map[string]string{
"no colon": "coverupload",
"leading colon": ":upload",
"trailing colon": "cov:",
"empty segment": "cov::upload",
"upper case": "Cover:upload",
"non-ascii": "cov:upload" + nbsp,
"lone nbsp": nbsp,
"control byte": "cov:up\x01load",
"bare wildcard 2": "cov:upload **",
} {
t.Run(name, func(t *testing.T) {
_, err := Parse(s)
require.Error(t, err)
assert.True(t, errors.Is(err, ErrInvalid), "want ErrInvalid, got %v", err)
})
}
}
// The lone non-breaking space deserves its own assertion, not just membership in
// the table above: the bug it guards against was not "this errors" but "this
// silently parsed as the universal set", and only comparing against All() says
// so.
func TestALoneNonBreakingSpaceIsNotTheUniversalSet(t *testing.T) {
g, err := Parse(nbsp)
require.Error(t, err, "a grant string nobody can see must not decide what a credential can do")
assert.True(t, errors.Is(err, ErrInvalid))
assert.False(t, g.All(), "the refused parse must not hand back the widest set on the instance")
assert.True(t, g.Empty())
}
func TestOverlongGrantsAreRefused(t *testing.T) {
_, err := Parse(strings.Repeat("a", MaxGrantsLen+1))
require.Error(t, err)
assert.True(t, errors.Is(err, ErrInvalid))
}
// The id: member is stamped by the daemon and must never be requestable: a
// caller who could choose it could point their token's revocation check at
// somebody else's live row and make their own revoke a no-op.
func TestRequestedGrantsRefuseTheReservedIDMember(t *testing.T) {
for _, s := range []string{"id:7", "cov:upload id:7", "id:0", "id:nope"} {
_, err := ParseRequested(s)
require.Error(t, err, "should refuse %q", s)
assert.True(t, errors.Is(err, ErrInvalid), "%q: %v", s, err)
}
}
func TestStoredGrantsCarryTheIDMember(t *testing.T) {
g, err := Parse("bench:upload id:42")
require.NoError(t, err)
assert.Equal(t, 42, g.TokenID())
assert.True(t, g.Has("bench:upload"))
assert.False(t, g.Has("id:42"), "the id is metadata, not a permission")
assert.Equal(t, []string{"bench:upload"}, g.Members(),
"the id must not show up in what a page lists")
assert.Equal(t, "bench:upload id:42", g.String(), "the id renders last")
}
func TestIDMemberIsRefusedWhenItNamesNoRow(t *testing.T) {
for _, s := range []string{"id:0", "id:-1", "id:x", "id:1.5"} {
_, err := Parse(s)
require.Error(t, err, "should refuse %q", s)
assert.True(t, errors.Is(err, ErrInvalid), "%q: %v", s, err)
}
}
func TestWithTokenIDStampsAndStrips(t *testing.T) {
g, err := ParseRequested("bench:upload")
require.NoError(t, err)
stamped := g.WithTokenID(42)
assert.Equal(t, "bench:upload id:42", stamped.String())
assert.Equal(t, "bench:upload", g.String(), "the original must not be mutated")
assert.Equal(t, "bench:upload", stamped.WithTokenID(0).String())
universal := All().WithTokenID(9)
assert.Equal(t, "* id:9", universal.String())
round, err := Parse(universal.String())
require.NoError(t, err)
assert.True(t, round.All())
assert.Equal(t, 9, round.TokenID())
}
// The narrowing rule of SPEC ch. 2: an exchange may drop grants and may not add
// them.
func TestIsSubsetOfIsTheNarrowingRule(t *testing.T) {
parse := func(s string) Grants {
g, err := Parse(s)
require.NoError(t, err)
return g
}
parent := parse("cov:upload bench:upload")
assert.True(t, parse("bench:upload").IsSubsetOf(parent), "dropping a grant narrows")
assert.True(t, parse("cov:upload bench:upload").IsSubsetOf(parent), "asking for all of them")
assert.True(t, Grants{}.IsSubsetOf(parent), "the empty set is a subset of anything")
assert.False(t, parse("spec:propose").IsSubsetOf(parent), "a grant the parent lacks")
assert.False(t, parse("bench:upload spec:propose").IsSubsetOf(parent), "one of two is not enough")
assert.False(t, parse("*").IsSubsetOf(parent), "universal is not a subset of a named set")
all := parse("*")
assert.True(t, parse("bench:upload").IsSubsetOf(all), "everything narrows a universal parent")
assert.True(t, all.IsSubsetOf(all), "universal narrows universal")
}
// A stamped child is still a narrowing of its parent: the id: is not a
// permission and must take no part in the comparison.
func TestSubsetIgnoresTheIDMember(t *testing.T) {
parent, err := Parse("cov:upload bench:upload")
require.NoError(t, err)
child, err := Parse("bench:upload id:42")
require.NoError(t, err)
assert.True(t, child.IsSubsetOf(parent))
}
func TestMembersNeverExposeTheWildcardAsAMember(t *testing.T) {
g, err := Parse("*")
require.NoError(t, err)
assert.Equal(t, []string{Universal}, g.Members())
}