Files
metabase/e2e/test/scenarios/admin-2/people.cy.spec.js
Phoomparin Mano c8f77a0925 Data Apps Permissions (#79822)
* add saved question extraction prototype

* fix additional failure cases

* refactor: reconciler

* revert me: manual skill file edit for source swapping

* set data permissions to view for databases referenced in the query

* rename data app collection and group

* prevent agents from removing savedQuestionSourceId

* docs: clarify data app query location

* chore: cleanup query-sync canonical module

* refactor: split query tests into different files

* Swap query source for data app query hooks and apply dynamic filters on top of it (#79584)

* code improvements and bug-fixes for data apps permissions

* Add data apps model imports

* address reviews on data apps permissions

* run sync queries before vite build

* Add data app resource link (#79878)

add button to view data app resources

* Show permission error when users cannot access a data app (#79874)

show permission error page when user do not have permission to view data apps

* Add data app group link in overflow menu (#79873)

Add data app group link

* Store and reconcile data app entity ids in data_app.yaml (#79833)

* Store data app resource entity IDs

* make entity ids in data_app.yaml authoritative

* add top-level value after path key

* simplify data_app.yaml manifest parser

* only allow changing data app collection if empty

* consolidate backend test helpers and remove redundant tests

* chore: restore README changes

* ci: fix failing tests

* Add support of actions to Data App reconciler (#79842)

Add support of actions to Data App reconciler

* Fix bugs/inconsistency and add more e2e for data apps reconciler/syncing (#80002)

* Update semantic schema skill about preventing segments and measures in dynamic parts of a query in Data Apps (#80288)

* Grant view data access on a table-by-table basis in data app groups (#80189)

* grant view data on table instead of database level

* refactor table scan in reconciler

* update e2e tests for permission grants

* Grant data app view permissions per table

* Skip unchanged data app permissions

* add e2e data app test for sandboxing

* reset host app sources after runs

* Preserve sandboxing for data app users (#80406)

* add e2e data app test for sandboxing

* improve e2e tests for data apps

* Remove sandbox E2E from table permissions change

* preserve existing sandboxing from sandboxed groups in data apps

* assert that we grant non-sandboxed tables

* Explain data app sandbox exclusion

* Add data app sandbox E2E coverage

* Exercise sandbox preservation in data app E2E

* Disable saved question sources in data apps (#80139)

* Disable saved question sources in data apps

* Preserve question-only schema scope

* Remove the saved question schema parameter

* Copy table-sourced metrics into data app collection and do not codegen card-sourced metrics (#80086)

* copy table-sourced metrics into the data app collections

* copy table-sourced metrics into the data app collections

* Address metric sync review feedback

* Accept keyword metric display in query response

* Fix data app metric reconciliation ordering

* Remove old lockfile format

* exclude card-sourced metrics from schema generator

* do not use card terms in metrics to avoid ambiguity with card-sourced metrics

* fix model imports

* Fix data app metric reconciliation

* refactor: cleanup reconciler unit tests

* Fix duplicated metric client helper

* add safe metrics path

* Handle unsupported metric dependencies

* Fix data app CI test regressions

* Fix "Re-adding removed query fails to adopt deleted card" (#80341)

* Clean-up data apps permissions endpoints (#80825)

* Remove entity ids from data_app.yaml (#81317)

Remove data app entity ids from data_app.yaml

* Adjust data apps permissions code (#81093)

* Remove the data app table permission grants (#81584)

* Add permissions_group.is_data_app_group and hide data app permission groups from UI (#81705)

* Add data app user management and access warnings (#81839)

* Address data app user access review feedback

* additional e2e test cases for bugs in manage users page

* fix failing frontend type check on unit test

* fix migration id collision for data apps

* combine migration changes into one file

* fix data app metric sync after trashing copied metrics

* fix cli runtime dependency packaging for embedding sdk

* avoid loading data app bundle during resource setup

* prevent redundant data app collection permission updates

* allow admins to remove data app members after token expires

* update docs and add e2e test for missing resource collection case

* isolate data app sync using transactions

* do not leave user on an empty page when removing the last member on the page

* reserve sandbox-host slug in data app config

* ignore generated data app action fixtures

* move queries into db.clj

* preserve legacy permissions when adding data app members

* regenerate the kondo modules config

The data-apps module reaches for :model/Permissions and :model/DataPermissions,
neither of which was recorded in :model-imports, so metabase.core.modules-test
fails on the current tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP

* reject data app queries whose metrics read another card

Sync copies a referenced metric but rewrites nothing inside the copy, and
copies nothing that copy in turn reads, so those references still point at
the originals. The app publishes successfully and then fails for every
viewer without access to the originals' collections.

Checking while resolving the query, rather than at codegen, also catches a
metric edited into that shape after its schema was generated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP

* exclude metrics that read a saved card anywhere from generated schemas

source-card-id only reads stage 0, so a table-sourced metric that joins a
saved question passed the codegen filter. The CLI checks the whole query and
rejects such a metric, aborting sync-resources for any app that uses it.

Checking the whole query here keeps codegen consistent with what sync
supports, and removes the database round trip that asked which of the
referenced cards were metrics -- the step that let a saved-question join
through in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP

* resolve copied resources' table dependencies through the server

Numeric source-table entries miss implicit joins: a copied model querying
Orders and reading Products.Category through a foreign key names Products
nowhere, so it was left out of the app's permission warnings.

Run the same metadata-based lookup the query endpoint uses, through a new
endpoint the CLI calls for copied models, actions and metrics, and drop the
JSON walk it replaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP

* cover the create-queries grant the database defaults leave on app groups

An app group that predates the first non-audit database is given
create-queries :query-builder-and-native by set-new-database-permissions!,
alongside the :blocked view-data it special-cases for app groups. A user with
data access through another group would gain native query authoring just by
joining the app.

The reconciler already resets it, but nothing failed when :perms/create-queries
was dropped from its list. The pairing cannot be produced through
set-database-permission!, which cascades view-data to :unrestricted for any
create-queries value other than :no, so the test writes the row the way the
defaults insert it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP

* prevent models based on saved questions from being copied

* return to a valid member page after overlapping removals

* do not resolve query objects when either enabled flag is false

* prevent shorthand properties and spreads from overriding generated ids

* prevent adding duplicate members and retry only members that are failed to add

* define schema for data app query endpoint

* reconcile only the current data app group during resource setup

* expand the schema fingerprint

* add data app group field in permission group schemas

* prevent new databases from granting query creation to data app groups

* throw validation errors when invalid query spec fields are passed

* do not fail validation on columns metadata

* add enabled metadata to query definition schema

* accept more data app specific query metadata and binning options

---------

Co-authored-by: Timofey Kachalov <timofei@metabase.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 15:41:01 +01:00

993 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import _ from "underscore";
const { H } = cy;
import { USERS, USER_GROUPS } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
COLLECTION_GROUP_ID,
NORMAL_USER_ID,
} from "e2e/support/cypress_sample_instance_data";
const { normal, admin, nocollection } = USERS;
const { ALL_USERS_GROUP, DATA_GROUP } = USER_GROUPS;
const TOTAL_USERS = Object.entries(USERS).length;
const TOTAL_GROUPS = Object.entries(USER_GROUPS).length;
const { ORDERS_ID } = SAMPLE_DATABASE;
const TEST_USER = {
first_name: "Testy",
last_name: "McTestface",
email: `testy${Math.round(Math.random() * 100000)}@metabase.test`,
password: "12341234",
};
const adminUserName = H.getFullName(admin);
const noCollectionUserName = H.getFullName(nocollection);
const normalUserName = H.getFullName(normal);
describe("scenarios > admin > people", () => {
beforeEach(() => {
// `*` matches the `?tenancy=…` variants (People tab, user modals), not just the param-less Groups-tab request.
cy.intercept("GET", "/api/permissions/group*").as("getGroups");
cy.intercept("GET", "/api/api-key").as("listApiKeys");
H.restore();
cy.signInAsAdmin();
});
describe("user management", () => {
it("should be possible to switch beteween 'People' and 'Groups' tabs and to add/remove users to groups (metabase-enterprise#210, metabase#12693, metabase#21521)", () => {
cy.visit("/admin/people");
assertTableRowsCount(TOTAL_USERS);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${TOTAL_USERS} people found`);
// A small sidebar selector
cy.findByTestId("admin-layout-sidebar").within(() => {
cy.contains("People").should("have.attr", "data-active", "true");
cy.log("Switch to 'Groups' and make sure it renders properly");
cy.findByText("Groups").click();
cy.contains("Groups").should("have.attr", "data-active", "true");
});
cy.findByTestId("admin-layout-content").findByText("Groups");
assertTableRowsCount(TOTAL_GROUPS);
cy.log(
"Dig into one of the user groups and make sure its members are listed",
);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("All Users").click();
cy.findByTestId("admin-pane-page-title").contains("All Users");
// The same list as for "People"
assertTableRowsCount(TOTAL_USERS);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${TOTAL_USERS} members`);
// We cannot add new users to the "All users" group directly
cy.button("Add members").should("not.exist");
// Navigate to the collection group using the UI
const GROUP = "collection";
cy.findByTestId("admin-layout-sidebar").findByText("Groups").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(GROUP).closest("tr").contains("4");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(GROUP).click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("4 members");
cy.button("Add members").click();
cy.focused().type(admin.first_name);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(adminUserName).click();
cy.button("Add").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("5 members");
removeUserFromGroup(adminUserName);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("4 members");
// should load the members when navigating to the group directly
cy.visit(`/admin/people/groups/${DATA_GROUP}`);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("2 members");
cy.findByTestId("admin-layout-sidebar").findByText("People").click();
showUserOptions(noCollectionUserName);
H.popover().findByText("Deactivate user").click();
clickButton("Deactivate");
cy.findByRole("link", { name: /group/i }).click();
cy.findByRole("table").findByRole("link", { name: /data$/i }).click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("1 member");
removeUserFromGroup(normalUserName);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("0 members");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Add members to get started.");
});
it(
"should allow admin to create new users",
{ tags: "@prerelease" },
() => {
const { first_name, last_name, email } = TEST_USER;
const FULL_NAME = `${first_name} ${last_name}`;
cy.visit("/admin/people");
clickButton("Invite someone");
// first modal
cy.findByLabelText("First name").type(first_name);
cy.findByLabelText("Last name").type(last_name);
//
cy.findByLabelText(/Email/).type(email);
clickButton("Create");
// second modal
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${FULL_NAME} has been added`);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Done").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(FULL_NAME);
cy.location().should((loc) =>
expect(loc.pathname).to.eq("/admin/people"),
);
},
);
it("should allow admin to create new users without first name or last name (metabase#22754)", () => {
const { email } = TEST_USER;
cy.visit("/admin/people");
clickButton("Invite someone");
cy.findByLabelText(/Email/).type(email);
clickButton("Create");
// second modal
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${email} has been added`);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Done").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(email);
});
it("should disallow admin to create new users with case mutation of existing user", () => {
const { first_name, last_name, email } = normal;
cy.visit("/admin/people");
clickButton("Invite someone");
cy.findByLabelText("First name").type(first_name + "New");
cy.findByLabelText("Last name").type(last_name + "New");
cy.findByLabelText(/Email/).type(email.toUpperCase());
clickButton("Create");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.contains("Email address already in use.");
});
it("should immediately reflect admin privileges when creating user with admin group (metabase#60241)", () => {
const { first_name, last_name, email } = TEST_USER;
const FULL_NAME = `${first_name} ${last_name}`;
cy.visit("/admin/people");
clickButton("Invite someone");
// Fill in user details
cy.findByLabelText("First name").type(first_name);
cy.findByLabelText("Last name").type(last_name);
cy.findByLabelText(/Email/).type(email);
// Add user to Administrators group
H.modal().findByRole("combobox", { name: "Groups" }).click();
H.popover().findByText("Administrators").click();
// dismiss the open dropdown so it doesn't cover the submit button
H.modal().findByText("Create user").click();
clickButton("Create");
// Close the success modal
H.modal().findByText(`${FULL_NAME} has been added`);
H.modal().findByText("Done").click();
// Verify the user appears in the list with Admin role
cy.findByTestId("admin-people-list-table").within(() => {
cy.findByText(FULL_NAME)
.closest("tr")
.within(() => {
cy.findByText("Admin").should("exist");
});
});
});
it("'Invite someone' button shouldn't be covered/blocked on smaller screen sizes (metabase#16350)", () => {
cy.viewport(1000, 600);
cy.visit("/admin/people");
cy.button("Invite someone").click();
// Modal should appear with the following input field
cy.findByLabelText("First name");
});
it("should disallow admin to deactivate themselves", () => {
cy.visit("/admin/people");
showUserOptions(adminUserName);
H.popover().within(() => {
cy.findByText("Edit user");
cy.findByText("Reset password");
cy.findByText("Deactivate user").should("not.exist");
});
});
it("should allow admin to deactivate and reactivate other admins/users", () => {
// Turn a random existing user into an admin
cy.request("PUT", `/api/user/${NORMAL_USER_ID}`, {
is_superuser: true,
}).then(({ body: user }) => {
const FULL_NAME = H.getFullName(user);
cy.visit("/admin/people");
showUserOptions(FULL_NAME);
cy.findByText("Deactivate user").click();
clickButton("Deactivate");
cy.findByText(FULL_NAME).should("not.exist");
cy.location().should((loc) =>
expect(loc.pathname).to.eq("/admin/people"),
);
cy.log("It should load inactive users");
cy.findByText("Deactivated").click();
cy.findByText(FULL_NAME);
cy.icon("refresh").click();
cy.findByText(`Reactivate ${FULL_NAME}?`);
clickButton("Reactivate");
cy.location().should((loc) =>
expect(loc.pathname).to.eq("/admin/people"),
);
});
});
it("should edit existing user details", () => {
const NEW_NAME = "John";
const NEW_FULL_NAME = `${NEW_NAME} ${normal.last_name}`;
cy.visit("/admin/people");
showUserOptions(normalUserName);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Edit user").click();
H.modal().within(() => {
cy.log("Should display error messages (metabase#46449)");
cy.findByLabelText("First name").click().clear().type(" ");
clickButton("Update");
cy.findByText(/non-blank string./).should("exist");
cy.findByLabelText("First name").click().clear().type(NEW_NAME);
clickButton("Update", { timeout: 4000 });
});
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(NEW_FULL_NAME);
cy.location().should((loc) =>
expect(loc.pathname).to.eq("/admin/people"),
);
});
it("should reset user password without SMTP set up", () => {
cy.visit("/admin/people");
showUserOptions(normalUserName);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Reset password").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`Reset ${normalUserName}'s password?`);
H.modal().within(() => {
cy.findByText("Are you sure you want to do this?");
cy.button("Cancel").should("exist");
cy.button("Get reset link").should("exist");
cy.button("Reset password").click();
});
H.modal().within(() => {
cy.findByText(`${normalUserName}'s password has been reset`).should(
"be.visible",
);
cy.findByLabelText(/^temporary password$/i).should(
"have.attr",
"type",
"password",
);
cy.findByLabelText("Toggle password visibility").click();
cy.findByLabelText(/^temporary password$/i).should(
"have.attr",
"type",
"text",
);
clickButton("Done");
});
cy.location().should((loc) =>
expect(loc.pathname).to.eq("/admin/people"),
);
});
it("should generate a password reset link without SMTP set up", () => {
cy.intercept("POST", "/api/user/*/password-reset-url").as("getResetUrl");
cy.visit("/admin/people");
showUserOptions(normalUserName);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Reset password").click();
H.modal().within(() => {
cy.findByText(`Reset ${normalUserName}'s password?`);
cy.button("Get reset link").click();
});
cy.wait("@getResetUrl");
H.modal().within(() => {
cy.findByText(`Password reset link for ${normalUserName}`);
cy.findByText(
"Share this link with the user. It will expire in 48 hours.",
);
cy.findByRole("textbox")
.invoke("val")
.should("contain", "reset_password");
cy.button("Done").click();
});
cy.location().should((loc) =>
expect(loc.pathname).to.eq("/admin/people"),
);
});
it("should not offer to reset passwords when password login is disabled", () => {
H.activateToken("pro-self-hosted");
cy.request("PUT", "/api/google/settings", {
"google-auth-auto-create-accounts-domain": null,
"google-auth-client-id": "example1.apps.googleusercontent.com",
"google-auth-enabled": true,
});
cy.request("PUT", "/api/setting", {
"enable-password-login": false,
});
cy.visit("/admin/people");
showUserOptions(normalUserName);
H.popover().findByText("Reset password").should("not.exist");
});
it(
"should reset user password with SMTP set up",
{ tags: "@external" },
() => {
H.setupSMTP();
cy.visit("/admin/people");
showUserOptions(normalUserName);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Reset password").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`Reset ${normalUserName}'s password?`);
H.modal().within(() => {
cy.button("Get reset link").should("exist");
cy.button("Reset password").click();
});
H.undoToast().within(() => {
cy.findByText(
`Password reset email sent to ${normalUserName}`,
).should("be.visible");
});
cy.log("Should not show temporary password modal");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${normalUserName}'s password has been reset`).should(
"not.exist",
);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(/^temporary password$/i).should("not.exist");
cy.log("Should close the modal");
H.modal().should("not.exist");
},
);
it("should allow to search people", () => {
cy.visit("/admin/people");
cy.findByPlaceholderText("Find someone").type("no");
cy.findByTestId("people-list-footer").findByText("6 people found");
assertTableRowsCount(6);
cy.findByPlaceholderText("Find someone").type("ne");
cy.findByTestId("people-list-footer").findByText("1 person found");
assertTableRowsCount(1);
cy.findByPlaceholderText("Find someone").clear();
cy.findByTestId("people-list-footer").findByText(
`${TOTAL_USERS} people found`,
);
assertTableRowsCount(TOTAL_USERS);
});
it("should allow group creation and deletion", () => {
const longGroupName =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
cy.intercept("POST", "/api/permissions/group").as("createGroup");
cy.intercept("DELETE", "/api/permissions/group/*").as("deleteGroup");
cy.visit("/admin/people/groups");
cy.wait(["@getGroups", "@listApiKeys"]);
cy.findByTestId("admin-panel").within(() => {
cy.button("Create a group").click();
cy.findByPlaceholderText(/something like/i).type("My New Group");
cy.button("Add").click();
cy.wait(["@createGroup", "@getGroups"]);
});
cy.log("should show API errors from group endpoints (metabase#52886)");
cy.findByTestId("admin-panel").within(() => {
cy.button("Create a group").click();
cy.findByPlaceholderText(/something like/i).type(longGroupName);
cy.button("Add").click();
});
cy.findByTestId("alert-modal").should("exist");
H.modal().findByText("Ok").click();
cy.findByTestId("admin-panel").within(() => {
cy.findByText("My New Group").closest("tr").icon("ellipsis").click();
});
H.popover().findByText("Edit Name").click();
cy.findByTestId("admin-panel").within(() => {
cy.findByDisplayValue("My New Group").clear().type(longGroupName);
cy.button("Done").click();
});
cy.findByTestId("alert-modal").should("exist");
H.modal().findByText("Ok").click();
cy.findByTestId("admin-panel")
.findByRole("button", { name: "Cancel" })
.click();
cy.findByTestId("admin-panel")
.findByText("My New Group")
.closest("tr")
.icon("ellipsis")
.click();
H.popover().findByText("Remove Group").click();
H.modal().button("Remove group").click();
cy.wait(["@deleteGroup", "@getGroups"]);
cy.findByTestId("admin-panel")
.findByText("My New Group")
.should("not.exist");
});
it("should display api keys included in a group and display a warning when deleting the group", () => {
H.createApiKey("MyApiKey", COLLECTION_GROUP_ID);
cy.visit("/admin/people/groups");
cy.wait(["@getGroups", "@listApiKeys"]);
cy.findByTestId("admin-panel")
.findByText("collection")
.closest("tr")
.findByText("(includes 1 API key)");
cy.findByTestId("admin-panel")
.findByText("collection")
.closest("tr")
.icon("ellipsis")
.click();
H.popover().findByText("Remove Group").click();
H.modal().within(() => {
cy.findByText(
"Are you sure you want remove this group and its API key?",
);
cy.button("Remove group and API key");
});
});
it("should display more than 50 groups (metabase#17200)", () => {
generateGroups(51);
cy.visit("/admin/people/groups");
cy.findByTestId("admin-layout-content").scrollTo("bottom");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("readonly");
});
describe("email configured", { tags: "@external" }, () => {
beforeEach(() => {
// Setup email server, since we show different modal message when email isn't configured
H.setupSMTP();
setupGoogleAuth();
});
it("invite member when SSO is not configured", () => {
const { first_name, last_name, email } = TEST_USER;
const FULL_NAME = `${first_name} ${last_name}`;
cy.visit("/admin/people");
clickButton("Invite someone");
// first modal
cy.findByLabelText("First name").type(first_name);
cy.findByLabelText("Last name").type(last_name);
cy.findByLabelText(/Email/).type(email);
clickButton("Create");
// second modal
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${FULL_NAME} has been added`);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.contains(
`We’ve sent an invite to ${email} with instructions to set their password.`,
);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Done").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(FULL_NAME);
});
});
describe("pagination", () => {
const NEW_USERS = 18;
const NEW_TOTAL_USERS = TOTAL_USERS + NEW_USERS;
beforeEach(() => {
generateUsers(NEW_USERS);
cy.intercept("GET", "/api/user?query*").as("users");
cy.intercept("GET", "/api/permissions/membership").as("memberships");
});
it("should allow paginating people forward and backward", () => {
const PAGE_SIZE = 25;
const footer = () =>
cy
.findByTestId("people-list-footer")
.scrollIntoView()
.should("be.visible");
cy.visit("/admin/people");
cy.findByTestId("admin-panel")
.findByText("Loading...")
.should("not.exist");
cy.wait("@users");
cy.log("Page 1");
assertTableRowsCount(PAGE_SIZE);
footer()
.should("contain", `${NEW_TOTAL_USERS} people found`)
.and("contain", `1 - ${PAGE_SIZE}`);
footer().within(() => {
cy.findByLabelText("Previous page").should("be.disabled");
cy.findByLabelText("Next page").click();
cy.wait("@users");
});
cy.log("Page 2");
assertTableRowsCount(NEW_TOTAL_USERS % PAGE_SIZE);
footer().should("contain", `${PAGE_SIZE + 1} - ${NEW_TOTAL_USERS}`);
cy.log("Back to the Page 1");
footer().within(() => {
cy.findByLabelText("Next page").should("be.disabled");
cy.findByLabelText("Previous page").click();
});
assertTableRowsCount(PAGE_SIZE);
footer().should("contain", `1 - ${PAGE_SIZE}`);
});
it("should allow paginating group members forward and backward", () => {
const PAGE_SIZE = 25;
cy.visit(`admin/people/groups/${ALL_USERS_GROUP}`);
// Total
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${NEW_TOTAL_USERS} members`);
// Page 1
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`1 - ${PAGE_SIZE}`);
assertTableRowsCount(PAGE_SIZE);
cy.findByLabelText("Previous page").should("be.disabled");
cy.findByLabelText("Next page").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Loading...").should("not.exist");
// Page 2
cy.findByTextEnsureVisible(`${PAGE_SIZE + 1} - ${NEW_TOTAL_USERS}`);
assertTableRowsCount(NEW_TOTAL_USERS % PAGE_SIZE);
cy.findByLabelText("Next page").should("be.disabled");
cy.findByLabelText("Previous page").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Loading...").should("not.exist");
// Page 1
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`1 - ${PAGE_SIZE}`);
assertTableRowsCount(PAGE_SIZE);
});
});
});
});
describe("scenarios > admin > people", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.activateToken("pro-self-hosted");
});
it("should unsubscribe a user from all subscriptions and alerts", () => {
H.getCurrentUser().then(({ body: { id: user_id } }) => {
H.createQuestionAndDashboard({
questionDetails: getQuestionDetails(),
}).then(({ body: { card_id, dashboard_id } }) => {
H.createQuestionAlert({ user_id, card_id });
H.createPulse(getPulseDetails({ card_id, dashboard_id }));
});
});
cy.visit("/account/notifications");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Question");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Dashboard");
cy.visit("/admin/people");
showUserOptions(adminUserName);
H.popover().within(() => {
cy.findByText("Unsubscribe from all subscriptions / alerts").click();
});
H.modal().within(() => {
cy.findAllByText(adminUserName, { exact: false });
cy.findByText("Unsubscribe").click();
cy.findByText("Unsubscribe").should("not.exist");
});
cy.visit("/account/notifications");
cy.findByLabelText("bell icon");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Question").should("not.exist");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Dashboard").should("not.exist");
});
it("invite member when SSO is configured metabase#23630", () => {
H.setupSMTP();
setupGoogleAuth();
cy.request("PUT", "/api/setting", { "enable-password-login": false });
const { first_name, last_name, email } = TEST_USER;
const FULL_NAME = `${first_name} ${last_name}`;
cy.visit("/admin/people");
clickButton("Invite someone");
// first modal
cy.findByLabelText("First name").type(first_name);
cy.findByLabelText("Last name").type(last_name);
cy.findByLabelText(/Email/).type(email);
clickButton("Create");
// second modal
H.modal().within(() => {
cy.findByText(`${FULL_NAME} has been added`);
cy.contains(
`We’ve sent an invite to ${email} with instructions to log in. If this user is unable to authenticate then you can reset their password.`,
);
cy.url().then((url) => {
const URL_REGEX = /\/admin\/people\/(?<userId>\d+)\/success/;
const { userId } = URL_REGEX.exec(url).groups;
assertLinkMatchesUrl(
"reset their password.",
`/admin/people/${userId}/reset`,
);
});
cy.button("Done").click();
});
cy.findByTestId("admin-people-list-table").should("contain", FULL_NAME);
});
});
describe("scenarios > admin > people > group managers", () => {
function confirmLosingAbilityToManageGroup() {
H.modal().within(() => {
cy.findByText(
"You will not be able to manage users of this group anymore.",
);
cy.button("Confirm").click();
});
}
function removeFirstGroup() {
cy.icon("ellipsis").eq(0).click();
cy.findByText("Remove Group").click();
cy.button("Remove group").click();
}
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.activateToken("pro-self-hosted");
cy.visit("/admin/people");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(normalUserName)
.closest("tr")
.findByText("2 other groups")
.click();
cy.findAllByTestId("user-type-toggle").click({ multiple: true });
cy.signInAsNormalUser();
cy.visit("/");
H.goToAdmin();
});
describe("group managers", () => {
it("can manage groups from the group page", () => {
cy.findByTestId("admin-layout-sidebar").within(() => {
cy.findByTextEnsureVisible("Groups").click();
});
// Edit group name
cy.icon("ellipsis").eq(0).click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Edit Name").click();
cy.findByDisplayValue("collection").type(" updated");
cy.button("Done").click();
// Click on the group with the new name
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("collection updated").click();
// Add "No Collection" user as a member
cy.button("Add members").click();
cy.focused().type("No");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(noCollectionUserName).click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Add").click();
// Find user row
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(noCollectionUserName).closest("tr").as("userRow");
// Promote to manager and demote back to member
cy.get("@userRow").within(() => {
cy.findByText("Member").realHover();
cy.findAllByTestId("user-type-toggle").click();
cy.findByText("Manager").realHover();
cy.findAllByTestId("user-type-toggle").click();
cy.findByText("Member");
});
// Delete the user
cy.get("@userRow").within(() => {
cy.icon("close").click();
});
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(noCollectionUserName).should("not.exist");
// Demote myself
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(normalUserName)
.closest("tr")
.within(() => {
cy.findByText("Manager").realHover();
cy.findAllByTestId("user-type-toggle").click();
});
confirmLosingAbilityToManageGroup();
// Redirected to the groups list
cy.url().should("match", /\/admin\/people\/groups$/);
// Open another group
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("data").click();
// Remove myself
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(normalUserName)
.closest("tr")
.within(() => {
cy.icon("close").click();
});
confirmLosingAbilityToManageGroup();
// Redirected to the home page
cy.url().should("match", /\/$/);
});
it("can manage members from the people page", () => {
// Open membership select for a user
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(noCollectionUserName)
.closest("tr")
.as("userRow")
.within(() => {
cy.findByText("data").click();
});
// Add the user to a group
H.popover().within(() => {
cy.findByText("collection").click();
});
cy.get("@userRow").within(() => {
cy.findByText("2 other groups");
});
// Remove the user from the group
H.popover().within(() => {
cy.findByText("collection").click();
});
cy.get("@userRow").within(() => {
cy.findByText("data");
});
// Promote and then demote the user
H.popover().within(() => {
cy.icon("arrow_up").click();
cy.icon("arrow_down").click();
});
// Find own row
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(normalUserName)
.closest("tr")
.within(() => {
cy.findByText("2 other groups").click();
});
// Demote myself from being manager
H.popover().findByLabelText("collection").click();
confirmLosingAbilityToManageGroup();
H.popover().findByLabelText("collection").should("not.exist");
// Remove myself from another group
H.popover().findByLabelText("data").click();
confirmLosingAbilityToManageGroup();
// Redirected to the home page
cy.location("pathname").should("eq", "/");
});
});
it("after removing the last group redirects to the home page", () => {
cy.findByTestId("admin-layout-sidebar").findByText("Groups").click();
removeFirstGroup();
cy.url().should("match", /\/admin\/people\/groups$/);
removeFirstGroup();
cy.url().should("match", /\/$/);
});
});
function showUserOptions(full_name) {
cy.findByText(full_name)
.closest("tr")
.within(() => {
cy.icon("ellipsis").click();
});
}
function clickButton(button_name, opts = {}) {
cy.button(button_name, opts).should("not.be.disabled").click();
}
function assertTableRowsCount(length) {
cy.findByTestId("admin-layout-content")
.get("table tbody tr")
.should("have.length", length);
}
function generateUsers(count, groupIds) {
const users = _.range(count).map((index) => ({
first_name: `FirstName ${index}`,
last_name: `LastName ${index}`,
email: `user_${index}@metabase.com`,
password: `secure password ${index}`,
groupIds,
}));
users.forEach((u) => cy.createUserFromRawData(u));
return users;
}
function generateGroups(count) {
_.range(count).map((index) => {
cy.request("POST", "api/permissions/group", { name: "Group" + index });
});
}
function getQuestionDetails() {
return {
name: "Question",
query: {
"source-table": ORDERS_ID,
},
};
}
function getPulseDetails({ card_id, dashboard_id }) {
return {
name: "Dashboard",
dashboard_id,
cards: [
{
id: card_id,
include_csv: false,
include_xls: false,
},
],
channels: [
{
enabled: true,
channel_type: "slack",
schedule_type: "hourly",
},
],
};
}
function assertLinkMatchesUrl(text, url) {
cy.findByRole("link", { name: text })
.should("have.attr", "href")
.and("eq", url);
}
function removeUserFromGroup(fullName) {
cy.findByText(fullName).closest("tr").find(".Icon-close").click();
}
const setupGoogleAuth = () => {
// Setup Google authentication
cy.request("PUT", "/api/setting", {
"google-auth-client-id": "fake-id.apps.googleusercontent.com",
"google-auth-auto-create-accounts-domain": "metabase.com",
"google-auth-enabled": true,
});
};