mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AccessControl: Change teams permissions page when accesscontrol is enabled (#43971)
* AccessControl: Change teams permissions page when frontend is hit * Implement frontend changes for group sync * Changing the org/teams/edit permissions Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Fixing routes Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Use props straight away no need to go through the state Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Update public/app/features/teams/TeamPages.tsx Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
parent
85ea1a5d64
commit
8217d6d206
@ -57,8 +57,9 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index)
|
||||
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
|
||||
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index)
|
||||
r.Get("/org/teams", reqCanAccessTeams, hs.Index)
|
||||
r.Get("/org/teams/*", reqCanAccessTeams, hs.Index)
|
||||
r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index)
|
||||
r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index)
|
||||
r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index)
|
||||
r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index)
|
||||
r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index)
|
||||
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
|
||||
|
@ -311,9 +311,10 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
|
||||
return &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Bus: bus.GetBus(),
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +148,7 @@ func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
|
||||
hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts)
|
||||
}
|
||||
|
||||
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
|
||||
func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool {
|
||||
return c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR)
|
||||
}
|
||||
|
||||
@ -263,7 +263,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
})
|
||||
}
|
||||
|
||||
if enableTeams(hs, c) {
|
||||
if hasAccess(hs.ReqCanAdminTeams, teamsAccessEvaluator) {
|
||||
configNodes = append(configNodes, &dtos.NavLink{
|
||||
Text: "Teams",
|
||||
Id: "teams",
|
||||
|
@ -298,3 +298,22 @@ var orgsCreateAccessEvaluator = accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(ActionOrgsRead),
|
||||
accesscontrol.EvalPermission(ActionOrgsCreate),
|
||||
)
|
||||
|
||||
// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access
|
||||
var teamsAccessEvaluator = accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead),
|
||||
accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite),
|
||||
),
|
||||
)
|
||||
|
||||
// teamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access
|
||||
var teamsEditAccessEvaluator = accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead),
|
||||
accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite),
|
||||
),
|
||||
)
|
||||
|
@ -2,12 +2,14 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@ -103,6 +105,20 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response {
|
||||
return response.Success("Team deleted")
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getTeamsAccessControlMetadata(c *models.ReqContext, teamIDs map[string]bool) (map[string]accesscontrol.Metadata, error) {
|
||||
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
|
||||
if err != nil || len(userPermissions) == 0 {
|
||||
hs.log.Warn("could not fetch accesscontrol metadata for teams", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs), nil
|
||||
}
|
||||
|
||||
// GET /api/teams/search
|
||||
func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
perPage := c.QueryInt("perpage")
|
||||
@ -134,8 +150,17 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
return response.Error(500, "Failed to search Teams", err)
|
||||
}
|
||||
|
||||
teamIDs := map[string]bool{}
|
||||
for _, team := range query.Result.Teams {
|
||||
team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
|
||||
teamIDs[strconv.FormatInt(team.Id, 10)] = true
|
||||
}
|
||||
|
||||
metadata, err := hs.getTeamsAccessControlMetadata(c, teamIDs)
|
||||
if err == nil && len(metadata) != 0 {
|
||||
for _, team := range query.Result.Teams {
|
||||
team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)]
|
||||
}
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
@ -144,6 +169,23 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
return response.JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID int64) (accesscontrol.Metadata, error) {
|
||||
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
|
||||
if err != nil || len(userPermissions) == 0 {
|
||||
hs.log.Warn("could not fetch accesscontrol metadata", "team", teamID, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d", teamID)
|
||||
teamIDs := map[string]bool{key: true}
|
||||
|
||||
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs)[key], nil
|
||||
}
|
||||
|
||||
// GET /api/teams/:teamId
|
||||
func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
|
||||
teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
|
||||
@ -165,6 +207,9 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
|
||||
return response.Error(500, "Failed to get Team", err)
|
||||
}
|
||||
|
||||
metadata, _ := hs.getTeamAccessControlMetadata(c, query.Result.Id)
|
||||
query.Result.AccessControl = metadata
|
||||
|
||||
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
|
||||
return response.JSON(200, &query.Result)
|
||||
}
|
||||
|
@ -77,13 +77,14 @@ type SearchTeamsQuery struct {
|
||||
}
|
||||
|
||||
type TeamDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
Permission PermissionType `json:"permission"`
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
Permission PermissionType `json:"permission"`
|
||||
AccessControl map[string]bool `json:"accessControl"`
|
||||
}
|
||||
|
||||
type SearchTeamQueryResult struct {
|
||||
|
@ -143,6 +143,13 @@ export class ContextSrv {
|
||||
return this.hasPermission(action);
|
||||
}
|
||||
|
||||
hasAccessInMetadata(action: string, object: WithAccessControlMetadata, fallBack: boolean) {
|
||||
if (!config.featureToggles['accesscontrol']) {
|
||||
return fallBack;
|
||||
}
|
||||
return this.hasPermissionInMetadata(action, object);
|
||||
}
|
||||
|
||||
// evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled
|
||||
evaluatePermission(fallback: () => string[], actions: string[]) {
|
||||
if (!this.accessControlEnabled()) {
|
||||
|
@ -6,6 +6,7 @@ import { getMockTeamGroups } from './__mocks__/teamMocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
isReadOnly: false,
|
||||
groups: [] as TeamGroup[],
|
||||
loadTeamGroups: jest.fn(),
|
||||
addTeamGroup: jest.fn(),
|
||||
|
@ -23,13 +23,17 @@ const mapDispatchToProps = {
|
||||
removeTeamGroup,
|
||||
};
|
||||
|
||||
interface OwnProps {
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isAdding: boolean;
|
||||
newGroupId: string;
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
export type Props = ConnectedProps<typeof connector>;
|
||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`;
|
||||
|
||||
@ -70,11 +74,12 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
renderGroup(group: TeamGroup) {
|
||||
const { isReadOnly } = this.props;
|
||||
return (
|
||||
<tr key={group.groupId}>
|
||||
<td>{group.groupId}</td>
|
||||
<td style={{ width: '1%' }}>
|
||||
<Button size="sm" variant="destructive" onClick={() => this.onRemoveGroup(group)}>
|
||||
<Button size="sm" variant="destructive" onClick={() => this.onRemoveGroup(group)} disabled={isReadOnly}>
|
||||
<Icon name="times" />
|
||||
</Button>
|
||||
</td>
|
||||
@ -84,7 +89,7 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { isAdding, newGroupId } = this.state;
|
||||
const groups = this.props.groups;
|
||||
const { groups, isReadOnly } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -95,7 +100,7 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
</Tooltip>
|
||||
<div className="page-action-bar__spacer" />
|
||||
{groups.length > 0 && (
|
||||
<Button className="pull-right" onClick={this.onToggleAdding}>
|
||||
<Button className="pull-right" onClick={this.onToggleAdding} disabled={isReadOnly}>
|
||||
<Icon name="plus" /> Add group
|
||||
</Button>
|
||||
)}
|
||||
@ -113,11 +118,12 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
value={newGroupId}
|
||||
onChange={this.onNewGroupIdChanged}
|
||||
placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<Button type="submit" disabled={!this.isNewGroupValid()}>
|
||||
<Button type="submit" disabled={isReadOnly || !this.isNewGroupValid()}>
|
||||
Add group
|
||||
</Button>
|
||||
</div>
|
||||
@ -135,6 +141,7 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
proTipLinkTitle="Learn more"
|
||||
proTipLink="http://docs.grafana.org/auth/enhanced_ldap/"
|
||||
proTipTarget="_blank"
|
||||
buttonDisabled={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -69,7 +69,11 @@ export class TeamList extends PureComponent<Props, State> {
|
||||
const { editorsCanAdmin, signedInUser } = this.props;
|
||||
const permission = team.permission;
|
||||
const teamUrl = `org/teams/edit/${team.id}`;
|
||||
const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser });
|
||||
const canDelete = contextSrv.hasAccessInMetadata(
|
||||
AccessControlAction.ActionTeamsDelete,
|
||||
team,
|
||||
isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser })
|
||||
);
|
||||
|
||||
return (
|
||||
<tr key={team.id}>
|
||||
|
@ -13,6 +13,7 @@ jest.mock('@grafana/runtime/src/config', () => ({
|
||||
licenseInfo: {
|
||||
enabledFeatures: { teamsync: true },
|
||||
},
|
||||
featureToggles: { accesscontrol: false },
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -4,9 +4,10 @@ import { includes } from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import TeamMembers from './TeamMembers';
|
||||
import TeamPermissions from './TeamPermissions';
|
||||
import TeamSettings from './TeamSettings';
|
||||
import TeamGroupSync from './TeamGroupSync';
|
||||
import { StoreState } from 'app/types';
|
||||
import { AccessControlAction, StoreState } from 'app/types';
|
||||
import { loadTeam, loadTeamMembers } from './state/actions';
|
||||
import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors';
|
||||
import { getTeamLoadingNav } from './state/navModel';
|
||||
@ -37,10 +38,17 @@ enum PageTypes {
|
||||
|
||||
function mapStateToProps(state: StoreState, props: OwnProps) {
|
||||
const teamId = parseInt(props.match.params.id, 10);
|
||||
const pageName = props.match.params.page ?? 'members';
|
||||
const team = getTeam(state.team, teamId);
|
||||
let defaultPage = 'members';
|
||||
if (contextSrv.accessControlEnabled()) {
|
||||
// With FGAC the settings page will always be available
|
||||
if (!team || !contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team)) {
|
||||
defaultPage = 'settings';
|
||||
}
|
||||
}
|
||||
const pageName = props.match.params.page ?? defaultPage;
|
||||
const teamLoadingNav = getTeamLoadingNav(pageName as string);
|
||||
const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav);
|
||||
const team = getTeam(state.team, teamId);
|
||||
const members = getTeamMembers(state.team);
|
||||
|
||||
return {
|
||||
@ -81,7 +89,10 @@ export class TeamPages extends PureComponent<Props, State> {
|
||||
const { loadTeam, teamId } = this.props;
|
||||
this.setState({ isLoading: true });
|
||||
const team = await loadTeam(teamId);
|
||||
await this.props.loadTeamMembers();
|
||||
// With accesscontrol, the TeamPermissions will fetch team members
|
||||
if (!contextSrv.accessControlEnabled()) {
|
||||
await this.props.loadTeamMembers();
|
||||
}
|
||||
this.setState({ isLoading: false });
|
||||
return team;
|
||||
}
|
||||
@ -105,6 +116,10 @@ export class TeamPages extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => {
|
||||
if (contextSrv.accessControlEnabled()) {
|
||||
return navModel;
|
||||
}
|
||||
|
||||
if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) {
|
||||
navModel.main.children
|
||||
.filter((navItem) => !this.textsAreEqual(navItem.text, PageTypes.Members))
|
||||
@ -121,15 +136,34 @@ export class TeamPages extends PureComponent<Props, State> {
|
||||
const { members, team } = this.props;
|
||||
const currentPage = this.getCurrentPage();
|
||||
|
||||
const canReadTeam = contextSrv.hasAccessInMetadata(
|
||||
AccessControlAction.ActionTeamsRead,
|
||||
team!,
|
||||
isSignedInUserTeamAdmin
|
||||
);
|
||||
const canReadTeamPermissions = contextSrv.hasAccessInMetadata(
|
||||
AccessControlAction.ActionTeamsPermissionsRead,
|
||||
team!,
|
||||
isSignedInUserTeamAdmin
|
||||
);
|
||||
const canWriteTeamPermissions = contextSrv.hasAccessInMetadata(
|
||||
AccessControlAction.ActionTeamsPermissionsWrite,
|
||||
team!,
|
||||
isSignedInUserTeamAdmin
|
||||
);
|
||||
|
||||
switch (currentPage) {
|
||||
case PageTypes.Members:
|
||||
return <TeamMembers syncEnabled={isSyncEnabled} members={members} />;
|
||||
|
||||
if (contextSrv.accessControlEnabled()) {
|
||||
return <TeamPermissions team={team!} />;
|
||||
} else {
|
||||
return <TeamMembers syncEnabled={isSyncEnabled} members={members} />;
|
||||
}
|
||||
case PageTypes.Settings:
|
||||
return isSignedInUserTeamAdmin && <TeamSettings team={team!} />;
|
||||
return canReadTeam && <TeamSettings team={team!} />;
|
||||
case PageTypes.GroupSync:
|
||||
if (isSignedInUserTeamAdmin && isSyncEnabled) {
|
||||
return <TeamGroupSync />;
|
||||
if (canReadTeamPermissions && isSyncEnabled) {
|
||||
return <TeamGroupSync isReadOnly={!canWriteTeamPermissions} />;
|
||||
} else if (config.featureToggles.featureHighlights) {
|
||||
return (
|
||||
<UpgradeBox
|
||||
|
31
public/app/features/teams/TeamPermissions.tsx
Normal file
31
public/app/features/teams/TeamPermissions.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Permissions } from 'app/core/components/AccessControl';
|
||||
import { AccessControlAction, Team } from '../../types';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
type TeamPermissionsProps = {
|
||||
team: Team;
|
||||
};
|
||||
|
||||
// TeamPermissions component replaces TeamMembers component when the accesscontrol feature flag is set
|
||||
const TeamPermissions = (props: TeamPermissionsProps) => {
|
||||
const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead);
|
||||
const canSetPermissions = contextSrv.hasPermissionInMetadata(
|
||||
AccessControlAction.ActionTeamsPermissionsWrite,
|
||||
props.team
|
||||
);
|
||||
|
||||
return (
|
||||
<Permissions
|
||||
title="Members"
|
||||
addPermissionTitle="Add member"
|
||||
buttonLabel="Add member"
|
||||
resource="teams"
|
||||
resourceId={props.team.id}
|
||||
canListUsers={canListUsers}
|
||||
canSetPermissions={canSetPermissions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamPermissions;
|
@ -3,6 +3,12 @@ import { shallow } from 'enzyme';
|
||||
import { Props, TeamSettings } from './TeamSettings';
|
||||
import { getMockTeam } from './__mocks__/teamMocks';
|
||||
|
||||
jest.mock('app/core/core', () => ({
|
||||
contextSrv: {
|
||||
hasPermissionInMetadata: () => true,
|
||||
},
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
team: getMockTeam(),
|
||||
|
@ -4,7 +4,8 @@ import { Input, Field, Form, Button, FieldSet, VerticalGroup } from '@grafana/ui
|
||||
|
||||
import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
|
||||
import { updateTeam } from './state/actions';
|
||||
import { Team } from 'app/types';
|
||||
import { AccessControlAction, Team } from 'app/types';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateTeam,
|
||||
@ -18,6 +19,8 @@ interface OwnProps {
|
||||
export type Props = ConnectedProps<typeof connector> & OwnProps;
|
||||
|
||||
export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
|
||||
const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team);
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<FieldSet label="Team settings">
|
||||
@ -26,6 +29,7 @@ export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
|
||||
onSubmit={(formTeam: Team) => {
|
||||
updateTeam(formTeam.name, formTeam.email);
|
||||
}}
|
||||
disabled={!canWriteTeamSettings}
|
||||
>
|
||||
{({ register }) => (
|
||||
<>
|
||||
@ -44,7 +48,7 @@ export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
|
||||
)}
|
||||
</Form>
|
||||
</FieldSet>
|
||||
<SharedPreferences resourceUri={`teams/${team.id}`} />
|
||||
<SharedPreferences resourceUri={`teams/${team.id}`} disabled={!canWriteTeamSettings} />
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
|
@ -44,6 +44,7 @@ exports[`Render should render component 1`] = `
|
||||
>
|
||||
<Input
|
||||
className="gf-form-input width-30"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
|
||||
type="text"
|
||||
@ -64,6 +65,7 @@ exports[`Render should render component 1`] = `
|
||||
</div>
|
||||
</SlideDown>
|
||||
<EmptyListCTA
|
||||
buttonDisabled={false}
|
||||
buttonIcon="users-alt"
|
||||
buttonTitle="Add Group"
|
||||
onClick={[Function]}
|
||||
@ -100,6 +102,7 @@ exports[`Render should render groups table 1`] = `
|
||||
/>
|
||||
<Button
|
||||
className="pull-right"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Icon
|
||||
@ -129,6 +132,7 @@ exports[`Render should render groups table 1`] = `
|
||||
>
|
||||
<Input
|
||||
className="gf-form-input width-30"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
|
||||
type="text"
|
||||
@ -183,6 +187,7 @@ exports[`Render should render groups table 1`] = `
|
||||
}
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
@ -207,6 +212,7 @@ exports[`Render should render groups table 1`] = `
|
||||
}
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
@ -231,6 +237,7 @@ exports[`Render should render groups table 1`] = `
|
||||
}
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
|
@ -17,7 +17,9 @@ exports[`Render should render group sync page 1`] = `
|
||||
<PageContents
|
||||
isLoading={true}
|
||||
>
|
||||
<Connect(TeamGroupSync) />
|
||||
<Connect(TeamGroupSync)
|
||||
isReadOnly={false}
|
||||
/>
|
||||
</PageContents>
|
||||
</Page>
|
||||
`;
|
||||
|
@ -16,12 +16,14 @@ exports[`Render should render component 1`] = `
|
||||
"permission": 0,
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<Component />
|
||||
</Form>
|
||||
</FieldSet>
|
||||
<SharedPreferences
|
||||
disabled={false}
|
||||
resourceUri="teams/1"
|
||||
/>
|
||||
</VerticalGroup>
|
||||
|
@ -4,17 +4,21 @@ import { TeamMember, ThunkResult } from 'app/types';
|
||||
import { updateNavIndex } from 'app/core/actions';
|
||||
import { buildNavModel } from './navModel';
|
||||
import { teamGroupsLoaded, teamLoaded, teamMembersLoaded, teamsLoaded } from './reducers';
|
||||
import { accessControlQueryParam } from 'app/core/utils/accessControl';
|
||||
|
||||
export function loadTeams(): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 });
|
||||
const response = await getBackendSrv().get(
|
||||
'/api/teams/search',
|
||||
accessControlQueryParam({ perpage: 1000, page: 1 })
|
||||
);
|
||||
dispatch(teamsLoaded(response.teams));
|
||||
};
|
||||
}
|
||||
|
||||
export function loadTeam(id: number): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
const response = await getBackendSrv().get(`/api/teams/${id}`);
|
||||
const response = await getBackendSrv().get(`/api/teams/${id}`, accessControlQueryParam());
|
||||
dispatch(teamLoaded(response));
|
||||
dispatch(updateNavIndex(buildNavModel(response)));
|
||||
};
|
||||
|
@ -1,8 +1,18 @@
|
||||
import { Team, TeamPermissionLevel } from 'app/types';
|
||||
import { AccessControlAction, Team, TeamPermissionLevel } from 'app/types';
|
||||
import { featureEnabled } from '@grafana/runtime';
|
||||
import { NavModelItem, NavModel } from '@grafana/data';
|
||||
import config from 'app/core/config';
|
||||
import { ProBadge } from 'app/core/components/Upgrade/ProBadge';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
const loadingTeam = {
|
||||
avatarUrl: 'public/img/user_profile.png',
|
||||
id: 1,
|
||||
name: 'Loading',
|
||||
email: 'loading',
|
||||
memberCount: 0,
|
||||
permission: TeamPermissionLevel.Member,
|
||||
};
|
||||
|
||||
export function buildNavModel(team: Team): NavModelItem {
|
||||
const navModel: NavModelItem = {
|
||||
@ -13,13 +23,8 @@ export function buildNavModel(team: Team): NavModelItem {
|
||||
text: team.name,
|
||||
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
||||
children: [
|
||||
{
|
||||
active: false,
|
||||
icon: 'users-alt',
|
||||
id: `team-members-${team.id}`,
|
||||
text: 'Members',
|
||||
url: `org/teams/edit/${team.id}/members`,
|
||||
},
|
||||
// With FGAC this tab will always be available (but not always editable)
|
||||
// With Legacy it will be hidden by hideTabsFromNonTeamAdmin should the user not be allowed to see it
|
||||
{
|
||||
active: false,
|
||||
icon: 'sliders-v-alt',
|
||||
@ -30,6 +35,22 @@ export function buildNavModel(team: Team): NavModelItem {
|
||||
],
|
||||
};
|
||||
|
||||
// While team is loading we leave the members tab
|
||||
// With FGAC the Members tab is available when user has ActionTeamsPermissionsRead for this team
|
||||
// With Legacy it will always be present
|
||||
if (
|
||||
team === loadingTeam ||
|
||||
contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team)
|
||||
) {
|
||||
navModel.children!.unshift({
|
||||
active: false,
|
||||
icon: 'users-alt',
|
||||
id: `team-members-${team.id}`,
|
||||
text: 'Members',
|
||||
url: `org/teams/edit/${team.id}/members`,
|
||||
});
|
||||
}
|
||||
|
||||
const teamGroupSync = {
|
||||
active: false,
|
||||
icon: 'sync',
|
||||
@ -38,7 +59,13 @@ export function buildNavModel(team: Team): NavModelItem {
|
||||
url: `org/teams/edit/${team.id}/groupsync`,
|
||||
};
|
||||
|
||||
if (featureEnabled('teamsync')) {
|
||||
// With both Legacy and FGAC the tab is protected being featureEnabled
|
||||
// While team is loading we leave the teamsync tab
|
||||
// With FGAC the External Group Sync tab is available when user has ActionTeamsPermissionsRead for this team
|
||||
if (
|
||||
featureEnabled('teamsync') &&
|
||||
(team === loadingTeam || contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team))
|
||||
) {
|
||||
navModel.children!.push(teamGroupSync);
|
||||
} else if (config.featureToggles.featureHighlights) {
|
||||
navModel.children!.push({ ...teamGroupSync, tabSuffix: ProBadge });
|
||||
@ -48,14 +75,7 @@ export function buildNavModel(team: Team): NavModelItem {
|
||||
}
|
||||
|
||||
export function getTeamLoadingNav(pageName: string): NavModel {
|
||||
const main = buildNavModel({
|
||||
avatarUrl: 'public/img/user_profile.png',
|
||||
id: 1,
|
||||
name: 'Loading',
|
||||
email: 'loading',
|
||||
memberCount: 0,
|
||||
permission: TeamPermissionLevel.Member,
|
||||
});
|
||||
const main = buildNavModel(loadingTeam);
|
||||
|
||||
let node: NavModelItem;
|
||||
|
||||
|
@ -207,18 +207,29 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
},
|
||||
{
|
||||
path: '/org/teams',
|
||||
roles: () => (config.editorsCanAdmin ? [] : ['Editor', 'Admin']),
|
||||
roles: () =>
|
||||
contextSrv.evaluatePermission(
|
||||
() => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']),
|
||||
[AccessControlAction.ActionTeamsRead]
|
||||
),
|
||||
component: SafeDynamicImport(() => import(/* webpackChunkName: "TeamList" */ 'app/features/teams/TeamList')),
|
||||
},
|
||||
{
|
||||
path: '/org/teams/new',
|
||||
|
||||
roles: () => (config.editorsCanAdmin ? [] : ['Admin']),
|
||||
roles: () =>
|
||||
contextSrv.evaluatePermission(
|
||||
() => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']),
|
||||
[AccessControlAction.ActionTeamsCreate]
|
||||
),
|
||||
component: SafeDynamicImport(() => import(/* webpackChunkName: "CreateTeam" */ 'app/features/teams/CreateTeam')),
|
||||
},
|
||||
{
|
||||
path: '/org/teams/edit/:id/:page?',
|
||||
roles: () => (config.editorsCanAdmin ? [] : ['Admin']),
|
||||
roles: () =>
|
||||
contextSrv.evaluatePermission(
|
||||
() => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']),
|
||||
[AccessControlAction.ActionTeamsWrite, AccessControlAction.ActionTeamsPermissionsWrite]
|
||||
),
|
||||
component: SafeDynamicImport(() => import(/* webpackChunkName: "TeamPages" */ 'app/features/teams/TeamPages')),
|
||||
},
|
||||
{
|
||||
|
@ -50,6 +50,11 @@ export enum AccessControlAction {
|
||||
ActionServerStatsRead = 'server.stats:read',
|
||||
|
||||
ActionTeamsCreate = 'teams:create',
|
||||
ActionTeamsDelete = 'teams:delete',
|
||||
ActionTeamsRead = 'teams:read',
|
||||
ActionTeamsWrite = 'teams:write',
|
||||
ActionTeamsPermissionsRead = 'teams.permissions:read',
|
||||
ActionTeamsPermissionsWrite = 'teams.permissions:write',
|
||||
|
||||
ActionRolesList = 'roles:list',
|
||||
ActionBuiltinRolesList = 'roles.builtin:list',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { WithAccessControlMetadata } from '@grafana/data';
|
||||
import { TeamPermissionLevel } from './acl';
|
||||
|
||||
export interface Team {
|
||||
export interface Team extends WithAccessControlMetadata {
|
||||
id: number;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
|
Loading…
Reference in New Issue
Block a user