351 lines
8.8 KiB
Go
351 lines
8.8 KiB
Go
package utils
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
|
|
"github.com/grafana/authlib/authn"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
)
|
|
|
|
type expect struct {
|
|
decision authorizer.Decision
|
|
reason string
|
|
err string
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
user identity.Requester
|
|
attrs authorizer.Attributes
|
|
expect expect
|
|
breakpoint bool
|
|
}
|
|
|
|
func TestAuthorizer_Authorize(t *testing.T) {
|
|
userABC := &identity.StaticRequester{
|
|
UserUID: "abc",
|
|
OrgRole: identity.RoleViewer,
|
|
AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
|
|
Rest: authn.AccessTokenClaims{
|
|
DelegatedPermissions: []string{"group/stars:*", "group/preferences:*", "group/ns:*"},
|
|
},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
teams func(t *testing.T) TeamService
|
|
resource map[string][]ResourceOwner
|
|
check []testCase
|
|
}{
|
|
{
|
|
name: "stars",
|
|
resource: map[string][]ResourceOwner{
|
|
"stars": {UserResourceOwner},
|
|
},
|
|
check: []testCase{{
|
|
name: "matches user",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
Verb: "get",
|
|
APIGroup: "group",
|
|
Resource: "stars",
|
|
Name: "user-abc", // note this matches in input user name
|
|
ResourceRequest: true,
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionAllow,
|
|
},
|
|
}, {
|
|
name: "different user",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
Verb: "get",
|
|
APIGroup: "group",
|
|
Resource: "stars",
|
|
Name: "user-xyz", // not abc
|
|
ResourceRequest: true,
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "your are not the owner of the resource",
|
|
},
|
|
}},
|
|
}, {
|
|
name: "fast path",
|
|
resource: map[string][]ResourceOwner{
|
|
"stars": {UserResourceOwner},
|
|
"preferences": {TeamResourceOwner},
|
|
},
|
|
check: []testCase{{
|
|
name: "missing user",
|
|
attrs: authorizer.AttributesRecord{},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
err: "a Requester was not found in the context",
|
|
},
|
|
}, {
|
|
name: "not a resource",
|
|
user: &identity.StaticRequester{},
|
|
attrs: authorizer.AttributesRecord{
|
|
ResourceRequest: false,
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionNoOpinion,
|
|
},
|
|
}, {
|
|
name: "unknown resource",
|
|
user: &identity.StaticRequester{},
|
|
attrs: authorizer.AttributesRecord{
|
|
Resource: "xxxx",
|
|
ResourceRequest: true,
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "missing resource name",
|
|
},
|
|
}, {
|
|
name: "missing service permissions",
|
|
user: &identity.StaticRequester{
|
|
UserUID: "abc",
|
|
AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
|
|
Rest: authn.AccessTokenClaims{
|
|
DelegatedPermissions: []string{""},
|
|
},
|
|
},
|
|
},
|
|
attrs: authorizer.AttributesRecord{
|
|
Resource: "stars",
|
|
ResourceRequest: true,
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "calling service lacks required permissions",
|
|
},
|
|
}, {
|
|
name: "wrong owner type",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
APIGroup: "group",
|
|
Resource: "stars",
|
|
ResourceRequest: true,
|
|
Verb: "create", // missing name
|
|
Name: "team-xxx", // not supported
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "unsupported owner type",
|
|
},
|
|
}, {
|
|
name: "unknown resource",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
APIGroup: "group",
|
|
Resource: "stars",
|
|
ResourceRequest: true,
|
|
Verb: "create", // missing name
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "mutating request without a name",
|
|
},
|
|
}, {
|
|
name: "list request",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
APIGroup: "group",
|
|
Resource: "stars",
|
|
ResourceRequest: true,
|
|
Verb: "list", // no name
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionAllow,
|
|
},
|
|
}, {
|
|
name: "teams request (but not configured)",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
APIGroup: "group",
|
|
Resource: "preferences",
|
|
ResourceRequest: true,
|
|
Verb: "get",
|
|
Name: "team-XYZ",
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "team checker not configured",
|
|
},
|
|
}},
|
|
}, {
|
|
name: "unknown owner",
|
|
resource: map[string][]ResourceOwner{
|
|
"stars": {UnknownResourceOwner},
|
|
},
|
|
check: []testCase{{
|
|
name: "get",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
APIGroup: "group",
|
|
Resource: "stars",
|
|
Name: "something-not-an-owner",
|
|
ResourceRequest: true,
|
|
Verb: "get",
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionAllow,
|
|
},
|
|
}},
|
|
}, {
|
|
name: "namespace",
|
|
resource: map[string][]ResourceOwner{
|
|
"ns": {NamespaceResourceOwner},
|
|
},
|
|
check: []testCase{{
|
|
name: "readonly",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
APIGroup: "group",
|
|
Resource: "ns",
|
|
ResourceRequest: true,
|
|
Verb: "get",
|
|
Name: "namespace",
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionAllow,
|
|
},
|
|
}, {
|
|
name: "mutating",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
APIGroup: "group",
|
|
Resource: "ns",
|
|
ResourceRequest: true,
|
|
Verb: "create",
|
|
Name: "namespace",
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "must be an org admin to edit",
|
|
},
|
|
}, {
|
|
name: "org admin",
|
|
user: &identity.StaticRequester{
|
|
UserUID: "abc",
|
|
OrgRole: identity.RoleAdmin,
|
|
AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
|
|
Rest: authn.AccessTokenClaims{
|
|
DelegatedPermissions: []string{"group/ns:create"},
|
|
},
|
|
},
|
|
},
|
|
attrs: authorizer.AttributesRecord{
|
|
APIGroup: "group",
|
|
Resource: "ns",
|
|
ResourceRequest: true,
|
|
Verb: "create",
|
|
Name: "namespace",
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionAllow,
|
|
},
|
|
}},
|
|
}, {
|
|
name: "preferences teams",
|
|
teams: func(t *testing.T) TeamService {
|
|
teams := NewMockTeamService(t)
|
|
teams.On("InTeam", mock.Anything, userABC, "xyz", false).Return(true, nil)
|
|
teams.On("InTeam", mock.Anything, userABC, "456", false).Return(false, nil)
|
|
teams.On("InTeam", mock.Anything, userABC, "XXX", false).Return(true, fmt.Errorf("error from team"))
|
|
return teams
|
|
},
|
|
resource: map[string][]ResourceOwner{
|
|
"preferences": {
|
|
TeamResourceOwner,
|
|
},
|
|
},
|
|
check: []testCase{{
|
|
name: "user in team",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
Verb: "get",
|
|
APIGroup: "group",
|
|
Resource: "preferences",
|
|
Name: "team-xyz",
|
|
ResourceRequest: true,
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionAllow,
|
|
},
|
|
}, {
|
|
name: "user not in team",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
Verb: "get",
|
|
APIGroup: "group",
|
|
Resource: "preferences",
|
|
Name: "team-456",
|
|
ResourceRequest: true,
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "you are not a member of the referenced team",
|
|
},
|
|
}, {
|
|
name: "team error",
|
|
user: userABC,
|
|
attrs: authorizer.AttributesRecord{
|
|
Verb: "get",
|
|
APIGroup: "group",
|
|
Resource: "preferences",
|
|
Name: "team-XXX",
|
|
ResourceRequest: true,
|
|
},
|
|
expect: expect{
|
|
decision: authorizer.DecisionDeny,
|
|
reason: "error fetching teams",
|
|
err: "error from team",
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
authz := &AuthorizeFromName{
|
|
Resource: tt.resource,
|
|
}
|
|
if tt.teams != nil {
|
|
authz.Teams = tt.teams(t)
|
|
}
|
|
for _, check := range tt.check {
|
|
t.Run(check.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
if check.user != nil {
|
|
ctx = identity.WithRequester(ctx, check.user)
|
|
}
|
|
e := check.expect
|
|
if check.breakpoint {
|
|
require.True(t, true) // Can set breakpoint in IDE here
|
|
}
|
|
d, r, err := authz.Authorize(ctx, check.attrs)
|
|
if e.err != "" {
|
|
require.ErrorContains(t, err, e.err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Equal(t, e.decision, d)
|
|
if e.reason != "" {
|
|
require.Equal(t, e.reason, r)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|