chore: add e2e tests for organization auditors

This commit is contained in:
McKayla Washburn
2025-03-12 23:05:58 +00:00
parent 5285c12b9e
commit a39a886f92
7 changed files with 197 additions and 28 deletions

View File

@ -1076,7 +1076,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
})
cookies, user, key, err := api.oauthLogin(r, params)
cookies, user, key, err := api.oauthLogin(rw, r, params)
defer params.CommitAuditLogs()
if err != nil {
if httpErr := idpsync.IsHTTPError(err); httpErr != nil {
@ -1448,7 +1448,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
})
cookies, user, key, err := api.oauthLogin(r, params)
cookies, user, key, err := api.oauthLogin(rw, r, params)
defer params.CommitAuditLogs()
if err != nil {
if hErr := idpsync.IsHTTPError(err); hErr != nil {
@ -1621,7 +1621,7 @@ func (p *oauthLoginParams) CommitAuditLogs() {
}
}
func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.Cookie, database.User, database.APIKey, error) {
func (api *API) oauthLogin(rw http.ResponseWriter, r *http.Request, params *oauthLoginParams) ([]*http.Cookie, database.User, database.APIKey, error) {
var (
ctx = r.Context()
user database.User
@ -1756,7 +1756,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
LoginType: params.LoginType,
accountCreatorName: "oauth",
RBACRoles: rbacRoles,
})
}, rw, r)
if err != nil {
return xerrors.Errorf("create user: %w", err)
}

View File

@ -202,7 +202,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
LoginType: database.LoginTypePassword,
RBACRoles: []string{rbac.RoleOwner().String()},
accountCreatorName: "coder",
})
}, rw, r)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error creating user.",
@ -485,7 +485,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
CreateUserRequestWithOrgs: req,
LoginType: loginType,
accountCreatorName: accountCreator.Name,
})
}, rw, r)
if dbauthz.IsNotAuthorizedError(err) {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
@ -1364,7 +1364,7 @@ type CreateUserRequest struct {
RBACRoles []string
}
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) {
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest, rw http.ResponseWriter, r *http.Request) (database.User, error) {
// Ensure the username is valid. It's the caller's responsibility to ensure
// the username is valid and unique.
if usernameValid := codersdk.NameValid(req.Username); usernameValid != nil {
@ -1380,8 +1380,6 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
var user database.User
err := store.InTx(func(tx database.Store) error {
orgRoles := make([]string, 0)
status := ""
if req.UserStatus != nil {
status = string(*req.UserStatus)
@ -1429,13 +1427,21 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
}
for _, orgID := range req.OrganizationIDs {
aReq, commitAudit := audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{
OrganizationID: orgID,
Audit: *api.Auditor.Load(),
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
})
aReq.Old = database.AuditableOrganizationMember{}
defer commitAudit()
_, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
OrganizationID: orgID,
UserID: user.ID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
// By default give them membership to the organization.
Roles: orgRoles,
Roles: []string{},
})
if err != nil {
return xerrors.Errorf("create organization member for %q: %w", orgID.String(), err)

View File

@ -319,7 +319,7 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
LoginType: database.LoginTypeOIDC,
// Do not send notifications to user admins as SCIM endpoint might be called sequentially to all users.
SkipNotifications: true,
})
}, rw, r)
if err != nil {
_ = handlerutil.WriteError(rw, scim.NewHTTPError(http.StatusInternalServerError, "internalError", xerrors.Errorf("failed to create user: %w", err)))
return

View File

@ -38,15 +38,25 @@ export const createUser = async (...orgIds: string[]) => {
return user;
};
export const createOrganizationMember = async (
orgRoles: Record<string, string[]>,
): Promise<LoginOptions> => {
type CreateOrganizationMemberOptions = {
username?: string;
email?: string;
password?: string;
orgRoles: Record<string, string[]>;
};
export const createOrganizationMember = async ({
username = randomName(),
email = `${username}@coder.com`,
password = defaultPassword,
orgRoles,
}: CreateOrganizationMemberOptions): Promise<LoginOptions> => {
const name = randomName();
const user = await API.createUser({
email: `${name}@coder.com`,
username: name,
name: name,
password: defaultPassword,
email,
username,
name: username,
password,
login_type: "password",
organization_ids: Object.keys(orgRoles),
user_status: null,
@ -59,7 +69,7 @@ export const createOrganizationMember = async (
return {
username: user.username,
email: user.email,
password: defaultPassword,
password,
};
};
@ -74,8 +84,7 @@ export const createGroup = async (orgId: string) => {
return group;
};
export const createOrganization = async () => {
const name = randomName();
export const createOrganization = async (name = randomName()) => {
const org = await API.createOrganization({
name,
display_name: `Org ${name}`,

View File

@ -34,7 +34,9 @@ test("create group", async ({ page }) => {
// Create a new organization
const org = await createOrganization();
const orgUserAdmin = await createOrganizationMember({
[org.id]: ["organization-user-admin"],
orgRoles: {
[org.id]: ["organization-user-admin"],
},
});
await login(page, orgUserAdmin);
@ -99,7 +101,9 @@ test("change quota settings", async ({ page }) => {
const org = await createOrganization();
const group = await createGroup(org.id);
const orgUserAdmin = await createOrganizationMember({
[org.id]: ["organization-user-admin"],
orgRoles: {
[org.id]: ["organization-user-admin"],
},
});
// Go to settings

View File

@ -0,0 +1,142 @@
import { type Page, expect, test } from "@playwright/test";
import { defaultPassword } from "../../constants";
import {
createTemplate,
createWorkspace,
currentUser,
login,
requiresLicense,
randomName,
} from "../../helpers";
import { beforeCoderTest } from "../../hooks";
import {
createOrganization,
createOrganizationMember,
createUser,
setupApiCalls,
} from "../../api";
test.describe.configure({ mode: "parallel" });
const orgName = randomName();
const orgAuditor = {
username: `org-auditor-${orgName}`,
password: defaultPassword,
email: `org-auditor-${orgName}@coder.com`,
};
test.beforeAll(async ({ page }) => {
beforeCoderTest(page);
await login(page);
await setupApiCalls(page);
const org = await createOrganization(orgName);
await createOrganizationMember({
...orgAuditor,
orgRoles: {
[org.id]: ["organization-auditor"],
},
});
await page.context().clearCookies();
});
async function resetSearch(page: Page, username: string) {
const clearButton = page.getByLabel("Clear search");
if (await clearButton.isVisible()) {
await clearButton.click();
}
// Filter by the auditor test user to prevent race conditions
await expect(page.getByText("All users")).toBeVisible();
await page.getByPlaceholder("Search...").fill(`username:${username}`);
await expect(page.getByText("All users")).not.toBeVisible();
}
test("organization auditors cannot see logins", async ({ page }) => {
requiresLicense();
// Go to the audit history
await login(page, orgAuditor);
await page.goto("/audit");
const username = orgAuditor.username;
const loginMessage = `${username} logged in`;
// Make sure those things we did all actually show up
await resetSearch(page, username);
await expect(page.getByText(loginMessage).first()).not.toBeVisible();
});
test("creating users as organization members is logged", async ({ page }) => {
requiresLicense();
// Do some stuff that should show up in the audit logs
await login(page, orgAuditor);
const username = orgAuditor.username;
// Go to the audit history
await page.goto("/audit", { waitUntil: "domcontentloaded" });
// Make sure those things we did all actually show up
await resetSearch(page, username);
// await expect(
// page.getByText(`${username} created template ${templateName}`),
// ).toBeVisible();
// await expect(
// page.getByText(`${username} created workspace ${workspaceName}`),
// ).toBeVisible();
// await expect(
// page.getByText(`${username} started workspace ${workspaceName}`),
// ).toBeVisible();
// Make sure we can inspect the details of the log item
// const createdWorkspace = page.locator(".MuiTableRow-root", {
// hasText: `${username} created workspace ${workspaceName}`,
// });
// await createdWorkspace.getByLabel("open-dropdown").click();
// await expect(
// createdWorkspace.getByText(`automatic_updates: "never"`),
// ).toBeVisible();
// await expect(
// createdWorkspace.getByText(`name: "${workspaceName}"`),
// ).toBeVisible();
});
// test("inspecting and filtering audit logs", async ({ page }) => {
// requiresLicense();
// // Do some stuff that should show up in the audit logs
// await login(page, users.templateAdmin);
// const username = users.templateAdmin.username;
// const templateName = await createTemplate(page);
// const workspaceName = await createWorkspace(page, templateName);
// // Go to the audit history
// await login(page, users.auditor);
// await page.goto("/audit");
// const loginMessage = `${username} logged in`;
// const startedWorkspaceMessage = `${username} started workspace ${workspaceName}`;
// // Filter by resource type
// await resetSearch(page, username);
// await page.getByText("All resource types").click();
// const workspaceBuildsOption = page.getByText("Workspace Build");
// await workspaceBuildsOption.scrollIntoViewIfNeeded({ timeout: 5000 });
// await workspaceBuildsOption.click();
// // Our workspace build should be visible
// await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
// // Logins should no longer be visible
// await expect(page.getByText(loginMessage)).not.toBeVisible();
// await page.getByLabel("Clear search").click();
// await expect(page.getByText("All resource types")).toBeVisible();
// // Filter by action type
// await resetSearch(page, username);
// await page.getByText("All actions").click();
// await page.getByText("Login", { exact: true }).click();
// // Logins should be visible
// await expect(page.getByText(loginMessage).first()).toBeVisible();
// // Our workspace build should no longer be visible
// await expect(page.getByText(startedWorkspaceMessage)).not.toBeVisible();
// });

View File

@ -106,7 +106,9 @@ test.describe("org-scoped roles admin settings access", () => {
test("org template admin can see admin settings", async ({ page }) => {
const org = await createOrganization();
const orgTemplateAdmin = await createOrganizationMember({
[org.id]: ["organization-template-admin"],
orgRoles: {
[org.id]: ["organization-template-admin"],
},
});
await login(page, orgTemplateAdmin);
@ -118,7 +120,9 @@ test.describe("org-scoped roles admin settings access", () => {
test("org user admin can see admin settings", async ({ page }) => {
const org = await createOrganization();
const orgUserAdmin = await createOrganizationMember({
[org.id]: ["organization-user-admin"],
orgRoles: {
[org.id]: ["organization-user-admin"],
},
});
await login(page, orgUserAdmin);
@ -130,7 +134,9 @@ test.describe("org-scoped roles admin settings access", () => {
test("org auditor can see admin settings", async ({ page }) => {
const org = await createOrganization();
const orgAuditor = await createOrganizationMember({
[org.id]: ["organization-auditor"],
orgRoles: {
[org.id]: ["organization-auditor"],
},
});
await login(page, orgAuditor);
@ -142,7 +148,9 @@ test.describe("org-scoped roles admin settings access", () => {
test("org admin can see admin settings", async ({ page }) => {
const org = await createOrganization();
const orgAdmin = await createOrganizationMember({
[org.id]: ["organization-admin"],
orgRoles: {
[org.id]: ["organization-admin"],
},
});
await login(page, orgAdmin);