mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Service Accounts: Polish service account detail page (#45846)
* ServiceAccounts: add teams to service account DTO * ServiceAccounts: Add team display to service accounts * ServiceAccounts: add AC metadata to detail route * ServiceAccounts: add role picker to detail page * ServiceAccounts: Add role to profile DTO * ServiceAccounts: remove wip mention of created by
This commit is contained in:
parent
2334b98802
commit
14ec0cbd3b
@ -197,8 +197,10 @@ func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saIDString := strconv.FormatInt(serviceAccount.Id, 10)
|
||||||
|
metadata := api.getAccessControlMetadata(ctx, map[string]bool{saIDString: true})
|
||||||
serviceAccount.AvatarUrl = dtos.GetGravatarUrlWithDefault("", serviceAccount.Name)
|
serviceAccount.AvatarUrl = dtos.GetGravatarUrlWithDefault("", serviceAccount.Name)
|
||||||
|
serviceAccount.AccessControl = metadata[saIDString]
|
||||||
return response.JSON(http.StatusOK, serviceAccount)
|
return response.JSON(http.StatusOK, serviceAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +178,18 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o
|
|||||||
return nil, serviceaccounts.ErrServiceAccountNotFound
|
return nil, serviceaccounts.ErrServiceAccountNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Teams of service account. Can be optimized by combining with the query above
|
||||||
|
// in refactor
|
||||||
|
getTeamQuery := models.GetTeamsByUserQuery{UserId: serviceAccountID, OrgId: orgID}
|
||||||
|
if err := s.sqlStore.GetTeamsByUser(ctx, &getTeamQuery); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
teams := make([]string, len(getTeamQuery.Result))
|
||||||
|
|
||||||
|
for i := range getTeamQuery.Result {
|
||||||
|
teams[i] = getTeamQuery.Result[i].Name
|
||||||
|
}
|
||||||
|
|
||||||
saProfile := &serviceaccounts.ServiceAccountProfileDTO{
|
saProfile := &serviceaccounts.ServiceAccountProfileDTO{
|
||||||
Id: query.Result[0].UserId,
|
Id: query.Result[0].UserId,
|
||||||
Name: query.Result[0].Name,
|
Name: query.Result[0].Name,
|
||||||
@ -185,6 +197,8 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o
|
|||||||
OrgId: query.Result[0].OrgId,
|
OrgId: query.Result[0].OrgId,
|
||||||
UpdatedAt: query.Result[0].Updated,
|
UpdatedAt: query.Result[0].Updated,
|
||||||
CreatedAt: query.Result[0].Created,
|
CreatedAt: query.Result[0].Created,
|
||||||
|
Role: query.Result[0].Role,
|
||||||
|
Teams: teams,
|
||||||
}
|
}
|
||||||
return saProfile, nil
|
return saProfile, nil
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
@ -76,7 +77,25 @@ func TestStore_RetrieveServiceAccount(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, c.user.Login, dto.Login)
|
require.Equal(t, c.user.Login, dto.Login)
|
||||||
|
require.Len(t, dto.Teams, 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func TestStore_RetrieveServiceAccountWithTeams(t *testing.T) {
|
||||||
|
userToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true}
|
||||||
|
db, store := setupTestDatabase(t)
|
||||||
|
user := tests.SetupUserServiceAccount(t, db, userToCreate)
|
||||||
|
|
||||||
|
team, err := store.sqlStore.CreateTeam("serviceTeam", "serviceTeam", user.OrgId)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.sqlStore.AddTeamMember(user.Id, user.OrgId, team.Id, false, models.PERMISSION_VIEW)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dto, err := store.RetrieveServiceAccount(context.Background(), user.OrgId, user.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, userToCreate.Login, dto.Login)
|
||||||
|
require.Len(t, dto.Teams, 1)
|
||||||
|
require.Equal(t, "serviceTeam", dto.Teams[0])
|
||||||
|
}
|
||||||
|
@ -53,5 +53,7 @@ type ServiceAccountProfileDTO struct {
|
|||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
AvatarUrl string `json:"avatarUrl"`
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Teams []string `json:"teams"`
|
||||||
AccessControl map[string]bool `json:"accessControl,omitempty"`
|
AccessControl map[string]bool `json:"accessControl,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -3,24 +3,29 @@ import { connect, ConnectedProps } from 'react-redux';
|
|||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { ServiceAccountProfile } from './ServiceAccountProfile';
|
import { ServiceAccountProfile } from './ServiceAccountProfile';
|
||||||
import { StoreState, ServiceAccountDTO, ApiKey } from 'app/types';
|
import { StoreState, ServiceAccountDTO, ApiKey, Role } from 'app/types';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
import {
|
import {
|
||||||
deleteServiceAccountToken,
|
deleteServiceAccountToken,
|
||||||
loadServiceAccount,
|
loadServiceAccount,
|
||||||
loadServiceAccountTokens,
|
loadServiceAccountTokens,
|
||||||
createServiceAccountToken,
|
createServiceAccountToken,
|
||||||
|
fetchACOptions,
|
||||||
|
updateServiceAccount,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
import { ServiceAccountTokensTable } from './ServiceAccountTokensTable';
|
import { ServiceAccountTokensTable } from './ServiceAccountTokensTable';
|
||||||
import { getTimeZone, NavModel } from '@grafana/data';
|
import { getTimeZone, NavModel } from '@grafana/data';
|
||||||
import { Button, VerticalGroup } from '@grafana/ui';
|
import { Button, VerticalGroup } from '@grafana/ui';
|
||||||
import { CreateTokenModal } from './CreateTokenModal';
|
import { CreateTokenModal } from './CreateTokenModal';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> {
|
interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
serviceAccount?: ServiceAccountDTO;
|
serviceAccount?: ServiceAccountDTO;
|
||||||
tokens: ApiKey[];
|
tokens: ApiKey[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
roleOptions: Role[];
|
||||||
|
builtInRoles: Record<string, Role[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState) {
|
function mapStateToProps(state: StoreState) {
|
||||||
@ -29,6 +34,8 @@ function mapStateToProps(state: StoreState) {
|
|||||||
serviceAccount: state.serviceAccountProfile.serviceAccount,
|
serviceAccount: state.serviceAccountProfile.serviceAccount,
|
||||||
tokens: state.serviceAccountProfile.tokens,
|
tokens: state.serviceAccountProfile.tokens,
|
||||||
isLoading: state.serviceAccountProfile.isLoading,
|
isLoading: state.serviceAccountProfile.isLoading,
|
||||||
|
roleOptions: state.serviceAccounts.roleOptions,
|
||||||
|
builtInRoles: state.serviceAccounts.builtInRoles,
|
||||||
timezone: getTimeZone(state.user),
|
timezone: getTimeZone(state.user),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -37,6 +44,7 @@ const mapDispatchToProps = {
|
|||||||
loadServiceAccountTokens,
|
loadServiceAccountTokens,
|
||||||
createServiceAccountToken,
|
createServiceAccountToken,
|
||||||
deleteServiceAccountToken,
|
deleteServiceAccountToken,
|
||||||
|
fetchACOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
@ -49,10 +57,13 @@ const ServiceAccountPageUnconnected = ({
|
|||||||
tokens,
|
tokens,
|
||||||
timezone,
|
timezone,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
roleOptions,
|
||||||
|
builtInRoles,
|
||||||
loadServiceAccount,
|
loadServiceAccount,
|
||||||
loadServiceAccountTokens,
|
loadServiceAccountTokens,
|
||||||
createServiceAccountToken,
|
createServiceAccountToken,
|
||||||
deleteServiceAccountToken,
|
deleteServiceAccountToken,
|
||||||
|
fetchACOptions,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [newToken, setNewToken] = useState('');
|
const [newToken, setNewToken] = useState('');
|
||||||
@ -61,7 +72,10 @@ const ServiceAccountPageUnconnected = ({
|
|||||||
const serviceAccountId = parseInt(match.params.id, 10);
|
const serviceAccountId = parseInt(match.params.id, 10);
|
||||||
loadServiceAccount(serviceAccountId);
|
loadServiceAccount(serviceAccountId);
|
||||||
loadServiceAccountTokens(serviceAccountId);
|
loadServiceAccountTokens(serviceAccountId);
|
||||||
}, [match, loadServiceAccount, loadServiceAccountTokens]);
|
if (contextSrv.accessControlEnabled()) {
|
||||||
|
fetchACOptions();
|
||||||
|
}
|
||||||
|
}, [match, loadServiceAccount, loadServiceAccountTokens, fetchACOptions]);
|
||||||
|
|
||||||
const onDeleteServiceAccountToken = (key: ApiKey) => {
|
const onDeleteServiceAccountToken = (key: ApiKey) => {
|
||||||
deleteServiceAccountToken(parseInt(match.params.id, 10), key.id!);
|
deleteServiceAccountToken(parseInt(match.params.id, 10), key.id!);
|
||||||
@ -76,6 +90,12 @@ const ServiceAccountPageUnconnected = ({
|
|||||||
setNewToken('');
|
setNewToken('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRoleChange = (role: OrgRole, serviceAccount: ServiceAccountDTO) => {
|
||||||
|
const updatedServiceAccount = { ...serviceAccount, role: role };
|
||||||
|
|
||||||
|
updateServiceAccount(updatedServiceAccount);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel}>
|
<Page navModel={navModel}>
|
||||||
<Page.Contents isLoading={isLoading}>
|
<Page.Contents isLoading={isLoading}>
|
||||||
@ -96,6 +116,9 @@ const ServiceAccountPageUnconnected = ({
|
|||||||
onServiceAccountEnable={() => {
|
onServiceAccountEnable={() => {
|
||||||
console.log(`not implemented`);
|
console.log(`not implemented`);
|
||||||
}}
|
}}
|
||||||
|
onRoleChange={onRoleChange}
|
||||||
|
roleOptions={roleOptions}
|
||||||
|
builtInRoles={builtInRoles}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React, { PureComponent, useRef, useState } from 'react';
|
import React, { PureComponent, useRef, useState } from 'react';
|
||||||
import { ServiceAccountDTO } from 'app/types';
|
import { Role, ServiceAccountDTO } from 'app/types';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { dateTimeFormat, GrafanaTheme, TimeZone } from '@grafana/data';
|
import { dateTimeFormat, GrafanaTheme, OrgRole, TimeZone } from '@grafana/data';
|
||||||
import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, stylesFactory } from '@grafana/ui';
|
import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, stylesFactory } from '@grafana/ui';
|
||||||
|
import { ServiceAccountRoleRow } from './ServiceAccountRoleRow';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceAccount: ServiceAccountDTO;
|
serviceAccount: ServiceAccountDTO;
|
||||||
@ -13,6 +14,10 @@ interface Props {
|
|||||||
onServiceAccountDelete: (serviceAccountId: number) => void;
|
onServiceAccountDelete: (serviceAccountId: number) => void;
|
||||||
onServiceAccountDisable: (serviceAccountId: number) => void;
|
onServiceAccountDisable: (serviceAccountId: number) => void;
|
||||||
onServiceAccountEnable: (serviceAccountId: number) => void;
|
onServiceAccountEnable: (serviceAccountId: number) => void;
|
||||||
|
|
||||||
|
onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void;
|
||||||
|
roleOptions: Role[];
|
||||||
|
builtInRoles: Record<string, Role[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceAccountProfile({
|
export function ServiceAccountProfile({
|
||||||
@ -22,6 +27,9 @@ export function ServiceAccountProfile({
|
|||||||
onServiceAccountDelete,
|
onServiceAccountDelete,
|
||||||
onServiceAccountDisable,
|
onServiceAccountDisable,
|
||||||
onServiceAccountEnable,
|
onServiceAccountEnable,
|
||||||
|
onRoleChange,
|
||||||
|
roleOptions,
|
||||||
|
builtInRoles,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [showDisableModal, setShowDisableModal] = useState(false);
|
const [showDisableModal, setShowDisableModal] = useState(false);
|
||||||
@ -73,9 +81,14 @@ export function ServiceAccountProfile({
|
|||||||
onChange={onServiceAccountNameChange}
|
onChange={onServiceAccountNameChange}
|
||||||
/>
|
/>
|
||||||
<ServiceAccountProfileRow label="ID" value={serviceAccount.login} />
|
<ServiceAccountProfileRow label="ID" value={serviceAccount.login} />
|
||||||
<ServiceAccountProfileRow label="Roles" value="WIP" />
|
<ServiceAccountRoleRow
|
||||||
<ServiceAccountProfileRow label="Teams" value="WIP" />
|
label="Roles"
|
||||||
<ServiceAccountProfileRow label="Created by" value="WIP" />
|
serviceAccount={serviceAccount}
|
||||||
|
onRoleChange={onRoleChange}
|
||||||
|
builtInRoles={builtInRoles}
|
||||||
|
roleOptions={roleOptions}
|
||||||
|
/>
|
||||||
|
<ServiceAccountProfileRow label="Teams" value={serviceAccount.teams.join(', ')} />
|
||||||
<ServiceAccountProfileRow
|
<ServiceAccountProfileRow
|
||||||
label="Creation date"
|
label="Creation date"
|
||||||
value={dateTimeFormat(serviceAccount.createdAt, { timeZone })}
|
value={dateTimeFormat(serviceAccount.createdAt, { timeZone })}
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { AccessControlAction, OrgRole, Role, ServiceAccountDTO } from 'app/types';
|
||||||
|
import { OrgRolePicker } from '../admin/OrgRolePicker';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
serviceAccount: ServiceAccountDTO;
|
||||||
|
onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void;
|
||||||
|
roleOptions: Role[];
|
||||||
|
builtInRoles: Record<string, Role[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServiceAccountRoleRow extends PureComponent<Props> {
|
||||||
|
render() {
|
||||||
|
const { label, serviceAccount, roleOptions, builtInRoles, onRoleChange } = this.props;
|
||||||
|
const canUpdateRole = contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount);
|
||||||
|
const rolePickerDisabled = !canUpdateRole;
|
||||||
|
const labelClass = cx(
|
||||||
|
'width-16',
|
||||||
|
css`
|
||||||
|
font-weight: 500;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputId = `${label}-input`;
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className={labelClass}>
|
||||||
|
<label htmlFor={inputId}>{label}</label>
|
||||||
|
</td>
|
||||||
|
<td className="width-25" colSpan={2}>
|
||||||
|
{contextSrv.licensedAccessControlEnabled() ? (
|
||||||
|
<UserRolePicker
|
||||||
|
userId={serviceAccount.id}
|
||||||
|
orgId={serviceAccount.orgId}
|
||||||
|
builtInRole={serviceAccount.role}
|
||||||
|
onBuiltinRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
||||||
|
roleOptions={roleOptions}
|
||||||
|
builtInRoles={builtInRoles}
|
||||||
|
disabled={rolePickerDisabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<OrgRolePicker
|
||||||
|
aria-label="Role"
|
||||||
|
value={serviceAccount.role}
|
||||||
|
disabled={!canUpdateRole}
|
||||||
|
onChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -39,7 +39,7 @@ export function setServiceAccountToRemove(serviceAccount: ServiceAccountDTO | nu
|
|||||||
export function loadServiceAccount(saID: number): ThunkResult<void> {
|
export function loadServiceAccount(saID: number): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
const response = await getBackendSrv().get(`${BASE_URL}/${saID}`);
|
const response = await getBackendSrv().get(`${BASE_URL}/${saID}`, accessControlQueryParam());
|
||||||
dispatch(serviceAccountLoaded(response));
|
dispatch(serviceAccountLoaded(response));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -33,6 +33,7 @@ export interface ServiceAccountDTO extends WithAccessControlMetadata {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
teams: string[];
|
||||||
role: OrgRole;
|
role: OrgRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user