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("cover:upload bench:upload spec:propose") require.NoError(t, err) assert.False(t, g.All()) assert.True(t, g.Has("cover:upload")) assert.True(t, g.Has("bench:upload")) assert.True(t, g.Has("spec:propose")) assert.False(t, g.Has("cover: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()) } // "* cover: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("cover: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 cover:upload bench:upload cover:upload") require.NoError(t, err) assert.Equal(t, "bench:upload cover: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": "cover:", "empty segment": "cover::upload", "upper case": "Cover:upload", "non-ascii": "cover:upload" + nbsp, "lone nbsp": nbsp, "control byte": "cover:up\x01load", "bare wildcard 2": "cover: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", "cover: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("cover:upload bench:upload") assert.True(t, parse("bench:upload").IsSubsetOf(parent), "dropping a grant narrows") assert.True(t, parse("cover: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("cover: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()) }