test(32.1-02): keep personal tokens unexpanded and reject bad Implies
- A personal token keeps its scopes and its credential TokenID - A write grant does not satisfy Implies read, and a present name is not duplicated - Document Implies and the pact.MemberTokenScopeCatalog publish
This commit is contained in:
@@ -106,6 +106,104 @@ func TestMCPImpliesOAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMCPImpliesPersonalUnchanged pins D-11: a personal token is not expanded,
|
||||
// and the credential ID stays the subject's TokenID.
|
||||
func TestMCPImpliesPersonalUnchanged(t *testing.T) {
|
||||
_, guard := impliesApp(t, impliesServersRead())
|
||||
ctx := context.Background()
|
||||
subject := pact.MCPSubject{Guard: "user", UserID: 7, TokenID: 42, Scopes: []string{"read"}}
|
||||
orig := subject.Scopes
|
||||
ok, err := guard.MCPAllows(ctx, subject, pact.MCPRequirements{Scopes: []string{"servers:read"}}, nil)
|
||||
if err != nil || ok {
|
||||
t.Fatalf("MCPAllows = %v, %v", ok, err)
|
||||
}
|
||||
if len(subject.Scopes) != 1 || &subject.Scopes[0] != &orig[0] || subject.Scopes[0] != "read" {
|
||||
t.Fatalf("subject.Scopes = %v, want the original grant slice", subject.Scopes)
|
||||
}
|
||||
dispatch, err := guard.MCPDispatch(ctx, subject, "GET")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, isToken := dispatch.Credential.(*models.APIToken)
|
||||
if !isToken || credential.ID != subject.TokenID || credential.HasScope("servers:read") || !credential.HasScope("read") {
|
||||
t.Fatalf("credential = %+v, want ID %d and scopes [read]", dispatch.Credential, subject.TokenID)
|
||||
}
|
||||
got := credential.Scopes.Get()
|
||||
if len(got) != 1 || &got[0] != &orig[0] {
|
||||
t.Fatalf("credential scopes = %v, want the unexpanded grant slice", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMCPImpliesWriteDoesNotGrantRead pins that a write grant does not
|
||||
// satisfy a scope whose Implies is read.
|
||||
func TestMCPImpliesWriteDoesNotGrantRead(t *testing.T) {
|
||||
_, guard := impliesApp(t, impliesServersRead())
|
||||
ctx := context.Background()
|
||||
subject := pact.MCPSubject{Guard: "user", UserID: 7, ClientID: "oauth-client", Scopes: []string{"write"}}
|
||||
ok, err := guard.MCPAllows(ctx, subject, pact.MCPRequirements{Scopes: []string{"servers:read"}}, nil)
|
||||
if err != nil || ok {
|
||||
t.Fatalf("MCPAllows = %v, %v", ok, err)
|
||||
}
|
||||
dispatch, err := guard.MCPDispatch(ctx, subject, "GET")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, isToken := dispatch.Credential.(*models.APIToken)
|
||||
if !isToken || credential.ID != 0 || credential.HasScope("servers:read") || !credential.HasScope("write") {
|
||||
t.Fatalf("credential = %+v", dispatch.Credential)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMCPImpliesAlreadyPresent does not append a declared name the grant
|
||||
// already holds, and does not append a catalog name whose Implies is empty.
|
||||
func TestMCPImpliesAlreadyPresent(t *testing.T) {
|
||||
_, guard := impliesApp(t, []pact.FrontendTokenScope{
|
||||
{Name: "servers:read", Label: "Read servers", Description: "List servers.", Implies: "read"},
|
||||
{Name: "servers:list", Label: "List servers", Description: "List only.", Implies: ""},
|
||||
})
|
||||
ctx := context.Background()
|
||||
subject := pact.MCPSubject{Guard: "user", UserID: 7, ClientID: "oauth-client", Scopes: []string{"read", "servers:read"}}
|
||||
dispatch, err := guard.MCPDispatch(ctx, subject, "GET")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, isToken := dispatch.Credential.(*models.APIToken)
|
||||
if !isToken || !slices.Equal(credential.Scopes.Get(), []string{"read", "servers:read"}) {
|
||||
t.Fatalf("credential scopes = %+v", dispatch.Credential)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMCPImpliesCompileRejects pins the boot errors for a bad Implies and
|
||||
// for the reserved names backend and offline_access. The error names the
|
||||
// plugin and the bad value. Empty, read, and write still compile.
|
||||
func TestMCPImpliesCompileRejects(t *testing.T) {
|
||||
t.Cleanup(classes.ResetDeclaredAPITokenScopes)
|
||||
one := func(name, implies string) pact.FrontendTokenScope {
|
||||
return pact.FrontendTokenScope{Name: name, Label: "Label", Description: "Description", Implies: implies}
|
||||
}
|
||||
boot := func(scope pact.FrontendTokenScope) error {
|
||||
classes.ResetDeclaredAPITokenScopes()
|
||||
return (&Plugin{}).bootAPITokenScopes(scopeBootApp(t, scopeDeclarer{"acme.scopes", []pact.FrontendTokenScope{scope}}))
|
||||
}
|
||||
for _, implies := range []string{"ai", "backend", "READ", " read"} {
|
||||
err := boot(one("servers:read", implies))
|
||||
if err == nil || !strings.Contains(err.Error(), "acme.scopes") || !strings.Contains(err.Error(), implies) {
|
||||
t.Fatalf("implies %q err = %v", implies, err)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"backend", "offline_access"} {
|
||||
err := boot(one(name, ""))
|
||||
if err == nil || !strings.Contains(err.Error(), "acme.scopes") || !strings.Contains(err.Error(), `"`+name+`"`) {
|
||||
t.Fatalf("name %q err = %v", name, err)
|
||||
}
|
||||
}
|
||||
for _, implies := range []string{"", "read", "write"} {
|
||||
if err := boot(one("servers:read", implies)); err != nil {
|
||||
t.Fatalf("implies %q: %v", implies, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
sql.Register("user_implies", impliesDriver{})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user