Skip to content

Commit

Permalink
frontend: fix remaining issues of #1236, add success/error toasts, fi…
Browse files Browse the repository at this point in the history
…x redirect, autocomplete inferred principals as well in role assignments
  • Loading branch information
rikimaru0345 committed May 13, 2024
1 parent bb0426b commit 1755773
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 57 deletions.
133 changes: 84 additions & 49 deletions frontend/src/components/pages/acls/RoleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
* by the Apache License, Version 2.0
*/

import { Box, Button, Flex, FormField, Heading, HStack, Input, isSingleValue, Select, Tag, TagCloseButton, TagLabel } from '@redpanda-data/ui';
import { Box, Button, Flex, FormField, Heading, HStack, Input, isSingleValue, Select, Tag, TagCloseButton, TagLabel, useToast } from '@redpanda-data/ui';
import { useEffect, useMemo, useState } from 'react';
import { AclPrincipalGroup, ClusterACLs, ConsumerGroupACLs, createEmptyClusterAcl, createEmptyConsumerGroupAcl, createEmptyTopicAcl, createEmptyTransactionalIdAcl, TopicACLs, TransactionalIdACLs, unpackPrincipalGroup } from './Models';
import { AclPrincipalGroup, ClusterACLs, ConsumerGroupACLs, createEmptyClusterAcl, createEmptyConsumerGroupAcl, createEmptyTopicAcl, createEmptyTransactionalIdAcl, principalGroupsView, TopicACLs, TransactionalIdACLs, unpackPrincipalGroup } from './Models';
import { observer, useLocalObservable } from 'mobx-react';
import { ResourceACLsEditor } from './PrincipalGroupEditor';
import { api, RolePrincipal, rolesApi } from '../../../state/backendApi';
Expand Down Expand Up @@ -53,6 +53,8 @@ export const RoleForm = observer(({ initialData }: RoleFormProps) => {


const [isFormValid, setIsFormValid] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const toast = useToast();

const originalUsernames = useMemo(() => initialData?.principals?.map(({ name }) => name) ?? [], [
initialData?.principals
Expand All @@ -66,53 +68,72 @@ export const RoleForm = observer(({ initialData }: RoleFormProps) => {
<Box>
<form onSubmit={async (e) => {
e.preventDefault()
try {
setIsLoading(true);
const usersToRemove = originalUsernames.filter(item => currentUsernames.indexOf(item) === -1)

const principalType: AclStrResourceType = 'RedpandaRole'

if (editMode) {
await api.deleteACLs({
resourceType: 'Any',
resourceName: undefined,
principal: `${principalType}:${formState.roleName}`,
resourcePatternType: 'Any',
operation: 'Any',
permissionType: 'Any'
})
}

const usersToRemove = originalUsernames.filter(item => currentUsernames.indexOf(item) === -1)

const principalType: AclStrResourceType = 'RedpandaRole'
const aclPrincipalGroup: AclPrincipalGroup = {
principalType: 'RedpandaRole',
principalName: formState.roleName,

if (editMode) {
await api.deleteACLs({
resourceType: 'Any',
resourceName: undefined,
principal: `${principalType}:${formState.roleName}`,
resourcePatternType: 'Any',
operation: 'Any',
permissionType: 'Any'
})
}
host: formState.host,

const aclPrincipalGroup: AclPrincipalGroup = {
principalType: 'RedpandaRole',
principalName: formState.roleName,

host: formState.host,
topicAcls: formState.topicACLs,
consumerGroupAcls: formState.consumerGroupsACLs,
transactionalIdAcls: formState.transactionalIDACLs,
clusterAcls: formState.clusterACLs,
sourceEntries: []
}

topicAcls: formState.topicACLs,
consumerGroupAcls: formState.consumerGroupsACLs,
transactionalIdAcls: formState.transactionalIDACLs,
clusterAcls: formState.clusterACLs,
sourceEntries: []
const newRole = await rolesApi.updateRoleMembership(
formState.roleName,
formState.principals.map(x => x.name), usersToRemove, true
)

unpackPrincipalGroup(aclPrincipalGroup).forEach((async x => {
await api.createACL({
host: x.host,
principal: x.principal,
resourceType: x.resourceType,
resourceName: x.resourceName,
resourcePatternType: x.resourcePatternType as unknown as 'Literal' | 'Prefixed',
operation: x.operation as unknown as Exclude<AclStrOperation, 'Unknown' | 'Any'>,
permissionType: x.permissionType as unknown as 'Allow' | 'Deny'
})
}))

setIsLoading(false);
toast({
status: 'success',
title: `Role ${newRole.roleName} successfully ${editMode ? 'updated' : 'created'}`
});

history.push(`/security/roles/${newRole.roleName}/details`);
}
catch (err) {
toast({
status: 'error', duration: null, isClosable: true,
title: `Failed to update role ${formState.roleName}`,
description: String(err),
});
}
finally {
setIsLoading(false);
}

const newRole = await rolesApi.updateRoleMembership(
formState.roleName,
formState.principals.map(x => x.name), usersToRemove, true
)

unpackPrincipalGroup(aclPrincipalGroup).forEach((async x => {
await api.createACL({
host: x.host,
principal: x.principal,
resourceType: x.resourceType,
resourceName: x.resourceName,
resourcePatternType: x.resourcePatternType as unknown as 'Literal' | 'Prefixed',
operation: x.operation as unknown as Exclude<AclStrOperation, 'Unknown' | 'Any'>,
permissionType: x.permissionType as unknown as 'Allow' | 'Deny'
})
}))

void history.push(`/security/roles/${newRole.roleName}/details`);
}}>
<Flex gap={10} flexDirection="column">
<Flex flexDirection="row" gap={20}>
Expand Down Expand Up @@ -226,10 +247,10 @@ export const RoleForm = observer(({ initialData }: RoleFormProps) => {

<Flex gap={4} mt={8}>
{editMode ?
<Button colorScheme="brand" type="submit" loadingText="Editing..." isDisabled={roleNameAlreadyExist || !isFormValid}>
<Button colorScheme="brand" type="submit" loadingText="Editing..." isLoading={isLoading} isDisabled={roleNameAlreadyExist || !isFormValid}>
Update
</Button>
: <Button colorScheme="brand" type="submit" loadingText="Creating..." isDisabled={roleNameAlreadyExist || !isFormValid}>
: <Button colorScheme="brand" type="submit" loadingText="Creating..." isLoading={isLoading} isDisabled={roleNameAlreadyExist || !isFormValid}>
Create
</Button>}
{editMode ? <Button variant="link" onClick={() => {
Expand All @@ -256,7 +277,23 @@ const PrincipalSelector = observer((p: { state: RolePrincipal[] }) => {
void api.refreshServiceAccounts();
}, []);

const state = p.state
const state = p.state;

const availableUsers = api.serviceAccounts?.users.map((u) => ({
value: u,
})) ?? [];

// Add all inferred users
// In addition, find all principals that are referenced by roles, or acls, that are not service accounts
for (const g of principalGroupsView.principalGroups)
if (g.principalType == 'User' && !g.principalName.includes('*')) // is it a user that is being referenced?
if (!availableUsers.any(u => u.value == g.principalName)) // is the user already listed as a service account?
availableUsers.push({ value: g.principalName });

for (const [_, roleMembers] of rolesApi.roleMembers)
for (const roleMember of roleMembers)
if (!availableUsers.any(u => u.value == roleMember.name)) // make sure that user isn't already in the list
availableUsers.push({ value: roleMember.name });


return <Flex direction="column" gap={4}>
Expand All @@ -266,9 +303,7 @@ const PrincipalSelector = observer((p: { state: RolePrincipal[] }) => {
inputValue={searchValue}
onInputChange={setSearchValue}
isMulti={false}
options={api.serviceAccounts?.users.map((u) => ({
value: u,
})) ?? []}
options={availableUsers}

creatable={true}

Expand Down
30 changes: 22 additions & 8 deletions frontend/src/components/pages/acls/UserEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ class UserEditPage extends PageComponent<{ userName: string; }> {
const hasChanges = addedRoles.length > 0 || removedRoles.length > 0

const onSave = async () => {
if (Features.rolesApi) {
if (!Features.rolesApi)
return;
try {
this.isSaving = true;

const promises: Promise<UpdateRoleMembershipResponse>[] = [];

// Remove user from "removedRoles"
Expand All @@ -110,13 +114,23 @@ class UserEditPage extends PageComponent<{ userName: string; }> {
// Update roles and memberships so that the change is reflected in the ui
await rolesApi.refreshRoles();
await rolesApi.refreshRoleMembers();
}

toast({
status: 'success',
title: `${addedRoles.length} roles added, ${removedRoles.length} removed from user ${userName}`
});
appGlobal.history.push(`/security/users/${userName}/details`);
this.isSaving = false;

toast({
status: 'success',
title: `${addedRoles.length} roles added, ${removedRoles.length} removed from user ${userName}`
});
appGlobal.history.push(`/security/users/${userName}/details`);
} catch (err) {
toast({
status: 'error', duration: null, isClosable: true,
title: `Failed to update user ${userName}`,
description: String(err),
});
} finally {
this.isSaving = false;
}
};

return <>
Expand All @@ -133,7 +147,7 @@ class UserEditPage extends PageComponent<{ userName: string; }> {
{Features.rolesApi && <>
<Heading as="h3" mt="4">Assignments</Heading>
<RoleSelector state={this.selectedRoles} />
</>
</>
}

<Flex gap={4} mt={8}>
Expand Down

0 comments on commit 1755773

Please sign in to comment.