mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Team access changes for editors when editorsCanAdmin is enabled (#45405)
* filter teams for editors to only show the teams that they are members of * frontend changes to only allow clicking on teams that the user can edit * update frontend test snapshots * extend docs * reword * remove the comment for now * Update backend tests * reword the warning, and add it back in * docs feedback Co-authored-by: gamab <gabi.mabs@gmail.com>
This commit is contained in:
parent
d718ee1918
commit
11433cba97
@ -13,7 +13,8 @@ Access to these API endpoints is restricted as follows:
|
|||||||
|
|
||||||
- All authenticated users are able to view details of teams they are a member of.
|
- All authenticated users are able to view details of teams they are a member of.
|
||||||
- Organization Admins are able to manage all teams and team members.
|
- Organization Admins are able to manage all teams and team members.
|
||||||
- If the `editors_can_admin` configuration flag is enabled, Organization Editors are able to view details of all teams and to manage teams that they are Admin members of.
|
- If you enable `editors_can_admin` configuration flag, then Organization Editors can create teams and manage teams where they are Admin.
|
||||||
|
- If you enable `editors_can_admin` configuration flag, Editors can find out whether a team that they are not members of exists by trying to create a team with the same name.
|
||||||
|
|
||||||
> If you are running Grafana Enterprise and have [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, access to endpoints will be controlled by Fine-grained access control permissions.
|
> If you are running Grafana Enterprise and have [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, access to endpoints will be controlled by Fine-grained access control permissions.
|
||||||
> Refer to specific endpoints to understand what permissions are required.
|
> Refer to specific endpoints to understand what permissions are required.
|
||||||
|
@ -133,7 +133,7 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
|||||||
// Using accesscontrol the filtering is done based on user permissions
|
// Using accesscontrol the filtering is done based on user permissions
|
||||||
userIdFilter := models.FilterIgnoreUser
|
userIdFilter := models.FilterIgnoreUser
|
||||||
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||||
userIdFilter = userFilter(hs.Cfg.EditorsCanAdmin, c)
|
userIdFilter = userFilter(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
query := models.SearchTeamsQuery{
|
query := models.SearchTeamsQuery{
|
||||||
@ -189,14 +189,12 @@ func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID
|
|||||||
|
|
||||||
// UserFilter returns the user ID used in a filter when querying a team
|
// UserFilter returns the user ID used in a filter when querying a team
|
||||||
// 1. If the user is a viewer or editor, this will return the user's ID.
|
// 1. If the user is a viewer or editor, this will return the user's ID.
|
||||||
// 2. If EditorsCanAdmin is enabled and the user is an editor, this will return models.FilterIgnoreUser (0)
|
// 2. If the user is an admin, this will return models.FilterIgnoreUser (0)
|
||||||
// 3. If the user is an admin, this will return models.FilterIgnoreUser (0)
|
func userFilter(c *models.ReqContext) int64 {
|
||||||
func userFilter(editorsCanAdmin bool, c *models.ReqContext) int64 {
|
|
||||||
userIdFilter := c.SignedInUser.UserId
|
userIdFilter := c.SignedInUser.UserId
|
||||||
if (editorsCanAdmin && c.OrgRole == models.ROLE_EDITOR) || c.OrgRole == models.ROLE_ADMIN {
|
if c.OrgRole == models.ROLE_ADMIN {
|
||||||
userIdFilter = models.FilterIgnoreUser
|
userIdFilter = models.FilterIgnoreUser
|
||||||
}
|
}
|
||||||
|
|
||||||
return userIdFilter
|
return userIdFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +208,7 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
|
|||||||
// Using accesscontrol the filtering has already been performed at middleware layer
|
// Using accesscontrol the filtering has already been performed at middleware layer
|
||||||
userIdFilter := models.FilterIgnoreUser
|
userIdFilter := models.FilterIgnoreUser
|
||||||
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||||
userIdFilter = userFilter(hs.Cfg.EditorsCanAdmin, c)
|
userIdFilter = userFilter(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
query := models.GetTeamByIdQuery{
|
query := models.GetTeamByIdQuery{
|
||||||
|
@ -40,39 +40,69 @@ func TestTeamAPIEndpoint(t *testing.T) {
|
|||||||
hs.SQLStore = store
|
hs.SQLStore = store
|
||||||
mock := &mockstore.SQLStoreMock{}
|
mock := &mockstore.SQLStoreMock{}
|
||||||
|
|
||||||
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
|
loggedInUserScenarioWithRole(t, "When admin is calling GET on", "GET", "/api/teams/search", "/api/teams/search",
|
||||||
_, err := hs.SQLStore.CreateTeam("team1", "", 1)
|
models.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||||
require.NoError(t, err)
|
_, err := hs.SQLStore.CreateTeam("team1", "", 1)
|
||||||
_, err = hs.SQLStore.CreateTeam("team2", "", 1)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
_, err = hs.SQLStore.CreateTeam("team2", "", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
sc.handlerFunc = hs.SearchTeams
|
sc.handlerFunc = hs.SearchTeams
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
require.Equal(t, http.StatusOK, sc.resp.Code)
|
require.Equal(t, http.StatusOK, sc.resp.Code)
|
||||||
var resp models.SearchTeamQueryResult
|
var resp models.SearchTeamQueryResult
|
||||||
err = json.Unmarshal(sc.resp.Body.Bytes(), &resp)
|
err = json.Unmarshal(sc.resp.Body.Bytes(), &resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.EqualValues(t, 2, resp.TotalCount)
|
assert.EqualValues(t, 2, resp.TotalCount)
|
||||||
assert.Equal(t, 2, len(resp.Teams))
|
assert.Equal(t, 2, len(resp.Teams))
|
||||||
}, mock)
|
}, mock)
|
||||||
|
|
||||||
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
|
loggedInUserScenario(t, "When editor (with editors_can_admin) is calling GET on", "/api/teams/search",
|
||||||
_, err := hs.SQLStore.CreateTeam("team1", "", 1)
|
"/api/teams/search", func(sc *scenarioContext) {
|
||||||
require.NoError(t, err)
|
team1, err := hs.SQLStore.CreateTeam("team1", "", 1)
|
||||||
_, err = hs.SQLStore.CreateTeam("team2", "", 1)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
_, err = hs.SQLStore.CreateTeam("team2", "", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
sc.handlerFunc = hs.SearchTeams
|
// Adding the test user to the teams in order for him to list them
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
err = hs.SQLStore.AddTeamMember(testUserID, testOrgID, team1.Id, false, 0)
|
||||||
require.Equal(t, http.StatusOK, sc.resp.Code)
|
require.NoError(t, err)
|
||||||
var resp models.SearchTeamQueryResult
|
|
||||||
err = json.Unmarshal(sc.resp.Body.Bytes(), &resp)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.EqualValues(t, 2, resp.TotalCount)
|
sc.handlerFunc = hs.SearchTeams
|
||||||
assert.Equal(t, 0, len(resp.Teams))
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
}, mock)
|
require.Equal(t, http.StatusOK, sc.resp.Code)
|
||||||
|
var resp models.SearchTeamQueryResult
|
||||||
|
err = json.Unmarshal(sc.resp.Body.Bytes(), &resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 1, resp.TotalCount)
|
||||||
|
assert.Equal(t, 1, len(resp.Teams))
|
||||||
|
}, mock)
|
||||||
|
|
||||||
|
loggedInUserScenario(t, "When editor (with editors_can_admin) calling GET with pagination on",
|
||||||
|
"/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
|
||||||
|
team1, err := hs.SQLStore.CreateTeam("team1", "", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
team2, err := hs.SQLStore.CreateTeam("team2", "", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Adding the test user to the teams in order for him to list them
|
||||||
|
err = hs.SQLStore.AddTeamMember(testUserID, testOrgID, team1.Id, false, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = hs.SQLStore.AddTeamMember(testUserID, testOrgID, team2.Id, false, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sc.handlerFunc = hs.SearchTeams
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||||
|
require.Equal(t, http.StatusOK, sc.resp.Code)
|
||||||
|
var resp models.SearchTeamQueryResult
|
||||||
|
err = json.Unmarshal(sc.resp.Body.Bytes(), &resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 2, resp.TotalCount)
|
||||||
|
assert.Equal(t, 0, len(resp.Teams))
|
||||||
|
}, mock)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("When creating team with API key", func(t *testing.T) {
|
t.Run("When creating team with API key", func(t *testing.T) {
|
||||||
|
@ -76,6 +76,18 @@ func getTeamSelectSQLBase(filteredUsers []string) string {
|
|||||||
` FROM team as team `
|
` FROM team as team `
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTeamSelectWithPermissionsSQLBase(filteredUsers []string) string {
|
||||||
|
return `SELECT
|
||||||
|
team.id AS id,
|
||||||
|
team.org_id,
|
||||||
|
team.name AS name,
|
||||||
|
team.email AS email,
|
||||||
|
team_member.permission, ` +
|
||||||
|
getTeamMemberCount(filteredUsers) +
|
||||||
|
` FROM team AS team
|
||||||
|
INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ? `
|
||||||
|
}
|
||||||
|
|
||||||
func (ss *SQLStore) CreateTeam(name, email string, orgID int64) (models.Team, error) {
|
func (ss *SQLStore) CreateTeam(name, email string, orgID int64) (models.Team, error) {
|
||||||
team := models.Team{
|
team := models.Team{
|
||||||
Name: name,
|
Name: name,
|
||||||
@ -188,14 +200,14 @@ func (ss *SQLStore) SearchTeams(ctx context.Context, query *models.SearchTeamsQu
|
|||||||
params := make([]interface{}, 0)
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
filteredUsers := getFilteredUsers(query.SignedInUser, query.HiddenUsers)
|
filteredUsers := getFilteredUsers(query.SignedInUser, query.HiddenUsers)
|
||||||
sql.WriteString(getTeamSelectSQLBase(filteredUsers))
|
|
||||||
|
|
||||||
for _, user := range filteredUsers {
|
for _, user := range filteredUsers {
|
||||||
params = append(params, user)
|
params = append(params, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.UserIdFilter != models.FilterIgnoreUser {
|
if query.UserIdFilter == models.FilterIgnoreUser {
|
||||||
sql.WriteString(` INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ?`)
|
sql.WriteString(getTeamSelectSQLBase(filteredUsers))
|
||||||
|
} else {
|
||||||
|
sql.WriteString(getTeamSelectWithPermissionsSQLBase(filteredUsers))
|
||||||
params = append(params, query.UserIdFilter)
|
params = append(params, query.UserIdFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,11 +69,9 @@ export class TeamList extends PureComponent<Props, State> {
|
|||||||
const { editorsCanAdmin, signedInUser } = this.props;
|
const { editorsCanAdmin, signedInUser } = this.props;
|
||||||
const permission = team.permission;
|
const permission = team.permission;
|
||||||
const teamUrl = `org/teams/edit/${team.id}`;
|
const teamUrl = `org/teams/edit/${team.id}`;
|
||||||
const canDelete = contextSrv.hasAccessInMetadata(
|
const isTeamAdmin = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser });
|
||||||
AccessControlAction.ActionTeamsDelete,
|
const canDelete = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsDelete, team, isTeamAdmin);
|
||||||
team,
|
const canReadTeam = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRead, team, isTeamAdmin);
|
||||||
isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser })
|
|
||||||
);
|
|
||||||
const canSeeTeamRoles = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRolesList, team, false);
|
const canSeeTeamRoles = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRolesList, team, false);
|
||||||
const canUpdateTeamRoles =
|
const canUpdateTeamRoles =
|
||||||
contextSrv.hasAccess(AccessControlAction.ActionTeamsRolesAdd, false) ||
|
contextSrv.hasAccess(AccessControlAction.ActionTeamsRolesAdd, false) ||
|
||||||
@ -86,20 +84,34 @@ export class TeamList extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<tr key={team.id}>
|
<tr key={team.id}>
|
||||||
<td className="width-4 text-center link-td">
|
<td className="width-4 text-center link-td">
|
||||||
<a href={teamUrl}>
|
{canReadTeam ? (
|
||||||
|
<a href={teamUrl}>
|
||||||
|
<img className="filter-table__avatar" src={team.avatarUrl} alt="Team avatar" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
<img className="filter-table__avatar" src={team.avatarUrl} alt="Team avatar" />
|
<img className="filter-table__avatar" src={team.avatarUrl} alt="Team avatar" />
|
||||||
</a>
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="link-td">
|
<td className="link-td">
|
||||||
<a href={teamUrl}>{team.name}</a>
|
{canReadTeam ? <a href={teamUrl}>{team.name}</a> : <div style={{ padding: '0px 8px' }}>{team.name}</div>}
|
||||||
</td>
|
</td>
|
||||||
<td className="link-td">
|
<td className="link-td">
|
||||||
<a href={teamUrl} aria-label={team.email?.length > 0 ? undefined : 'Empty email cell'}>
|
{canReadTeam ? (
|
||||||
{team.email}
|
<a href={teamUrl} aria-label={team.email?.length > 0 ? undefined : 'Empty email cell'}>
|
||||||
</a>
|
{team.email}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div style={{ padding: '0px 8px' }} aria-label={team.email?.length > 0 ? undefined : 'Empty email cell'}>
|
||||||
|
{team.email}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="link-td">
|
<td className="link-td">
|
||||||
<a href={teamUrl}>{team.memberCount}</a>
|
{canReadTeam ? (
|
||||||
|
<a href={teamUrl}>{team.memberCount}</a>
|
||||||
|
) : (
|
||||||
|
<div style={{ padding: '0px 8px' }}>{team.memberCount}</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
{displayRolePicker && (
|
{displayRolePicker && (
|
||||||
<td>
|
<td>
|
||||||
|
@ -445,42 +445,50 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
|
|||||||
<td
|
<td
|
||||||
className="width-4 text-center link-td"
|
className="width-4 text-center link-td"
|
||||||
>
|
>
|
||||||
<a
|
<img
|
||||||
href="org/teams/edit/1"
|
alt="Team avatar"
|
||||||
>
|
className="filter-table__avatar"
|
||||||
<img
|
src="some/url/"
|
||||||
alt="Team avatar"
|
/>
|
||||||
className="filter-table__avatar"
|
|
||||||
src="some/url/"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="link-td"
|
className="link-td"
|
||||||
>
|
>
|
||||||
<a
|
<div
|
||||||
href="org/teams/edit/1"
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0px 8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
test-1
|
test-1
|
||||||
</a>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="link-td"
|
className="link-td"
|
||||||
>
|
>
|
||||||
<a
|
<div
|
||||||
href="org/teams/edit/1"
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0px 8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
test-1@test.com
|
test-1@test.com
|
||||||
</a>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="link-td"
|
className="link-td"
|
||||||
>
|
>
|
||||||
<a
|
<div
|
||||||
href="org/teams/edit/1"
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0px 8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
1
|
1
|
||||||
</a>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="text-right"
|
className="text-right"
|
||||||
@ -583,42 +591,50 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
|
|||||||
<td
|
<td
|
||||||
className="width-4 text-center link-td"
|
className="width-4 text-center link-td"
|
||||||
>
|
>
|
||||||
<a
|
<img
|
||||||
href="org/teams/edit/1"
|
alt="Team avatar"
|
||||||
>
|
className="filter-table__avatar"
|
||||||
<img
|
src="some/url/"
|
||||||
alt="Team avatar"
|
/>
|
||||||
className="filter-table__avatar"
|
|
||||||
src="some/url/"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="link-td"
|
className="link-td"
|
||||||
>
|
>
|
||||||
<a
|
<div
|
||||||
href="org/teams/edit/1"
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0px 8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
test-1
|
test-1
|
||||||
</a>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="link-td"
|
className="link-td"
|
||||||
>
|
>
|
||||||
<a
|
<div
|
||||||
href="org/teams/edit/1"
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0px 8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
test-1@test.com
|
test-1@test.com
|
||||||
</a>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="link-td"
|
className="link-td"
|
||||||
>
|
>
|
||||||
<a
|
<div
|
||||||
href="org/teams/edit/1"
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0px 8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
1
|
1
|
||||||
</a>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="text-right"
|
className="text-right"
|
||||||
|
Loading…
Reference in New Issue
Block a user