Teams: Support team UIDs in APIs (#94011)

* support team UIDs in APIs

* unify middleware logic and add team tests

* add UID test to resource permissions

* remove unused middleware
This commit is contained in:
Jo
2024-10-07 10:18:24 +02:00
committed by GitHub
parent 413548806d
commit acd13e05ef
13 changed files with 202 additions and 50 deletions
+11 -10
View File
@@ -55,34 +55,35 @@ func ProvideTeamAPI(
func (tapi *TeamAPI) registerRoutes(router routing.RouteRegister, ac accesscontrol.AccessControl) {
authorize := accesscontrol.Middleware(ac)
teamResolver := team.MiddlewareTeamUIDResolver(tapi.teamService, ":teamId")
router.Group("/api", func(apiRoute routing.RouteRegister) {
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Post("/", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate)),
routing.Wrap(tapi.createTeam))
teamsRoute.Put("/:teamId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite,
teamsRoute.Put("/:teamId", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.updateTeam))
teamsRoute.Delete("/:teamId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsDelete,
teamsRoute.Delete("/:teamId", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsDelete,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.deleteTeamByID))
teamsRoute.Get("/:teamId/members", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsRead,
teamsRoute.Get("/:teamId/members", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsRead,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.getTeamMembers))
teamsRoute.Post("/:teamId/members", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite,
teamsRoute.Post("/:teamId/members", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.addTeamMember))
teamsRoute.Put("/:teamId/members/:userId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite,
teamsRoute.Put("/:teamId/members/:userId", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.updateTeamMember))
teamsRoute.Put("/:teamId/members", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite,
teamsRoute.Put("/:teamId/members", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.setTeamMemberships))
teamsRoute.Delete("/:teamId/members/:userId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite,
teamsRoute.Delete("/:teamId/members/:userId", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.removeTeamMember))
teamsRoute.Get("/:teamId/preferences", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead,
teamsRoute.Get("/:teamId/preferences", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.getTeamPreferences))
teamsRoute.Put("/:teamId/preferences", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite,
teamsRoute.Put("/:teamId/preferences", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.updateTeamPreferences))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Get("/:teamId", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead,
teamsRoute.Get("/:teamId", teamResolver, authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead,
accesscontrol.ScopeTeamsID)), routing.Wrap(tapi.getTeamByID))
teamsRoute.Get("/search", authorize(accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead)),
routing.Wrap(tapi.searchTeams))
+45 -8
View File
@@ -28,14 +28,18 @@ import (
"github.com/grafana/grafana/pkg/web/webtest"
)
func SetupAPITestServer(t *testing.T, opts ...func(a *TeamAPI)) *webtest.Server {
func SetupAPITestServer(t *testing.T, teamService team.Service, opts ...func(a *TeamAPI)) *webtest.Server {
t.Helper()
router := routing.NewRouteRegister()
cfg := setting.NewCfg()
cfg.LDAPAuthEnabled = true
if teamService == nil {
teamService = teamtest.NewFakeService()
}
a := ProvideTeamAPI(router,
teamtest.NewFakeService(),
teamService,
actest.FakeService{},
acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
&actest.FakePermissionsService{},
@@ -55,7 +59,7 @@ func SetupAPITestServer(t *testing.T, opts ...func(a *TeamAPI)) *webtest.Server
}
func TestAddTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t)
server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})
t.Run("should be able to add team member with correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
@@ -68,6 +72,17 @@ func TestAddTeamMembersAPIEndpoint(t *testing.T) {
require.NoError(t, res.Body.Close())
})
t.Run("should be able to add team member with correct permission by UID", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodPost, "/api/teams/a00001/members", strings.NewReader("{\"userId\": 1}")),
authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should not be able to add team member without correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodPost, "/api/teams/1/members", strings.NewReader("{\"userId\": 1}")),
@@ -81,7 +96,7 @@ func TestAddTeamMembersAPIEndpoint(t *testing.T) {
}
func TestGetTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t)
server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedIsMember: true, ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})
t.Run("should be able to get team members with correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
@@ -93,6 +108,18 @@ func TestGetTeamMembersAPIEndpoint(t *testing.T) {
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should be able to get team members with correct permission by UID", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewGetRequest("/api/teams/a00001/members"),
authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should not be able to get team members without correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewGetRequest("/api/teams/1/members"),
@@ -106,9 +133,7 @@ func TestGetTeamMembersAPIEndpoint(t *testing.T) {
}
func TestUpdateTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
hs.teamService = &teamtest.FakeService{ExpectedIsMember: true}
})
server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedIsMember: true, ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})
t.Run("should be able to update team member with correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
@@ -120,6 +145,18 @@ func TestUpdateTeamMembersAPIEndpoint(t *testing.T) {
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should be able to update team member with correct permission by team UID", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodPut, "/api/teams/a00001/members/1", strings.NewReader("{\"permission\": 1}")),
authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
)
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("should not be able to update team member without correct permission", func(t *testing.T) {
req := webtest.RequestWithSignedInUser(
server.NewRequest(http.MethodPut, "/api/teams/1/members/1", strings.NewReader("{\"permission\": 1}")),
@@ -133,7 +170,7 @@ func TestUpdateTeamMembersAPIEndpoint(t *testing.T) {
}
func TestDeleteTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
hs.teamService = &teamtest.FakeService{ExpectedIsMember: true}
hs.teamPermissionsService = &actest.FakePermissionsService{}
})
+63 -22
View File
@@ -21,16 +21,14 @@ import (
const (
searchTeamsURL = "/api/teams/search"
createTeamURL = "/api/teams/"
detailTeamURL = "/api/teams/%d"
detailTeamPreferenceURL = "/api/teams/%d/preferences"
detailTeamURL = "/api/teams/%v"
detailTeamPreferenceURL = "/api/teams/%v/preferences"
teamCmd = `{"name": "MyTestTeam%d"}`
teamPreferenceCmd = `{"theme": "dark"}`
)
func TestTeamAPIEndpoint_CreateTeam(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
hs.teamService = teamtest.NewFakeService()
})
server := SetupAPITestServer(t, nil)
input := strings.NewReader(fmt.Sprintf(teamCmd, 1))
t.Run("Access control allows creating teams with the correct permissions", func(t *testing.T) {
@@ -54,9 +52,7 @@ func TestTeamAPIEndpoint_CreateTeam(t *testing.T) {
}
func TestTeamAPIEndpoint_SearchTeams(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
hs.teamService = teamtest.NewFakeService()
})
server := SetupAPITestServer(t, nil)
t.Run("Access control prevents searching for teams with the incorrect permissions", func(t *testing.T) {
req := server.NewGetRequest(searchTeamsURL)
@@ -80,9 +76,7 @@ func TestTeamAPIEndpoint_SearchTeams(t *testing.T) {
}
func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}}
})
server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})
url := fmt.Sprintf(detailTeamURL, 1)
@@ -106,6 +100,30 @@ func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) {
require.NoError(t, res.Body.Close())
})
t.Run("Access control prevents getting a team when missing permissions by UID", func(t *testing.T) {
url := fmt.Sprintf(detailTeamURL, "a00001")
req := server.NewGetRequest(url)
req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:2"},
}))
res, err := server.Send(req)
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows getting a team by UID with the correct permissions", func(t *testing.T) {
url := fmt.Sprintf(detailTeamURL, "a00001")
req := server.NewGetRequest(url)
req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
}))
res, err := server.Send(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows getting a team with wildcard scope", func(t *testing.T) {
req := server.NewGetRequest(url)
req = webtest.RequestWithSignedInUser(req, authedUserWithPermissions(1, 1, []accesscontrol.Permission{
@@ -122,11 +140,9 @@ func TestTeamAPIEndpoint_GetTeamByID(t *testing.T) {
// Then the endpoint should return 200 if the user has accesscontrol.ActionTeamsWrite with teams:id:1 scope
// else return 403
func TestTeamAPIEndpoint_UpdateTeam(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}}
})
server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})
request := func(teamID int64, user *user.SignedInUser) (*http.Response, error) {
request := func(teamID any, user *user.SignedInUser) (*http.Response, error) {
req := server.NewRequest(http.MethodPut, fmt.Sprintf(detailTeamURL, teamID), strings.NewReader(teamCmd))
req = webtest.RequestWithSignedInUser(req, user)
return server.SendJSON(req)
@@ -141,6 +157,15 @@ func TestTeamAPIEndpoint_UpdateTeam(t *testing.T) {
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows updating team by UID with the correct permissions", func(t *testing.T) {
res, err := request("a00001", authedUserWithPermissions(1, 1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:id:1"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows updating teams with the wildcard scope", func(t *testing.T) {
res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:*"},
@@ -164,11 +189,9 @@ func TestTeamAPIEndpoint_UpdateTeam(t *testing.T) {
// Then the endpoint should return 200 if the user has accesscontrol.ActionTeamsDelete with teams:id:1 scope
// else return 403
func TestTeamAPIEndpoint_DeleteTeam(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
hs.teamService = &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{}}
})
server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})
request := func(teamID int64, user *user.SignedInUser) (*http.Response, error) {
request := func(teamID any, user *user.SignedInUser) (*http.Response, error) {
req := server.NewRequest(http.MethodDelete, fmt.Sprintf(detailTeamURL, teamID), http.NoBody)
req = webtest.RequestWithSignedInUser(req, user)
return server.Send(req)
@@ -191,17 +214,26 @@ func TestTeamAPIEndpoint_DeleteTeam(t *testing.T) {
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows deleting teams with the correct permissions by UID", func(t *testing.T) {
res, err := request("a00001", authedUserWithPermissions(1, 1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsDelete, Scope: "teams:id:1"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
// Given a team with a user, when the user is granted X permission,
// Then the endpoint should return 200 if the user has accesscontrol.ActionTeamsRead with teams:id:1 scope
// else return 403
func TestTeamAPIEndpoint_GetTeamPreferences(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}}, func(hs *TeamAPI) {
hs.preferenceService = &preftest.FakePreferenceService{ExpectedPreference: &pref.Preference{}}
})
request := func(teamID int64, user *user.SignedInUser) (*http.Response, error) {
request := func(teamID any, user *user.SignedInUser) (*http.Response, error) {
req := server.NewGetRequest(fmt.Sprintf(detailTeamPreferenceURL, teamID))
req = webtest.RequestWithSignedInUser(req, user)
return server.Send(req)
@@ -216,6 +248,15 @@ func TestTeamAPIEndpoint_GetTeamPreferences(t *testing.T) {
require.NoError(t, res.Body.Close())
})
t.Run("Access control allows getting team preferences with the correct permissions by UID", func(t *testing.T) {
res, err := request("a00001", authedUserWithPermissions(1, 1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
}))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Access control prevents getting team preferences with the incorrect permissions", func(t *testing.T) {
res, err := request(1, authedUserWithPermissions(1, 1, []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:2"},
@@ -230,7 +271,7 @@ func TestTeamAPIEndpoint_GetTeamPreferences(t *testing.T) {
// Then the endpoint should return 200 if the user has accesscontrol.ActionTeamsWrite with teams:id:1 scope
// else return 403
func TestTeamAPIEndpoint_UpdateTeamPreferences(t *testing.T) {
server := SetupAPITestServer(t, func(hs *TeamAPI) {
server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
hs.preferenceService = &preftest.FakePreferenceService{ExpectedPreference: &pref.Preference{}}
})