From 733b7f424079730e422b5c101ca158f35294948a Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 22 Jul 2024 13:40:29 -0800 Subject: [PATCH 1/6] Ignore organization ID in audit logs --- docs/admin/audit-logs.md | 4 ++-- enterprise/audit/table.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 40eb173cad869..87f07cf243125 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -13,8 +13,8 @@ We track the following resources: | APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| | AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| | Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| -| AuditableOrganizationMember
|
FieldTracked
created_attrue
organization_idtrue
rolestrue
updated_attrue
user_idtrue
usernametrue
| -| CustomRole
|
FieldTracked
created_atfalse
display_nametrue
idfalse
nametrue
org_permissionstrue
organization_idtrue
site_permissionstrue
updated_atfalse
user_permissionstrue
| +| AuditableOrganizationMember
|
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| +| CustomRole
|
FieldTracked
created_atfalse
display_nametrue
idfalse
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| | GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| | HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| | License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 3a0a1eb209ed9..0a3608dae7169 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -53,7 +53,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ &database.AuditableOrganizationMember{}: { "username": ActionTrack, "user_id": ActionTrack, - "organization_id": ActionTrack, + "organization_id": ActionIgnore, // Never changes. "created_at": ActionTrack, "updated_at": ActionTrack, "roles": ActionTrack, @@ -64,7 +64,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "site_permissions": ActionTrack, "org_permissions": ActionTrack, "user_permissions": ActionTrack, - "organization_id": ActionTrack, + "organization_id": ActionIgnore, // Never changes. "id": ActionIgnore, "created_at": ActionIgnore, From abcdd01ccc1e724438af4b1754e50eed5168d9ca Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 22 Jul 2024 14:18:12 -0800 Subject: [PATCH 2/6] Add organization to audit log rows --- .../pages/AuditPage/AuditLogRow/AuditLogRow.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index 15e1a8e8254b4..6a27bdff375e7 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -1,7 +1,9 @@ import type { CSSObject, Interpolation, Theme } from "@emotion/react"; import Collapse from "@mui/material/Collapse"; +import Link from "@mui/material/Link"; import TableCell from "@mui/material/TableCell"; import { type FC, useState } from "react"; +import { Link as RouterLink } from "react-router-dom"; import userAgentParser from "ua-parser-js"; import type { AuditLog } from "api/typesGenerated"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; @@ -132,6 +134,20 @@ export const AuditLogRow: FC = ({ )} + {auditLog.organization && ( + + <>Org: + + + {auditLog.organization.display_name || + auditLog.organization.name} + + + + )} Date: Mon, 22 Jul 2024 15:17:47 -0800 Subject: [PATCH 3/6] Add organizations filter to audit table --- site/src/pages/AuditPage/AuditFilter.tsx | 81 +++++++++++++++++++ site/src/pages/AuditPage/AuditPage.tsx | 16 +++- .../pages/AuditPage/AuditPageView.stories.tsx | 2 + 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 0127637a4b69d..a24a8261ff6d1 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -1,5 +1,6 @@ import capitalize from "lodash/capitalize"; import type { FC } from "react"; +import { API } from "api/api"; import { AuditActions, ResourceTypes } from "api/typesGenerated"; import { Filter, @@ -13,9 +14,11 @@ import { } from "components/Filter/menu"; import { SelectFilter, + SelectFilterSearch, type SelectFilterOption, } from "components/Filter/SelectFilter"; import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { docs } from "utils/docs"; const PRESET_FILTERS = [ @@ -42,6 +45,7 @@ interface AuditFilterProps { user: UserFilterMenu; action: ActionFilterMenu; resourceType: ResourceTypeFilterMenu; + organization: OrganizationsFilterMenu; }; } @@ -58,6 +62,7 @@ export const AuditFilter: FC = ({ filter, error, menus }) => { + } skeleton={ @@ -157,3 +162,79 @@ const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => { /> ); }; + +export const useOrganizationsFilterMenu = ({ + value, + onChange, +}: Pick, "value" | "onChange">) => { + return useFilterMenu({ + onChange, + value, + id: "organizations", + getSelectedOption: async () => { + if (value) { + const organizations = await API.getOrganizations(); + const organization = organizations.find((o) => o.name === value); + if (organization) { + return { + label: organization.display_name || organization.name, + value: organization.name, + startIcon: ( + + ), + }; + } + } + return null; + }, + getOptions: async () => { + const organizationsRes = await API.getOrganizations(); + return organizationsRes.map((organization) => ({ + label: organization.display_name || organization.name, + value: organization.name, + startIcon: ( + + ), + })); + }, + }); +}; + +export type OrganizationsFilterMenu = ReturnType< + typeof useOrganizationsFilterMenu +>; + +interface OrganizationsMenuProps { + menu: OrganizationsFilterMenu; +} + +export const OrganizationsMenu: FC = ({ menu }) => { + return ( + + } + /> + ); +}; diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index d014972ef13a8..7f6d6840fae76 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -8,7 +8,11 @@ import { isNonInitialPage } from "components/PaginationWidget/utils"; import { usePaginatedQuery } from "hooks/usePaginatedQuery"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { pageTitle } from "utils/page"; -import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter"; +import { + useActionFilterMenu, + useOrganizationsFilterMenu, + useResourceTypeFilterMenu, +} from "./AuditFilter"; import { AuditPageView } from "./AuditPageView"; const AuditPage: FC = () => { @@ -55,6 +59,15 @@ const AuditPage: FC = () => { }), }); + const organizationsMenu = useOrganizationsFilterMenu({ + value: filter.values.organization, + onChange: (option) => + filter.update({ + ...filter.values, + organization: option?.value, + }), + }); + return ( <> @@ -74,6 +87,7 @@ const AuditPage: FC = () => { user: userMenu, action: actionMenu, resourceType: resourceTypeMenu, + organization: organizationsMenu, }, }} /> diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index fa6ac9f17f066..5d29cc5928fcb 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -21,11 +21,13 @@ const defaultFilterProps = getDefaultFilterProps({ username: MockUser.username, action: undefined, resource_type: undefined, + organization: undefined, }, menus: { user: MockMenu, action: MockMenu, resourceType: MockMenu, + organization: MockMenu, }, }); From 8556f9286d62a0ae46f575a4bd4920efe73000df Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 22 Jul 2024 15:25:22 -0800 Subject: [PATCH 4/6] Decrease audit filter button widths Now that there are four items. --- site/src/components/Filter/SelectFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Filter/SelectFilter.tsx b/site/src/components/Filter/SelectFilter.tsx index 7521affc7efb6..a60fd4cb97d00 100644 --- a/site/src/components/Filter/SelectFilter.tsx +++ b/site/src/components/Filter/SelectFilter.tsx @@ -11,7 +11,7 @@ import { SelectMenuIcon, } from "components/SelectMenu/SelectMenu"; -const BASE_WIDTH = 200; +const BASE_WIDTH = 175; const POPOVER_WIDTH = 320; export type SelectFilterOption = { From 00ef7542a061b8b3a204041627b9377e36f157cc Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 22 Jul 2024 16:36:21 -0800 Subject: [PATCH 5/6] Add more audit mocks To test different org names and no org. --- .../pages/AuditPage/AuditPageView.stories.tsx | 9 +++- site/src/testHelpers/entities.ts | 50 +++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index 5d29cc5928fcb..904ab00c0a287 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -10,7 +10,12 @@ import { } from "components/PaginationWidget/PaginationContainer.mocks"; import type { UsePaginatedQueryResult } from "hooks/usePaginatedQuery"; import { chromaticWithTablet } from "testHelpers/chromatic"; -import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities"; +import { + MockAuditLog, + MockAuditLog2, + MockAuditLog3, + MockUser, +} from "testHelpers/entities"; import { AuditPageView } from "./AuditPageView"; type FilterProps = ComponentProps["filterProps"]; @@ -35,7 +40,7 @@ const meta: Meta = { title: "pages/AuditPage", component: AuditPageView, args: { - auditLogs: [MockAuditLog, MockAuditLog2], + auditLogs: [MockAuditLog, MockAuditLog2, MockAuditLog3], isAuditLogVisible: true, filterProps: defaultFilterProps, }, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 453f1455615ec..f69c400838052 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2211,6 +2211,9 @@ export const MockEntitlementsWithUserLimit: TypesGen.Entitlements = { export const MockExperiments: TypesGen.Experiment[] = []; +/** + * An audit log for MockOrganization. + */ export const MockAuditLog: TypesGen.AuditLog = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", request_id: "53bded77-7b9d-4e82-8771-991a34d759f9", @@ -2218,9 +2221,9 @@ export const MockAuditLog: TypesGen.AuditLog = { organization_id: MockOrganization.id, organization: { id: MockOrganization.id, - name: "mock name", - display_name: "mock display name", - icon: "/emojis/1f48f-1f3ff.png", + name: MockOrganization.name, + display_name: MockOrganization.display_name, + icon: MockOrganization.icon, }, ip: "127.0.0.1", user_agent: @@ -2245,12 +2248,22 @@ export const MockAuditLog: TypesGen.AuditLog = { is_deleted: false, }; +/** + * An audit log for MockOrganization2. + */ export const MockAuditLog2: TypesGen.AuditLog = { ...MockAuditLog, id: "53bded77-7b9d-4e82-8771-991a34d759f9", action: "write", time: "2022-05-20T16:45:57.122Z", description: "{user} updated workspace {target}", + organization_id: MockOrganization2.id, + organization: { + id: MockOrganization2.id, + name: MockOrganization2.name, + display_name: MockOrganization2.display_name, + icon: MockOrganization2.icon, + }, diff: { workspace_name: { old: "old-workspace-name", @@ -2275,6 +2288,37 @@ export const MockAuditLog2: TypesGen.AuditLog = { }, }; +/** + * An audit log without an organization. + */ +export const MockAuditLog3: TypesGen.AuditLog = { + id: "8efa9208-656a-422d-842d-b9dec0cf1bf3", + request_id: "57ee9510-8330-480d-9ffa-4024e5805465", + time: "2024-06-11T01:32:11.123Z", + organization_id: "00000000-0000-0000-000000000000", + ip: "127.0.0.1", + user_agent: + '"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"', + resource_type: "template", + resource_id: "a624458c-1562-4689-a671-42c0b7d2d0c5", + resource_target: "docker", + resource_icon: "", + action: "write", + diff: { + display_name: { + old: "old display", + new: "new display", + secret: false, + }, + }, + status_code: 200, + additional_fields: {}, + description: "{user} updated template {target}", + user: MockUser, + resource_link: "/templates/docker", + is_deleted: false, +}; + export const MockWorkspaceCreateAuditLogForDifferentOwner = { ...MockAuditLog, additional_fields: { From aff7321909dc2833e45038d48bbe6fdd5dd9b91e Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 23 Jul 2024 09:50:58 -0800 Subject: [PATCH 6/6] Check multi-org before adding org filter and details As part of this, make the width adjustable so we only lower it when we have the new filter added. --- site/src/components/Filter/SelectFilter.tsx | 8 ++-- site/src/components/Filter/UserFilter.tsx | 4 +- site/src/pages/AuditPage/AuditFilter.tsx | 38 +++++++++++++++---- .../AuditPage/AuditLogRow/AuditLogRow.tsx | 4 +- site/src/pages/AuditPage/AuditPage.tsx | 7 +++- .../pages/AuditPage/AuditPageView.stories.tsx | 17 ++++++++- site/src/pages/AuditPage/AuditPageView.tsx | 8 +++- 7 files changed, 70 insertions(+), 16 deletions(-) diff --git a/site/src/components/Filter/SelectFilter.tsx b/site/src/components/Filter/SelectFilter.tsx index a60fd4cb97d00..e679c3fbd7572 100644 --- a/site/src/components/Filter/SelectFilter.tsx +++ b/site/src/components/Filter/SelectFilter.tsx @@ -11,7 +11,7 @@ import { SelectMenuIcon, } from "components/SelectMenu/SelectMenu"; -const BASE_WIDTH = 175; +const BASE_WIDTH = 200; const POPOVER_WIDTH = 320; export type SelectFilterOption = { @@ -32,6 +32,7 @@ export type SelectFilterProps = { onSelect: (option: SelectFilterOption | undefined) => void; // SelectFilterSearch element selectFilterSearch?: ReactNode; + width?: number; }; export const SelectFilter: FC = ({ @@ -42,6 +43,7 @@ export const SelectFilter: FC = ({ placeholder, emptyText, selectFilterSearch, + width = BASE_WIDTH, }) => { const [open, setOpen] = useState(false); @@ -50,7 +52,7 @@ export const SelectFilter: FC = ({ {selectedOption?.label ?? placeholder} @@ -64,7 +66,7 @@ export const SelectFilter: FC = ({ // wide as possible. width: selectFilterSearch ? "100%" : undefined, maxWidth: POPOVER_WIDTH, - minWidth: BASE_WIDTH, + minWidth: width, }, }} > diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx index 2a69717cb8eaa..29267eb855214 100644 --- a/site/src/components/Filter/UserFilter.tsx +++ b/site/src/components/Filter/UserFilter.tsx @@ -97,9 +97,10 @@ export type UserFilterMenu = ReturnType; interface UserMenuProps { menu: UserFilterMenu; + width?: number; } -export const UserMenu: FC = ({ menu }) => { +export const UserMenu: FC = ({ menu, width }) => { return ( = ({ menu }) => { onChange={menu.setQuery} /> } + width={width} /> ); }; diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index a24a8261ff6d1..01a38a1c5077f 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -45,11 +45,14 @@ interface AuditFilterProps { user: UserFilterMenu; action: ActionFilterMenu; resourceType: ResourceTypeFilterMenu; - organization: OrganizationsFilterMenu; + // The organization menu is only provided in a multi-org setup. + organization?: OrganizationsFilterMenu; }; } export const AuditFilter: FC = ({ filter, error, menus }) => { + // Use a smaller width if including the organization filter. + const width = menus.organization && 175; return ( = ({ filter, error, menus }) => { error={error} options={ <> - - - - + + + + {menus.organization && ( + + )} } skeleton={ @@ -97,7 +102,12 @@ export const useActionFilterMenu = ({ export type ActionFilterMenu = ReturnType; -const ActionMenu = (menu: ActionFilterMenu) => { +interface ActionMenuProps { + menu: ActionFilterMenu; + width?: number; +} + +const ActionMenu: FC = ({ menu, width }) => { return ( { options={menu.searchOptions} onSelect={menu.selectOption} selectedOption={menu.selectedOption ?? undefined} + width={width} /> ); }; @@ -151,7 +162,12 @@ export type ResourceTypeFilterMenu = ReturnType< typeof useResourceTypeFilterMenu >; -const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => { +interface ResourceTypeMenuProps { + menu: ResourceTypeFilterMenu; + width?: number; +} + +const ResourceTypeMenu: FC = ({ menu, width }) => { return ( { options={menu.searchOptions} onSelect={menu.selectOption} selectedOption={menu.selectedOption ?? undefined} + width={width} /> ); }; @@ -216,9 +233,13 @@ export type OrganizationsFilterMenu = ReturnType< interface OrganizationsMenuProps { menu: OrganizationsFilterMenu; + width?: number; } -export const OrganizationsMenu: FC = ({ menu }) => { +export const OrganizationsMenu: FC = ({ + menu, + width, +}) => { return ( = ({ menu }) => { onChange={menu.setQuery} /> } + width={width} /> ); }; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index 6a27bdff375e7..5cd7a26120a25 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -35,11 +35,13 @@ export interface AuditLogRowProps { auditLog: AuditLog; // Useful for Storybook defaultIsDiffOpen?: boolean; + showOrgDetails: boolean; } export const AuditLogRow: FC = ({ auditLog, defaultIsDiffOpen = false, + showOrgDetails, }) => { const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen); const diffs = Object.entries(auditLog.diff); @@ -134,7 +136,7 @@ export const AuditLogRow: FC = ({ )} - {auditLog.organization && ( + {showOrgDetails && auditLog.organization && ( <>Org: { const { audit_log: isAuditLogVisible } = useFeatureVisibility(); + const { experiments } = useDashboard(); /** * There is an implicit link between auditsQuery and filter via the @@ -80,6 +82,7 @@ const AuditPage: FC = () => { isAuditLogVisible={isAuditLogVisible} auditsQuery={auditsQuery} error={auditsQuery.error} + showOrgDetails={experiments.includes("multi-organization")} filterProps={{ filter, error: auditsQuery.error, @@ -87,7 +90,9 @@ const AuditPage: FC = () => { user: userMenu, action: actionMenu, resourceType: resourceTypeMenu, - organization: organizationsMenu, + organization: experiments.includes("multi-organization") + ? organizationsMenu + : undefined, }, }} /> diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index 904ab00c0a287..1a2c65763d4ea 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -32,7 +32,6 @@ const defaultFilterProps = getDefaultFilterProps({ user: MockMenu, action: MockMenu, resourceType: MockMenu, - organization: MockMenu, }, }); @@ -43,6 +42,7 @@ const meta: Meta = { auditLogs: [MockAuditLog, MockAuditLog2, MockAuditLog3], isAuditLogVisible: true, filterProps: defaultFilterProps, + showOrgDetails: false, }, }; @@ -92,3 +92,18 @@ export const NotVisible: Story = { auditsQuery: mockInitialRenderResult, }, }; + +export const MultiOrg: Story = { + parameters: { chromatic: chromaticWithTablet }, + args: { + showOrgDetails: true, + auditsQuery: mockSuccessResult, + filterProps: { + ...defaultFilterProps, + menus: { + ...defaultFilterProps.menus, + organization: MockMenu, + }, + }, + }, +}; diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 70c12fa1ca294..c93193c823869 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -38,6 +38,7 @@ export interface AuditPageViewProps { error?: unknown; filterProps: ComponentProps; auditsQuery: PaginationResult; + showOrgDetails: boolean; } export const AuditPageView: FC = ({ @@ -47,6 +48,7 @@ export const AuditPageView: FC = ({ error, filterProps, auditsQuery: paginationResult, + showOrgDetails, }) => { const isLoading = (auditLogs === undefined || paginationResult.totalRecords === undefined) && @@ -117,7 +119,11 @@ export const AuditPageView: FC = ({ items={auditLogs} getDate={(log) => new Date(log.time)} row={(log) => ( - + )} /> )}