Skip to content

feat: manage schema object permissions #2398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/components/SubjectWithAvatar/SubjectWithAvatar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.ydb-subject-with-avatar {
&__avatar-wrapper {
position: relative;
}

&__subject {
overflow: hidden;

text-overflow: ellipsis;
}
}
34 changes: 34 additions & 0 deletions src/components/SubjectWithAvatar/SubjectWithAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Avatar, Flex, Text} from '@gravity-ui/uikit';

import {cn} from '../../utils/cn';

export const block = cn('ydb-subject-with-avatar');

import './SubjectWithAvatar.scss';

interface SubjectProps {
subject: string;
title?: string;
renderIcon?: () => React.ReactNode;
}

export function SubjectWithAvatar({subject, title, renderIcon}: SubjectProps) {
return (
<Flex gap={2} alignItems="center">
<div className={block('avatar-wrapper')}>
<Avatar theme="brand" text={subject} title={subject} />
{renderIcon?.()}
</div>
<Flex direction="column" overflow="hidden">
<Text variant="body-2" className={block('subject')}>
{subject}
</Text>
{title && (
<Text variant="body-2" color="secondary">
{title}
</Text>
)}
</Flex>
</Flex>
);
}
27 changes: 0 additions & 27 deletions src/containers/Tenant/Acl/Acl.scss

This file was deleted.

222 changes: 23 additions & 199 deletions src/containers/Tenant/Acl/Acl.tsx
Original file line number Diff line number Diff line change
@@ -1,208 +1,32 @@
import React from 'react';
import {Button, Flex, Icon, Text} from '@gravity-ui/uikit';

import {DefinitionList} from '@gravity-ui/components';
import type {DefinitionListItem} from '@gravity-ui/components';
import {SquareCheck} from '@gravity-ui/icons';
import {Icon} from '@gravity-ui/uikit';

import {ResponseError} from '../../../components/Errors/ResponseError';
import {Loader} from '../../../components/Loader';
import {schemaAclApi} from '../../../store/reducers/schemaAcl/schemaAcl';
import type {TACE} from '../../../types/api/acl';
import {valueIsDefined} from '../../../utils';
import {cn} from '../../../utils/cn';
import {
TENANT_DIAGNOSTICS_TABS_IDS,
TENANT_PAGES_IDS,
} from '../../../store/reducers/tenant/constants';
import {setDiagnosticsTab, setTenantPage} from '../../../store/reducers/tenant/tenant';
import {useTypedDispatch} from '../../../utils/hooks';

import i18n from './i18n';

import './Acl.scss';

const b = cn('ydb-acl');

const prepareLogin = (value: string | undefined) => {
if (value && value.endsWith('@staff') && !value.startsWith('svc_')) {
const login = value.split('@')[0];
return login;
}

return value;
};

const aclParams = ['access', 'type', 'inheritance'] as const;

type AclParameter = (typeof aclParams)[number];

const aclParamToName: Record<AclParameter, string> = {
access: 'Access',
type: 'Access type',
inheritance: 'Inheritance type',
};

const defaultInheritanceType = ['Object', 'Container'];
const defaultAccessType = 'Allow';

const defaultInheritanceTypeSet = new Set(defaultInheritanceType);

function normalizeAcl(acl: TACE[]) {
return acl.map((ace) => {
const {AccessRules = [], AccessRights = [], AccessType, InheritanceType, Subject} = ace;
const access = AccessRules.concat(AccessRights);
//"Allow" is default access type. We want to show it only if it isn't default
const type = AccessType === defaultAccessType ? undefined : AccessType;
let inheritance;
// ['Object', 'Container'] - is default inheritance type. We want to show it only if it isn't default
if (
InheritanceType?.length !== defaultInheritanceTypeSet.size ||
InheritanceType.some((t) => !defaultInheritanceTypeSet.has(t))
) {
inheritance = InheritanceType;
}
return {
access: access.length ? access : undefined,
type,
inheritance,
Subject,
};
});
}
import ArrowRightFromSquareIcon from '@gravity-ui/icons/svgs/arrow-right-from-square.svg';

interface DefinitionValueProps {
value: string | string[];
}
export const Acl = () => {
const dispatch = useTypedDispatch();

function DefinitionValue({value}: DefinitionValueProps) {
const normalizedValue = typeof value === 'string' ? [value] : value;
return (
<div className={b('definition-content')}>
{normalizedValue.map((el) => (
<span key={el}>{el}</span>
))}
</div>
);
}

function getAclListItems(acl?: TACE[]): DefinitionListItem[] {
if (!acl || !acl.length) {
return [];
}

const normalizedAcl = normalizeAcl(acl);

return normalizedAcl.map(({Subject, ...data}) => {
const definedDataEntries = Object.entries(data).filter(([_key, value]) =>
Boolean(value),
) as [AclParameter, string | string[]][];

if (definedDataEntries.length === 1 && definedDataEntries[0][0] === 'access') {
return {
name: Subject,
content: <DefinitionValue value={definedDataEntries[0][1]} />,
multilineName: true,
};
}
return {
label: <span className={b('group-label')}>{Subject}</span>,
items: aclParams
.map((key) => {
const value = data[key];
if (value) {
return {
name: aclParamToName[key],
content: <DefinitionValue value={value} />,
multilineName: true,
};
}
return undefined;
})
.filter(valueIsDefined),
};
});
}

function getOwnerItem(owner?: string): DefinitionListItem[] {
const preparedOwner = prepareLogin(owner);
if (!preparedOwner) {
return [];
}
return [
{
name: preparedOwner,
content: i18n('title_owner'),
multilineName: true,
},
];
}

function getInterruptInheritanceItem(flag?: boolean): DefinitionListItem[] {
if (!flag) {
return [];
}
return [
{
name: i18n('title_interupt-inheritance'),
content: <Icon data={SquareCheck} size={20} />,
multilineName: true,
},
];
}

export const Acl = ({path, database}: {path: string; database: string}) => {
const {currentData, isFetching, error} = schemaAclApi.useGetSchemaAclQuery({path, database});

const loading = isFetching && !currentData;

const {acl, effectiveAcl, owner, interruptInheritance} = currentData || {};

const aclListItems = getAclListItems(acl);
const effectiveAclListItems = getAclListItems(effectiveAcl);

const ownerItem = getOwnerItem(owner);

const interruptInheritanceItem = getInterruptInheritanceItem(interruptInheritance);

if (loading) {
return <Loader />;
}

if (error) {
return <ResponseError error={error} />;
}

if (!acl && !owner && !effectiveAcl) {
return <React.Fragment>{i18n('description_empty')}</React.Fragment>;
}

const accessRightsItems = ownerItem.concat(aclListItems);

return (
<div className={b()}>
<AclDefinitionList items={interruptInheritanceItem} />
<AclDefinitionList items={accessRightsItems} title={i18n('title_rights')} />
<AclDefinitionList
items={effectiveAclListItems}
title={i18n('title_effective-rights')}
/>
</div>
<Flex gap={2} alignItems="center">
<Text variant="body-2">{i18n('description_section-moved')}</Text>
<Button
title={i18n('action-open-in-diagnostics')}
onClick={() => {
dispatch(setTenantPage(TENANT_PAGES_IDS.diagnostics));
dispatch(setDiagnosticsTab(TENANT_DIAGNOSTICS_TABS_IDS.access));
}}
size="s"
>
<Icon data={ArrowRightFromSquareIcon} size={14} />
</Button>
</Flex>
);
};

interface AclDefinitionListProps {
items: DefinitionListItem[];
title?: string;
}

function AclDefinitionList({items, title}: AclDefinitionListProps) {
if (!items.length) {
return null;
}
return (
<React.Fragment>
{title && <div className={b('list-title')}>{title}</div>}
<DefinitionList
items={items}
nameMaxWidth={200}
className={b('result', {'no-title': !title})}
responsive
/>
</React.Fragment>
);
}
7 changes: 2 additions & 5 deletions src/containers/Tenant/Acl/i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
"title_rights": "Access Rights",
"title_effective-rights": "Effective Access Rights",
"title_owner": "Owner",
"title_interupt-inheritance": "Interrupt inheritance",
"description_empty": "No Acl data"
"description_section-moved": "Section was moved to Diagnostics",
"action-open-in-diagnostics": "Open in Diagnostics"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
.ydb-access-rights {
$block: &;
&__header {
position: sticky;
left: 0;

margin-bottom: var(--g-spacing-3);
}
&__owner-card {
width: max-content;
padding: var(--g-spacing-2) var(--g-spacing-3);
}

&__icon-wrapper {
position: absolute;
right: -2px;
bottom: -2px;

height: 16px;

color: var(--g-color-base-warning-heavy);
border-radius: 50%;
background: var(--g-color-base-background);
aspect-ratio: 1;
}
&__owner-divider {
height: 24px;
}
&__owner-description {
max-width: 391px;
}
&__dialog-content-wrapper {
position: relative;

height: 46px;
}
&__dialog-error {
position: absolute;
bottom: 0;
left: 0;

overflow: hidden;

max-width: 100%;

white-space: nowrap;
text-overflow: ellipsis;
}

&__note {
display: flex;
.g-help-mark__button {
display: flex;
}
}
&__rights-wrapper {
position: relative;

width: 100%;
height: 100%;
}
&__rights-actions {
position: absolute;
right: 0;

visibility: hidden;

height: 100%;
padding-left: var(--g-spacing-2);

background-color: var(--ydb-data-table-color-hover);
}
&__rights-table {
.data-table__row:hover {
#{$block}__rights-actions {
visibility: visible;
}
}
}
}
Loading
Loading