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) } }) } }) } }