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 |
Field | Tracked |
---|
created_at | true |
expires_at | true |
hashed_secret | false |
id | false |
ip_address | false |
last_used | true |
lifetime_seconds | false |
login_type | false |
scope | false |
token_name | false |
updated_at | false |
user_id | true |
|
| AuditOAuthConvertState
| Field | Tracked |
---|
created_at | true |
expires_at | true |
from_login_type | true |
to_login_type | true |
user_id | true |
|
| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
display_name | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
source | false |
|
-| AuditableOrganizationMember
| Field | Tracked |
---|
created_at | true |
organization_id | true |
roles | true |
updated_at | true |
user_id | true |
username | true |
|
-| CustomRole
| Field | Tracked |
---|
created_at | false |
display_name | true |
id | false |
name | true |
org_permissions | true |
organization_id | true |
site_permissions | true |
updated_at | false |
user_permissions | true |
|
+| AuditableOrganizationMember
| Field | Tracked |
---|
created_at | true |
organization_id | false |
roles | true |
updated_at | true |
user_id | true |
username | true |
|
+| CustomRole
| Field | Tracked |
---|
created_at | false |
display_name | true |
id | false |
name | true |
org_permissions | true |
organization_id | false |
site_permissions | true |
updated_at | false |
user_permissions | true |
|
| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
| HealthSettings
| Field | Tracked |
---|
dismissed_healthchecks | true |
id | false |
|
| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
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,
diff --git a/site/src/components/Filter/SelectFilter.tsx b/site/src/components/Filter/SelectFilter.tsx
index 7521affc7efb6..e679c3fbd7572 100644
--- a/site/src/components/Filter/SelectFilter.tsx
+++ b/site/src/components/Filter/SelectFilter.tsx
@@ -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 0127637a4b69d..01a38a1c5077f 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,10 +45,14 @@ interface AuditFilterProps {
user: UserFilterMenu;
action: ActionFilterMenu;
resourceType: ResourceTypeFilterMenu;
+ // 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={
@@ -92,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}
/>
);
};
@@ -146,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}
+ />
+ );
+};
+
+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;
+ width?: number;
+}
+
+export const OrganizationsMenu: FC = ({
+ menu,
+ width,
+}) => {
+ return (
+
+ }
+ width={width}
/>
);
};
diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx
index 15e1a8e8254b4..5cd7a26120a25 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";
@@ -33,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);
@@ -132,6 +136,20 @@ export const AuditLogRow: FC = ({
)}
+ {showOrgDetails && auditLog.organization && (
+
+ <>Org: >
+
+
+ {auditLog.organization.display_name ||
+ auditLog.organization.name}
+
+
+
+ )}
{
const { audit_log: isAuditLogVisible } = useFeatureVisibility();
+ const { experiments } = useDashboard();
/**
* There is an implicit link between auditsQuery and filter via the
@@ -55,6 +61,15 @@ const AuditPage: FC = () => {
}),
});
+ const organizationsMenu = useOrganizationsFilterMenu({
+ value: filter.values.organization,
+ onChange: (option) =>
+ filter.update({
+ ...filter.values,
+ organization: option?.value,
+ }),
+ });
+
return (
<>
@@ -67,6 +82,7 @@ const AuditPage: FC = () => {
isAuditLogVisible={isAuditLogVisible}
auditsQuery={auditsQuery}
error={auditsQuery.error}
+ showOrgDetails={experiments.includes("multi-organization")}
filterProps={{
filter,
error: auditsQuery.error,
@@ -74,6 +90,9 @@ const AuditPage: FC = () => {
user: userMenu,
action: actionMenu,
resourceType: resourceTypeMenu,
+ 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 fa6ac9f17f066..1a2c65763d4ea 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"];
@@ -21,6 +26,7 @@ const defaultFilterProps = getDefaultFilterProps({
username: MockUser.username,
action: undefined,
resource_type: undefined,
+ organization: undefined,
},
menus: {
user: MockMenu,
@@ -33,9 +39,10 @@ const meta: Meta = {
title: "pages/AuditPage",
component: AuditPageView,
args: {
- auditLogs: [MockAuditLog, MockAuditLog2],
+ auditLogs: [MockAuditLog, MockAuditLog2, MockAuditLog3],
isAuditLogVisible: true,
filterProps: defaultFilterProps,
+ showOrgDetails: false,
},
};
@@ -85,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) => (
-
+
)}
/>
)}
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: {